0. 环境搭建
Apache Commons是Apache开源的Java通用类项目在Java中项目中被广泛的使用,Apache Commons当中有一个组件叫做Apache Commons Collections,主要封装了Java的Collection(集合) 相关类对象。
Commons的目的是提供可重用的、解决各种实际的通用问题且开源的Java代码。
包结构介绍
- org.apache.commons.collections – CommonsCollections自定义的一组公用的接口和工具类
- org.apache.commons.collections.bag – 实现Bag接口的一组类
- org.apache.commons.collections.bidimap – 实现BidiMap系列接口的一组类
- org.apache.commons.collections.buffer – 实现Buffer接口的一组类
- org.apache.commons.collections.collection –实现java.util.Collection接口的一组类
- org.apache.commons.collections.comparators– 实现java.util.Comparator接口的一组类
- org.apache.commons.collections.functors –Commons Collections自定义的一组功能类
- org.apache.commons.collections.iterators – 实现java.util.Iterator接口的一组类
- org.apache.commons.collections.keyvalue – 实现集合和键/值映射相关的一组类
- org.apache.commons.collections.list – 实现java.util.List接口的一组类
- org.apache.commons.collections.map – 实现Map系列接口的一组类
- org.apache.commons.collections.set – 实现Set系列接口的一组类
Commons Collections的maven仓库:https://mvnrepository.com/artifact/commons-collections/commons-collections/3.2.1
存在漏洞的版本:commons-collections 3.1-3.2.1;JDK 8u71之后已修复不可利⽤
实验环境: JDK-8u65、commons-collections 3.2.1
新建Maven项目,选择JDK-8u65
修改pom.xml文件
1 | <dependencies> |
查看CC的依赖包,里面都是class字节码文件;如果想要看源码发现都是反编译过来的(变量名都是v1、v2之类的),无法进行直观的审计(都是class文件,另外使用find usages的时候,IDEA是不会去class文件查找的)
JDK的源码不包含sun包,在JDK的src目录中加入sun源码包,sun目录从源码包:src/share/classes目录中拷贝到JDK的src目录下即可
配置CC模块的JDK
然后查看源码既可,以上环境就基本搭建好了
1. 前置知识
首先了解Java的命令执行:Runtime.getRuntime().exec(“calc”)
- 普通调用
1 | Runtime.getRuntime().exec("calc"); |
反射调用
1
2
3
4
5Class c = Class.forName("java.lang.Runtime");
Method getRuntimeMethod = c.getMethod("getRuntime");
Runtime r = (Runtime) getRuntimeMethod.invoke(c);
Method execMethod = c.getMethod("exec",String.class);
execMethod.invoke(r,"calc");
分析:
在IDEA中,快捷键两次shift,搜索Runtime;发现getRuntime方法是public修饰的,返回类型是Runtime类型且无参
注:Runtime类是无法进行序列化的,因为没有实现Serializable接口
2. 实战
分析commons-collections.jar包中的InvokerTransformer.class类:该类中有个transform方法,如果input对象不为null的话,就会调用下图红框中的代码(可以反射调用input对象中的某个方法);
那么如果想要命令执行的话,就可以传入Runtime类的exec方法。对比前面反射调用RCE的例子,那么transform方法的input参数就应该为Runtime.getRuntime。
继续分析InvokerTransformer.java类的构造方法:有两个构造方法,要想利用transform方法中的反射的话,就需要调用三个参数的构造方法
变量methodName赋值给iMethodName属性,paramTypes数组赋值给iParamTypes属性,args数组赋值给iArgs属性。这三个属性分别对应着要调用的方法名、方法参数类型
方法的参数。
构造POC
1 | # 1. 创建InvokerTransformer对象并调用transform方法 |
执行弹出计算器;感觉构造方法和transform也太凑巧了,很像是在写一个后门
找到恶意利用点,接下来就可以构造链子了。查找哪里调用了transform方法(本质就是找入口,一直找到readObject方法即可),在TransformedMap类中找到checkSetValue方法,该方法调用了InvokerTransformer类的transform方法
分析:
例如checkSetValue方法中调用了transform方法,而在某个类中的readObject方法中又调用了checkSetValue方法并且该类实现了序列化接口,可以被序列化,那么就可以把该类当成反序列化的入口了。
分析checkSetValue方法:直接返回valueTransformer对象的transform方法。跟踪valueTransformer对象,找到定义它的地方;最终在该类(TransformedMap类)的构造方法中找到有valueTransformer的定义
- 自己的分析方法:
可以尝试通过反射的方法来动态构造TransformedMap类的对象,并把InvokerTransformer作为参数传入。(因为TransformedMap类的checkSetValue方法中的valueTransformer属性可以通过构造方法获得)
1 | Runtime r = Runtime.getRuntime(); |
- 网上的分析方法:
因为如果想要通过TransformedMap类的构造方法传入InvokerTransformer对象的话,就需要创建TransformedMap对象。这里跟踪发现在前面有个静态方法:decorate;该方法中创建了TransformedMap对象。
因为是静态方法,可以直接通过类名来调用,而不用通过反射。构造POC
1 | Runtime r = Runtime.getRuntime(); |
跟踪到decorate方法,继续find useages,发现无法继续深入,没有对该decorate方法继续调用的地方。
所以只能回到chekcSetValue方法中,重新寻找链子;在AbstractInputCheckedMapDecorator类的内部类MapEntry有个setValue方法调用了checkSetValue方法。
继续往下分析:
跟踪到AbstractInputCheckedMapDecorator类,该类中有个内部类MapEntry的setValue方法里有对checkSetValue方法的调用。
对Java熟悉的话,看到setVlaue可能会比较敏感;map接口中有对setValue方法的定义,setValue() 实际上就是在 Map 中对一组 entry(键值对)进行
setValue() 操作。
那么只需要对map即可进行遍历,就可以调用setValue方法间接调用checkSetValue方法。
1 | map.put("key", "value"); // 键值对中要有东西,不然无法调用setValue方法 |
修改payload,如下
1 | Runtime r = Runtime.getRuntime(); |
执行payload,弹出计算器
攻击思路:
找到一个是Map的入口类,遍历这个集合,并执行 setValue 方法,即可构造 Poc。(如果一开始就能在一些readObject方法中找到可控对象(InvokerTransformer类)的transform方法,那这条链子就会很轻松;但是往往都是通过不断间接的调用,才构成链子)
继续跟踪setValue方法的调用,定位到JDK-8u65版本中的AnnotationInvocationHandler类的setValue方法(前面都是CC组件中的链子),并且该setValue方法是在readObject中的,AnnotationInvocationHandler类还实现了Serializable接口,可以被序列化。
该类中的setValue方法是通过memberValue调用的,参数memberValue又是通过memberValues集合获得的;那么只需要控制memberValues为TransformedMap类decorate方法的返回即可
分析AnnotationInvocationHandler类的构造方法:
刚好memberValues参数通过构造方法传入,且该类和构造方法的作用域都是default,只能本类中调用;就需要通过反射来获取类及构造函数,再实例化它。
通过反射,构造POC
1 | Class annotationInvocationHandlerClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); |
此时,大致的链子已经构成;但是想要正确序列化还需要满足三个问题:
- Runtime类没有实现Serializable接口,不能对其进行序列化
解决办法:
可以通过反射的方法来实例化
1 | Class c = Runtime.class; |
返回类型是Class,Class类实现了Serializeable接口
通过反射调用来RCE
1 | Class c = Runtime.class; |
上述的Runtime通过反射,虽然可以进行序列化;但是可以直接利用之前的利用点InvokerTransformer类中的transform方法来实现对Runtime的RCE调用,InvokerTransformer类本身就实现了Serializable接口,可以进行序列化。
修改代码:
1 | # 结构:new InvokerTransformer("方法名",new Class[]{},new Object[]{}).transform(); |
- 获得getMethod方法
1 | Method getMethod = (Method) new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}).transform(Runtime.class); |
- 获得inovke方法
1 | Runtime getInvoke = (Runtime) new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}).transform(getMethod); |
- 获得exec方法
1 | new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(getInvoke); |
执行结果,弹出计算器
上述通过InvokerTransformer类的transform方法实现的RCE还是有个缺点,就是每个的返回值都得手动传给下一个,比较麻烦。这里有个ChainedTransformer类的transform方法可以遍历实现自动把每个返回值传给下一个的功能。
修改代码:需要先创建一个Transform数组
分析ChainedTransformer类的transform方法:遍历Transformer数组,并把每次的执行结构返回给下一次使用
因为InvokerTransformer类和ChainedTransformer类本身都实现Transformer接口,所以创建InvokerTransformer对象的返回类型就是Transformer类型的
1 | Transformer[] transforms = new Transformer[]{ |
- AnnotationInvocationHandler类的readObject方法中需要满足两个IF条件才能调用setValue方法
在IF语句处下个断点,在poc.java文件中进行debug,成功断在IF语句处;可以看到memberType参数为null,所以进入不了IF判断
根据下图中的分析:
如果传入Override.class的话,是没有成员方法的。也就无法进入下面的IF判断。所以需要传入一个有成员方法的注解。memberTpye参数是通过memberTypes.get(name)获得的;而name是通过POC中Map集合传入获得的参数,即name的值为Map集合传入的键值
修改POC传入的参数为Target.class
1 | Object annotationInvocationHandlerObject = annotationInvocationHandlerConstructor.newInstance(Target.class,decorateMap); |
分析:可以看到memberTypes中已经有内容了,但是到446行代码处,返回的值仍然是null;该函数的意思是获取变量name的值。因为POC传入的键值是key_test,get函数匹配不到所以返回为null
修改POC
1 | map.put("value","value_test"); |
分析:Map集合传入键值为value字符串后,满足get函数的条件,获得内容,进入IF判断
继续debug,步进;内层IF判断:判断变量value的是否是memberType类的实例,这里明显不是。value的内容为value_test字符串,两者没有任何关系,直接进入内层IF判断。
- AnnotationInvocationHandler类中的setValue无法直接传入Runtime对象
分析:
setValue() 处中的参数并不可控,而是指定了 AnnotationTypeMismatchExceptionProxy 类,是无法进行命令执行的。需要换一个思路,不能从此处入手;如果能找到一个类有transform方法并且还能够控制transform方法的传参的话,就可以在链子调用到transform方法前通过这个类修改传参内容。
ConstantTransformer类可以实现上述的功能:根据构造方法的分析,只要传入什么对象就返回什么对象,而transform方法不管传入什么都返回构造方法传入的对象参数
构造最终POC
3. POC
1 | import org.apache.commons.collections.Transformer; |
运行POC,弹出计算器
4. 调用链
拓展
- 有无readObject方法的情况
当一个类实现了Serializable接口并且在类中定义了readObject方法时,在反序列化该类的过程中,readObject方法会被执行。readObject方法是在反序列化期间由Java的序列化机制自动调用的特殊方法。它的作用是在对象被反序列化后控制其自定义的处理逻辑。
当一个类实现了Serializable接口,它的对象可以被序列化和反序列化。如果在反序列化时,被反序列化的类没有定义readObject方法,Java的序列化机制会使用默认的反序列化行为。默认情况下,它会按照类的成员变量的顺序和类型来进行反序列化,然后为对象的字段赋予相应的值。