ysoserial学习与实践(二)
调用链分析
调用链分析
上篇介绍过了Apache Commons Collections,仅与此相关的调用链,ysoserial就分了7组(其实还有8、9、10)。
慢慢分析,分析多少算多少。
Commons Collections 1
代码不长,并且注释里给了调用链。
1 | package ysoserial.payloads; |
首先应该知道Java的反射机制,即在运行状态中:
对于任意一个类,都能够判断一个对象所属的类;
对于任意一个类,都能够知道这个类的所有属性和方法;
对于任意一个对象,都能够调用它的任意一个方法和属性。
这种动态获取的信息以及动态调用对象的方法的功能,称为Java语言的反射机制。
基于这个机制,Apache Commons Collections中有个特殊的接口,实现此接口的InvokerTransformer类可以通过调用Java的反射机制来调用任意函数。
了解之后回去捋链,最后的Runtime.exec()用于执行外部(恶意)命令,为了实现这个目的,需要寻找一个对象,能够存储并在特定情况下执行。
InvokerTransformer
跟进,看代码:org/apache/commons/collections/functors/InvokerTransformer.java (105-134行)
1 | public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) { |
没有问题,确实看到调用反射。iMethodName、iParamTypes、iArgs传参都可控,试试写个demo弹计算器:
1 | package ysoserial.test; |
ChainedTransformer
跟进ChainedTransformer接口,org/apache/commons/collections/functors/ChainedTransformer.java(109-125行)
1 | public ChainedTransformer(Transformer[] transformers) { |
定义Transformer数组对象,iTransformers传入transform方法遍历(这就为exp提供了多次调用的可能)。
审计CommonsCollections1中遍历了哪些操作:
实例化另一个ConstantTransformer类(序列化和Transformer两个方法),可以看到,这个类在之后实例化了Runtime.class类以执行系统命令。
尝试对此类写POC验证命令调用。
1 | package ysoserial.test; |
LazyMap
主要关注:
1 | public class LazyMap extends AbstractMapDecorator implements Map, Serializable; |
LazyMap继承了AbstractMapDecorator类,调用Map和序列化接口。get方法会返回传入key值的value值,如果没有这个值则调用本类中factory.transform为此key创建值。
factory则来自Transformer接口类:
1 | //LazyMap中 |
传入时必须要具体实例化,并且具体使用该接口的哪一个子类来实例化对象,这是可控的。
前面分析了ChainedTransformer这个类可以触发RCE,并且这个类实现了Transformer,接上这条链。
AnnotationInvocationHandler
一开始并没有找到,网上查资料了解其在jdk/jre/lib/rt.jar中。
————————————分割线————————————
等一下再继续走,解决一下前面捋下来的一些问题,回过头去详细分析一下ChainedTransformer弹计算器的POC逻辑:
1、构造一个
ConstantTransformer
,把Runtime
的Class
对象传入,当transform()
时,始终返回此对象;
2、构造一个
InvokerTransformer
,待调用方法名为getMethod
,参数为getRuntime
,当transform()
时,传入1、的结果,此时的input应该是java.lang.Runtime
。
审计InvokerTransfoemer但经过getclass()
之后,cls为java.lang.Class
,之后getMethod()
只能获取java.lang.Class
的方法。
因此才会定义待调用方法名为getMethod
,其参数为getRuntime
,invoke()
调用此方法,最终得到的是getRuntime
方法的Method
对象;
3、构造一个
InvokerTransformer
,待调用方法名为invoke
,参数空,当transform()
时,传入2、的结果。
同理,cls将是java.lang.reflect.Method
,再获取并调用其invoke()
方法————实则调用上面的getRuntime()
,得到Runtime
对象;
4、构造一个
InvokerTransformer
,待调用方法名为exec
,参数为calc(命令字符串),当transform()
时,传入3、的结果,获取java.lang.Runtime
的exec
方法,传参调用;
5、将2、3、4组装成数组,放入
ChainedTransformer
中,transform()
时,将前一个元素的返回结果作为后一个元素的参数,实现该过程。
问题1
看上去此过程的2、3、步有点绕,是否可以用下面的逻辑更清晰地构造呢?
1 | Transformer[] trans = new Transformer[] { |
这样似乎逻辑是通的,把2、3、步合并成一步,但是这样的执行是无法成功的,原因:Runtime.getRuntime()得到为一个对象,此对象也必须参与序列化过程,但Runtime本身没有Serializable接口,所以会导致序列化失败。
因此必须用InvokerTransformer逐步进行。
问题2
ysoserial的Payload是先定义好一个,包含“无效”Transformer的ChainedTransformer,等所有对象(数组中)装填完毕后再利用反射,将此数组放进去,这样做的原因是什么?
作者在一个Issue中给出解释:
Generally any reflection at the end of gadget-chain set up is done to “arm” the chain because constructing it while armed can result in premature “detonation” during set-up and cause it to be inert when serialized and deserialized by the target application.
大概就是说数据装填期间,这个链可能会被提前反序列化,或者最后处于非正常的状态,所以先把这个链的“形式”摆好,然后等后面数组做好之后利用反射顶进去。
————————————分割线————————————
继续捋链,ChainedTransformer之前已经清楚了。
之后,只需要稳定执行其transform(),LazyMap类和TransformedMap类均可以实现,因为ysoserial用的是LazyMap,所以对它进行分析。
LazyMap(装饰器)的decorate方法传入Map接口类的map和Transformer接口类的factory,get方法返回后者的value,当后者的键不存在时,调用Transformer的transform()创建。
transform运行的问题解决了,那么现在需要思考如何能够自动调用此get()方法。前面说了AnnotationInvocationHandler类在rt.jar下,Gadgets中进一步指明了其路径:
其关键部分:
1 | AnnotationInvocationHandler(Class<? extends Annotation> paramClass, Map<String, Object> paramMap) { |
看不明白也没事,先看看ysoserial里是怎么打的:
1 | final Map mapProxy = Gadgets.createMemoitizedProxy(lazyMap, Map.class); |
发现依次调用Gadgets的createMemoitizedProxy和createMemoizedInvocationHandler:
1 | public static <T> T createMemoitizedProxy ( final Map<String, Object> map, final Class<T> iface, final Class<?>... ifaces ) throws Exception { |
先看后面,createMemoizedInvocationHandler()调用了AnnotationInvocationHandler的newInstance方法,将mapProxy置为其memberValues属性。
而mapProxy则是由createMemoitizedProxy()创建的一个动态代理,此动态代理又是一个AnnotationInvocationHandler对象。
根据调用链,审计AnnotationInvocationHandler.readObject()的逻辑,果然在map.entrySet()处发现了修改值的功能,此功能调用了前面invoke(),而此函数恰好存在对于其memberValues属性的get函数使用,即可以调用LazyMap的get方法。至此,整个链打通。
写最后的POC之前还是解答一个可能的问题。
问题3
ysoserial以这种Java动态代理的方式请求invoke()的原因是什么,为什么不直接想办法调用invoke()呢?
它确实也想,但是invoke()对方法名做了很复杂的要求:
因此ysoserial以Java动态代理的方式处理LazyMap,使readObject()在调用entrySet()时代理进入AnnotationInvocationHandler.invoke(),且正好方法名entrySet顺利跳过这几个判断条件,最终实现我们的目的。
注意createMemoitizedProxy方法,也就是下面这行语句返回的值是evilMap。
1 | final Map mapProxy = Gadgets.createMemoitizedProxy(lazyMap, Map.class); |
则有POC:
1 | package ysoserial.test; |
调用链的最终目标为ObjectInputStream.readObject(),真实的反序列化场景则类似于:
服务端:
1 | package CommonsCollections1; |
EXP:
1 | package CommonsCollections1; |