Author:Glassy@平安银行应用安全团队
引言
CommonsCollection在java反序列化的源流中已经存在了4年多了,关于其中的分析也是层出不穷,本文旨在整合分析一下ysoserial中CommonsCollection反序列化漏洞的多种利用手段,从中探讨一下漏洞的思路,并且对于ysoserial的代码做一下普及,提升大家对于ysoserial的代码阅读能力。
ysoserial的关键编码技术介绍
首先我们先去了解一下ysoserial的源码中的一些常用技术做一个简单的科普。
动态代理
动态代理比较常见的用处就是:在不修改类的源码的情况下**,**通过代理的方式为类的方法提供更多的功能。
举个例子来说(这个例子在开发中很常见):我的开发们实现了业务部分的所有代码,忽然我期望在这些业务代码中多添加日志记录功能的时候,一个一个类去添加代码就会非常麻烦,这个时候我们就能通过动态代理的 方式对期待添加日志的类进行代理。
看一个简单的demo:
Work接口需要实现work函数
public interface Work { public
Teacher类实现了Work接口
public class Teacher implements
WorkHandler用来处理被代理对象,它必须继承InvocationHandler接口,并实现invoke方法
import java.lang.reflect.InvocationHandler; import
在Test类中通过Proxy.newProxyInstance进行动态代理,这样当我们调用代理对象proxy对象的work方法的时候,实际__上__调用的是WorkHandler的invoke方法。
import java.lang.reflect.InvocationHandler;
看一下输出结果,我们再没有改变Teacher类的前提下通过代理Work接口,实现了work函数调用的重写。
before invoke...
javassist动态编程
ysoserial中基本上所有的恶意object都是通过动态编程的方式生成的,通过这种方式我们可以直接对已经存在的java文件字节码进行操作,也可以在内存中动态生成Java代码,动态编译执行,关于这样做的好处,作者在工具中也有提到:
could also do fun things like injecting a pure-java rev/bind-shell to bypass naive protections
关于javassist动态编程,我就只把关键的函数及其功能罗列一下了
//获取默认类池,只有在这个ClassPool里面已经加载的类,才能使用ClassPool pool = ClassPool.getDefault();
CommonsCollections反序列化漏洞的利用
关于CommonsCollections利用的最基础的原理,已经在很久以前都被各位大佬,分析的烂熟了,所以这里我就只点到为止了,如果有不熟悉的可以去看一下@BadCode写的分析,非常的详细。
这边先看一下基础模块:
public void CommonsCollectionsTest() { String[]
CommonsCollections造成RCE的根本原因就在于我们构造了一个特殊的ChainedTransformer类的对象,这样当我们调用这个对象的transform函数的时候,就会造成命令执行,于是,我们需要做的事情就是去寻找某个类,把包含恶意代码的transformerChain放到这个类里面,当对这个类的对象进行反序列化的时候会调用
transformerChain的transform函数。
上代码前先把pom.xml的依赖给出来
<dependencies>
注1:本次试验我这边主要使用了jdk1.7u21、jdk1.8_101、jdk1.8_171几个版本的jdk,所以关于漏洞适用的jdk可能存在总结不对的可能性,还望斧正
注2:关于试验适用的CommonsCollections版本,其实下面的每个payload基本都可以同时在3.1-3.2.1和4.0 版本适用,但是不同版本生成一些对象的方式会稍有不同,所以实验中标记的CommonsCollections适用版本是指ysoserial生成变量的时候用的是哪个版本的代码规范。
适用版本:3.1-3.2.1,jdk1.8以前
这边先上CommonsCollections1的代码,为了便于阅读,这些代码都是我从ysoserial里面抽离并简化了的,关键的部分已经给上了注释。
import java.io.*;
看一下关键部分的调用栈
当对恶意对象AnnotationInvocationHandler进行反序列化的时候,调用readObject方法,并对成员变量memberValues调用entrySet方法
由于memberValues是一个代理对象,所以回去调用该对象对应handler的invoke方法,一定要注意,这个时候的handler就是memberValues为lazyMap的handler了
由于entrySet匹配不到if语句中的判断,走到else,,从而调用_this.memberValues.get(var4)_,于是到达了lazyMap.get()了
我们代码注释里已经说过了,lazyMap的factory变量就是我们的恶意对象transformerChain,并且调用了他的transform方法,成功造成命令执行。
我们去看一下3.2.2版本的时候这个漏洞是如何修复的
private void writeObject(ObjectOutputStream os) throws IOException { FunctorUtils.checkUnsafeSerialization(class$org$apache$commons$collections
就是在调用readObject和writeObject的时候把InvokerTransformer类给拉黑了。(当然这个还是需要看一下系统的配置org.apache.commons.collections.enableUnsafeSerialization的值的,不过这个值默认是false)
适用版本:commons-collections-4.0, jdk7u21及以前 其实这个CommonsCollections2只能叫另一种利用方式, 而不能叫做CommonsCollections1的绕过,因为CommonsCollections1也是可以在commons-collections-4.0利用成功的,但是因为commons-collections-4.0删除了lazyMap的decode方法,所以需要将代码中的
Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
修改为
Map lazyMap = LazyMap.lazyMap(innerMap,transformerChain);
而且,更重要的一点,CommonsCollections2不能在3.1-3.2.1版本利用成功,根本原因在于
CommonsCollections2的payload中使用的TransformingComparator在3.1-3.2.1版本中还没有实现
Serializable接口_,无法被反序列化。_
现在我们来看一下CommonsCollections2的payload
import com.nqzero.permit.Permit;
这个payload和1在造成RCE的原理上有个不同是不再去依靠Runtime类的exec,而是使用了我们知名的7u21模 块,关于7u21模块RCE的讲解,我们也不细谈,网上大把大把的分析,只需要明白一件事情,我们只要能调用包含恶意字节码的TemplatesImpl对象的利用链中的任意函数(getOutputProperties、newTransformer等等),就能造成RCE。
先去看调用链:
反序列化时候调用PriorityQueue的readObject方法,并在函数最后调用heapify方法
heapify方法会把PriorityQueue的queue变量作为参数去调用siftDown方法
只要有comparetor就会去调用siftDownUsingComparator方法
调用comparator的compare方法,这个comparator就是我们传入的tranformer为恶意InvokerTransformer对象的TransformingComparator
成功调用到恶意tranformer的tranform方法,并把恶意TemplatesImpl作为参数传入,剩下的不在去跟。
关于4.0的补丁,和3.2.2的时候一模一样,就是把InvokerTransformer给拉黑了。
适用版本:3.1-3.2.1,jdk7u21及以前
CommonsCollections3出现了一个我们在CommonsCollections1中没怎么见到过的InstantiateTransformer,先去看一下这个InstantiateTransformer的transform方法
public Object transform(Object input) { try {
简单来说这个transform方法的用处就是调用input参数的构造函数,并且这个类的两个成员变量就是传给构造函数的参数类型和参数值。其他的地方就和CommonsCollections1差不多了。
看一下代码,考虑到代码中用到的一些关键函数在前面两个payload里面已经给出了,所以接下来的代码里面我就只给出main函数代码了。
public static void main(String[] args) throws Exception { String command = "open /Applications/Calculator.app/"; Object templatesImpl = createTemplatesImpl(command);
看一下调用栈:
基本所有地方都和CommonsCollections1差不多,唯一的区别就在于循环调用的transformers变成了这个样子
final Transformer[] transformers = new Transformer[] { new ConstantTransformer(TrAXFilter.class), new InstantiateTransformer(
ConstantTransformer返回了TrAXFilter的对象给到InstantiateTransformer的tranform方法,最终调用TrAXFilter的构造函数,并把恶意的templatesImpl作为参数给到构造函数
看一下TrAXFilter的构造函数会去调用传入的恶意templatesImpl的newTransformer方法,正好符合我们的期望,造成RCE。
看一下3.2.2版本的修复补丁,和CommonsCollections1一样,就是把InstantiateTransformer类给拉黑了。
适用版本:4.0,jdk7u21及以前
CommonsCollections4这个payload认真的说,完全没有任何新的东西,其实就是把CommonsCollections2和CommonsCollections3做了一个杂交。不过从中我们可以窥探到,其实不论是4.0版本还是3.1-3.2.1版本,利用的方法基本都是可以共通的。
看一下代码:
public static void main(String[] args) throws Exception { String command = "open /Applications/Calculator.app/"; Object templates = createTemplatesImpl(command);
看一下调用栈:
代码在上面都是分析过的,简要的文字分析一下:反序列化的时候调用PriorityQueue的readObject方法,从而调用了TransformingComparator中恶意的ChainedTransformer对象的tranform方法,通过循环调用ChainedTransformer中iTransformer对象的tranform方法,进而调用TrAXFilter的构造方法,从而造成命令执行。
不过关于4.1版本的修复和3.2.2是存在不一样的地方的,3.2.2对InstantiateTransformer类的处理是拉入黑名单,而4.1版本选择把InstantiateTransformer类的反序列化接口给删除了。
适用版本:3.1-3.2.1,jdk1.8(1.9没试)
CommonsCollections5和前面几个payload有些不太一样的地方,因为jdk在1.8之后对AnnotationInvocationHandler类做了限制,所以在jdk1.8版本就必须找出能替代AnnotationInvocationHandler的新 的可以利用的类,所以BadAttributeValueExpException就被发掘了除了,我们直接根据代码来了解这个类。
public static void main(String[] args) throws Exception { String command = "open /Applications/Calculator.app/"; final String[] execArgs = new String[] { command };
这个利用链很简单
在BadAttributeValueExpException的readObject中,会调用它的成员变量val(也就是我们传入的恶意TiedMapEntry对象)的toString方法
在TiedMapEntry中会调用自身的getKey和getValue方法
在getValue方法中会调用成员变量map(也就是我们传入的恶意LazyMap对象)的get方法
接下来就和CommonsCollections1是一样的了,不再分析。
CommonsCollections在3.2.2版本的时候同样将BadAttributeValueExpException拉入了黑名单。
适用版本:3.1-3.2.1,jdk1.7,1.8均可成功
CommonsCollections6是一个实用性比较广的payload,和上面五个payload相比,它的利用受jdk版本的影响是最小的,先看代码。
public static void main(String[] args) throws Exception { String command = "open /Applications/Calculator.app/"; final String[] execArgs = new String[] { command };
CommonsCollections6的代码是最可以体现出使用反射机制生成恶意对象的优势的代码,我们看一下上面payload中使用反射机制的代码
HashSet map = new HashSet(1); map.add("foo");
上面给出的这么一长串代码,其实如果不使用反射机制去生成恶意对象,只需要两行代码
HashSet map = new HashSet(1); map.add(entry);
两个代码生成的map对象是一摸一样的对象,但是使用第二种方式,你会发现在反序列化的时候无法造成
RCE(第二种同样会造成RCE但是是发生在map.add的时候而不是ois.readObject()的时候),原因就出在了lazyMap类的get函数处。
触发RCE的关键就在于_this.factory.transform(key),然而想走到这一步,需要一个条件:!this.map.containsKey(key),翻译一下就是加载的这个key之前未被get函数调用过,并且一旦调用过一次 后,就会直接把这个key-value对放进this.map中,下次调用直接走else语句,而不会再去调用_this.factory.transform(key)。
这样的话,当我们通过*map.add(entry)*的方式去生成恶意HashSet对象的时候,看一下add方法的调用栈
add方法本身就会调用LazyMap的get方法,这样的话,我们需要造成RCE的map在还没进行反序列化的时候,就已经被put到this.map中去了,到了反序列化企图造成rce的时候,调用 LazyMap的get方法,不会再去走if语句而走到else里面去了,从而无法造成命令执行。
当然这种情况并不是无解的,我们可以将上述两行代码中做一下修改变成这种形式:
HashSet map = new HashSet(1); map.add(entry); lazyMap.remove("foo");
就是记得把lazyMap中的this.map记得删除一下就完事了,也很简单~~~
现在我们去看一下使用反射机制生成恶意对象从而在反序列化后造成RCE的调用链:
调用HashSet的readObject方法进行反序列化,将恶意的TiedMapEntry对象带入put函数
在put函数中会把恶意的TiedMapEntry对象放入hash函数中
在hash函数中调用了恶意的TiedMapEntry对象的hashCode函数
在hashCode函数中会调用恶意的TiedMapEntry对象自身的getValue函数
在getValue函数中调用this.map的get函数
在get函数中调用恶意TiedMapEntry的恶意factory对象的tranform方法,从而造成rce,这里面可以看到这个时候this.map是空的,所以我们能成功进入到if语句中。
因为这个payload也是用到了InvokerTransformer类的,所以修复方案和第一个payload是一样的。
适用版本:3.1-3.2.1,jdk1.7,1.8均可成功
CommonsCollections7也是一个适用性比较好的payload,在多种版本jdk中都可以执行成功,它的坑点和CommonsCollections6都有着一定的相似性。可以窥探到当把lazyMap作为key传入到hashset或者hashtable的时候往往都会对lazyMap本身的map参数造成一定影响,而这种影响很容易导致rce的失败。
看一下代码,关键的两步我都在代码里面加上了注释。
这个代码的坑就在于当调用*hashtable.put(lazyMap2, 2)*的时候会因为put函数的一系列操作把lazyMap2变成了我们不期望的模样。
可以看到lazyMap中的map多了一个yy->yy,其实一旦出现这种情况我们就知道肯定和lazyMap的get函数有关, 打个断点看一下什么情况。
遇到这种情况处理起来也很简单,*lazyMap2.remove("yy")*就完事了。
OK,明白了payload的生成代码,接下来我们就去看一下反序列化时候的利用链。
在Hashtable的readObject方法中会把每个key-value往table里面丢
从往table中丢第二个map的时候,就需要开始让它的key和之前的key进行对比,看看有没有重复以决定是新添加一个map还是覆盖原有的
然后经过两个equals函数后自然而然的要去调用对于map的get函数以获取值,以做修改,于是就又来到了我们熟悉额lazyMap的get函数,从而调用tranform方法导致了RCE
===
1.ChainedTransformer:循环调用成员变量iTransformers数组的中ransformer中的tranform方法。
2.InvokerTransformer:通过反射的方法调用传入tranform方法中的inuput对象的方法(方法通过成员变量iMethodName设置,参数通过成员变量iParamTypes设置)
3.ConstantTransformer:返回成员变量iConstant的值。
4.InstantiateTransformer:通过反射的方法返回传入参数input的实力。(构造函数的参数通过成员变量iArgs传入,参数类型通过成员变量iParamTypes传入)
1. lazyMap:通过调用lazyMap的get方法可以触发它的成员变量factory的tranform方法,用来和上一节中的Tranformer配合使用。
2. TiedMapEntry:通过调用TiedMapEntry的getValue方法实现对他的成员变量map的get方法的调用,用来和lazyMap配合使用。
3. HashMap:通过调用HashMap的put方法实现对成员变量hashCode方法的调用,用来和TiedMapEntry配合使用(TiedMapEntry的hashCode函数会再去调自身的getValue)。
五大反序列化利用基类
1. AnnotationInvocationHandler:反序列化的时候会循环调用成员变量的get方法,用来和lazyMap配合使用。
2. PriorityQueue:反序列化的时候会调用TransformingComparator中的transformer的tranform方法,用来直接和Tranformer配合使用。
3. BadAttributeValueExpException:反序列化的时候会去调用成员变量val的toString函数,用来和TiedMapEntry配合使用。(TiedMapEntry的toString函数会再去调自身的getValue)。
4. HashSet:反序列化的时候会去循环调用自身map中的put方法,用来和HashMap配合使用。
5. Hashtable:当里面包含2个及以上的map的时候,回去循环调用map的get方法,用来和lazyMap配合使用。