前言
最近在看p牛的Java安全漫谈反序列化篇,详细分析了CommonsCollections 这条链。
环境
jdk 版本 <=8u70
Apache Commons Collections <= 3.2.1
CommonsCollections 分析
先给出完整的利用链
1 | Transformer[] transformers = new Transformer[]{ |
利用点
从利用点开始分析
InvokerTransformer->transform
1 | public Object transform(Object input) { |
这个方法可以实现任意方法执行,通过传入一个对象,然后反射调用该对象的方法执行,通过查看该对象的构造方法,发现方法名、参数名可控。
1 | public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) { |
接下来通过该对象执行命令
1 | Runtime runtime = Runtime.getRuntime(); |
寻找利用链
刚刚是我自己调用了transform
方法,接下来来看看有哪个方法中调用了transform
。
点进transform
方法,find Usages
查看引用。
上面的21处都引用了transform
方法,LazyMap
和TransformedMap
类都能可以利用,这里选择TransformedMap
进行进一步利用。
在TransformedMap
类中checkSetValue
方法调用了transform
方法。
接下来看看什么地方引用了checkSetValue
方法。
在TransformedMap
的父类AbstractInputCheckedMapDecorator
找到 MapEntry
类的setValue
中调用了checkSetValue
。
怎么去执行这个setValue呢?
每个Entry其实就是每个(key,value),通过for(Map.Entry entry:transformedMap.entrySet())
去遍历每个Enety,就可以调用setValue方法,然后就会执行checkSetValue,进而调用transform方法执行命令。
1 | Runtime runtime = Runtime.getRuntime(); |
目前为止,我们可以通过setValue方法设置map的值来触发命令执行。我们都知道反序列化会自动执行readObject方法,我们去查找哪个类的readObject方法引用了setValue方法。
有34处引用了setValue方法,看看有没有readObject方法引用setValue方法。
在AnnotationInvocationHandler
类中的readObject方法调用了setValue。
1 | private void readObject(java.io.ObjectInputStream s) |
看看这个类的构造方法
发现需要传入两个参数,一个需要传入一个注解,另一个个可以控制memberValues
属性,而恰好就是通过该属性调用了setValue
方法,这就很棒。
接下来创建该对象
1 | Class<?> annotationInvocationHandler = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); |
该类比较特殊,需要通过反射才能拿到,传入的是一个Override
注解,这个注解在后面会有一个坑,待会调试后再来调整。
接下来对最终的AnnotationInvocationHandler对象进行序列化,然后再将其反序列化,看看是否可以成功执行刚刚构造的InvokerTransformer->transform
。
1 | Runtime runtime = Runtime.getRuntime(); |
发现执行后,只输出了序列化后的数据,并没有执行到命令,接下来打个断点进行调试下。
这里有个坑,就是断点打在上面这条语句是断不到的。
执行readObject
方法的过程中,需要满足一个条件 memberType != null
, 调试中memberType
的值为null
。
那么要怎么才能满足这个条件呢?
因为看源码比较绕,通过看p牛的文章结合自己的思考,直接给出条件。
在对AnnotationInvocationHandler初始化的时候,第一个参数需要传入一个注解,这个注解当中要有变量,对map进行赋值,它的key 要跟注解当中的变量同名。
1
2String name = memberValue.getKey(); // 拿到map的键名
Class<?> memberType = memberTypes.get(name); // 在通过键名在注解中进行查找
最开始,传入的是Override.class
1 | Object o = constructor.newInstance(Override.class, transformedMap); |
里面并没有定义任何变量,在Retention
,发现一个value
接下来将Override.class
替换成Retention.class
Map的的键名改成value
1 | Runtime runtime = Runtime.getRuntime(); |
再次进行调试
可以发现memberType
已经不为空。
当我再次进行反序列化的时候,却报了个错
说的是exec
这个方法 在class sun.reflect.annotation.AnnotationTypeMismatchExceptionProxy
不存在。
原因是Runtime
这个类并没有实现序列化接口,不能够直接进行序列化,需要进行反射调用。
1 | Transformer[] transformers = new Transformer[]{ |
接下来,涉及到关键的三个类ChainedTransformer
ConstantTransformer
InvokerTransformer
ChainedTransformer
构造方法
构造方法的作用很简单,就是接收一个transformer数组,然后将该数组赋值给了类属性this.iTransformers。
transformer方法
这个方法的作用是遍历传入的transformer数组,里面保存的都是实现了transformer接口的对象,会挨个调用每个对象tranform方法,并且第一次调用后会将返回的结果作为下一个对象的参数再次调用。(放一张p牛的图)
ConstantTransformer类
构造方法 和 transform方法
这个类的构造方法会接收一个对象,并且将该对象赋值给this.iConstant,调用 transform方法就会返回该对象。上面代码传入的是Runtime.getRuntime()
。执行transform方法后将得到一个Runtime类的实例。
InvokerTransformer类
构造方法
transform方法
这个方法的功能是传入一个对象,然后通过反射拿到这个对象的类,在通过类拿到和方法名、参数类型拿到这个方法,在调用该方法。简单来说就是通过传入一个对象拿到指定方法,传指定参数进行调用,并且对象、方法名、参数都可控。
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"})
再回过头看看刚刚的代码
1 | Transformer[] transformers = new Transformer[]{ |
只需要执行new ChainedTransformer(transformers).transform
,就会将transformers
数组中的四个对象链接起来,最后执行命令。
1 | // new ConstantTransformer(Runtime.class) |
用最笨的办法将上面的语句连接起来执行,最后成功执行了命令。
有点像俄罗斯套娃,利用反射获取反射。
完整利用链
至此,一个完整的CC1利用链就构造完成了。
1 | package org.example; |
总结
这条链对于我这种Java安全新手来说确实还是有很大难度,因为是第一次分析反序列化链条,后序还有很多链条会分析,所以就详细分析cc1,为之后分析打好基础,也陆陆续续分析了几天,踩了很多的坑,看了很多人的博客,理清楚每个知识点。其实这个过程还是很有意思的,再把整个过程总结出来,更能发现那些地方没搞懂。
参考文章&视频
https://blog.chaitin.cn/2015-11-11_java_unserialize_rce/
https://www.yuque.com/tianxiadamutou/zcfd4v/hsh32p
Java安全漫谈反序列化篇4