前言
本章我们来谈一谈 YSoSerial 中的第一条 ACC 链 CommonsCollections1 Payload 的构造原理 .
在 Java 反序列化漏洞(4) – Apache Commons Collections POP Gadget Chains 剖析 一文中 , 我就已经分析过 Apache Commons Collections 这个利用链 . 但是当时我还没仔细看过 YSoSerial , 也没有去学习 Java 动态代理机制 . 所以分析重心放在了漏洞利用的 "后半段" , 即 TransformedMap
和 LazyMap
两条 POP Chains 是如何执行攻击者代码的 .
而本章的重点则是漏洞利用的 "前半段" : YSoSerial 是如何构造 Apache CommonsCollections1 Payload 的.
没有了解过 Apache CommonsCollections 的师傅可以先去了解一下基础内容 , 不然本章内容可能会很难理解 .
Apache CommonsCollections1 POP Chain
CommonsCollections1 Payload 简介
ysoserial.payloads.CommonsCollections1.java
通过注释得知 CommonsCollections1 Payload 使用的是 LazyMap
利用链 , 可以看到该利用链的核心方法 LazyMap.get()
我们根据该方法将整个 POP Chain 分为 "前半段" 和 "后半段" . "后半段" 我们已经非常熟悉了 , 是一套 ChainedTransformer
+ ConstantTransformer
+ InvokerTransformer
的组合拳 . 如果不明白下列反射调用链是如何构造的 , 可以参考我上面放的链接 .
但是 "前半段" 的内容 , 网上很多帖子对这部分内容避而不谈 . 本章的重点内容是 YSoSerial , 因此我们l来分析一下这个利用链是如何构造的 .
在分析代码前 , 让我们先通过 YSoSerial 打一发 CommonsCollections1 来复现一下这条利用链 .
CommonsCollections1 Payload 复现
测试环境 : JDK1.7 + Tomcat7
-
首先指定 YSoSerial 使用的 Payload 以及要执行的系统命令.
通过 YSoSerial 生成指定 Payload 到文件中 , 关于文件写入的部分可以参考 <Java 反序列化漏洞(5) – 解密 YSoSerial : Java动态代理机制> .
-
然后开启 Tomcat 服务器 , 具体配置可以参考上面的链接 .
-
通过 Curl 向 Tomcat 服务器发送 YSoSerial 生成的 Payload .
curl "http://localhost:8080/webdemotest_war_exploded/demotest" --data-binary @/tmp/payload.ser
成功执行系统命令( 弹出计算器 )
这里我们可以关注一下报错信息 .
看不懂 , 告辞 . 好吧 , 这里的内容刚开始看确实是挺难理解的 , 我们还是从 YSoSerial 来进行分析 . 不过从报错信息中我们可以了解到 , 整个利用链的执行依赖于 Java 反射机制与 JDK动态代理机制 , 因此没有了解过这两个机制的师傅可以去先了解一下 , 或者参考下面两个链接 :
CommonsCollections1 Payload 构造原理
-
首先是获取命令参数 , 并初始化一条 transformerChain .里面初始化了一个 transformer[] 数组 , 包含一个 ConstantTransformer 实例对象.
我没有弄明白这里为什么要初始化 TransformerChain , 看上去这个操作是无意义的.
可见 , 取消初始化操作并没有影响 Payload 的生成与使用 . 我在 Google 上并没有找到这里初始化的原因 , 如果您知道原因 , 欢迎留言解读下
这里调试时我们还是将代码改回去.
-
然后是构造真正的 transformer[] 数组 , 该数组中有一组反射调用 , 用于执行
java.lang.Runtime.getRuntime().exec("系统命令")
这组反射调用大家都比较熟悉 , 我就不多说了 . 不过这里在数组最后添加了一个 ConstantTransformer 实例对象 , 注释掉这句后不影响 Payload 的生成与使用 .
-
接着就是生成 LazyMap 实例对象 , 该对象是CommonsCollections1 Payload 的核心.
LazyMap 类的构造方法是
Protected
修饰符修饰的 , 但我们可以通过LazyMap.decorate()
方法来获取其实例对象.传入的第二个参数为 Transformer 类型 , 因此调用
public static Map decorate(Map map, Transformer factory)
这个构造方法 , 最终返回一个 LazyMap 实例对象.
下面开始代码将进入 YSoSerial 的核心类 ysoserial.payloads.util.Gadgets.java
中 , 这部分内容我们慢慢来看.
ysoserial.payloads.util.Gadgets.createMemoitizedProxy()
根据函数名 , 我们可以猜测到函数可能用于创建代理
跟进该函数后 , 会看到一连串的函数调用 .
了解过 Java 动态代理的师傅会看到熟悉的东西 : Proxy.newProxyInstance()
方法 , 该方法用于创建 JDK 原生动态代理 . 此处也验证了上面的猜想 .
那么这里是如何生成动态代理的呢 ? 根据函数定义 , 调用 Gadgets.createMemoitizedProxy()
方法时至少需要传入一个 Map 类型的对象和一个以上的接口类 , 于是这里传入了 LazyMap 实例对象及 Map 类 .
随后会调用 Gadgets.createProxy()
方法来生成动态代理对象 , 并将该对象返回 . Gadgets.createProxy()
的第一个参数会调用 Gadgets.createMemoizedInvocationHandler()
方法 . 根据方法名和 JDK原生动态代理的知识我们可以猜测 , 该方法应该用于生成调用处理器( InvocationHandler
).
ysoserial.payloads.util.Gadgets.createMemoizedInvocationHandler()
该方法的返回值类型( InvocationHandler ) 直接印证了上面的猜想 : 用于生成调用处理器.
-
首先通过反射获取了参数
ANN_INV_HANDLER_CLASS
的第一个构造器.参数
ANN_INV_HANDLER_CLASS
值为sun.reflect.annotation.AnnotationInvocationHandler
. 该类是 Java 中专门用来处理注解的调用处理器 , 但 Java 中不允许直接获取该类 , 所以必须要通过反射( Reflection ) 才能拿到该类.关于什么是注解 , 不了解的师傅们可以自行查阅.
为什么要获取到该类呢 ? 实际上是因为该类
invoke()
方法中可以控制参数并调用 LazyMap.get() 方法 . 从而拼接利用链 . 在 JDK1.8 中正是通过完善该类来修复 CommonsCollections1 的.这部分内容我们放到后面再看
Reflections.getFirstCtor(ANN_INV_HANDLER_CLASS)
最终返回了一个构造器 . 这里的代码我们都比较熟悉 . 但需要注意在开启访问权限(ctor.setAccessible(true)
) 时 ,ctor.override
变量值由false
被赋值为true
. 这个点后面会用到 .
为什么
Constructor.newInstance()
方法第一个参数为Override.class
-
当获取了到
AnnotationInvocationHandler
的构造器后 , 就通过newInstance()
方法来创建实例对象 . 这一步在 Java 反射中常常用到 . 但是这里newInstance()
方法的参数是Override.class
和map
.很多不熟悉 Java 的师傅会对这个
Override.class
感到疑惑 : 这个类是哪来的 ? 于是他们随便换了个类 , 将生成 Payload 打出 , 然后打了个寂寞 .这到底是为什么呢 ? 实际上我在分析 Java 反序列化漏洞(4) – Apache Commons Collections POP Gadget Chains 剖析 就已经提到了 , 只不过当时不明白原因 , 写的也比较乱 .
在利用链执行期间有一步注解类的判断 . 我当时是这么写的 :
也就是说 , 只有参数 var1 是一个注解类 , 利用链才能继续走完 , 执行恶意代码.
看到
AnnotationInvocationHandler()
的两个参数 , 再想到此处newInstance()
的两个参数 , 您是否有个猜想 : 不会这里的参数刚好对应反序列化时AnnotationInvocationHandler()
的两个参数吧 !如果推测成立 , 那么此处
newInstance()
的第一个参数必须是一个注解类 !打开 菜鸟教程 , 搜索注解类 :
真相大白 !
Override.class
果然是一个内置注解类 !换句话说 , 如果换一个注解类( 以
Deprecated.class
为例 )作为参数 , 生成的 Payload 同样可以利用成功 .现在您应该能明白这里填写
Override.class
的原因了.
-
在进入
newInstance()
方法后 , 首先进行权限检验工作 . 例如对this.override
变量进行判断 . 当然这个变量的值在ctor.setAccessible(true)
时已经修改过了 .然后会获取构造函数的声明类 , 最后创建实例对象 . 这部分内容比较底层 , 我也搞不太明白 . 所以这里部分内容参考了 掘金-小李不秃前辈的 <深入理解Constructor之newInstance方法> 一文 .
-
在获取构造函数声明类时 , 首先会检查构造函数声明类是否为空 , 如果为空则会调用
acquireConstructorAccessor()
方法来获取构造函数声明类.当然这里肯定为空 , 不然怎么叫获取声明类呢 , 哈哈
-
acquireConstructorAccessor()
方法中会先判断构造函数声明类ConstructorAccessor
是否被创建 , 如果没有创建则通过reflectionFactory.newConstructorAccessor()
方法来创建构造函数声明类 -
newConstructorAccessor()
方法中一堆条件判断 , 不过判断条件都还算简单 , 可以看明白 . 我们仅需要知道 , 这里哪个 if 语句都不会进 , 直接进入 else 语句.具体的分析过程我这就不说明了 , 感兴趣的师傅可以参考 小李不秃前辈 的分析截图
-
接下来分别生成了
NativeConstructorAccessorImpl
和DelegatingConstructorAccessorImpl
两个实例对象值得一提的是 , 在生成
DelegatingConstructorAccessorImpl
实例对象时 , 将参数 delegate 的值设置为NativeConstructorAccessorImpl
. 当调用DelegatingConstructorAccessorImpl.newInstance()
方法时 , 会调用NativeConstructorAccessorImpl.newInstance()
方法. -
通过
setParent()
方法将NativeConstructorAccessorImpl
的参数parent
指向DelegatingConstructorAccessorImpl
对象 , 返回DelegatingConstructorAccessorImpl
对象 .这里都是 JDK 底层实现代码 , 看不明白没关系 . 我们仅需要知道到这里返回了形如上图的
DelegatingConstructorAccessorImpl
实例对象即可.
如何使得
AnnotationInvocationHandler.memberValues
参数值为 LazyMap .-
最后调用
DelegatingConstructorAccessorImpl.newInstance()
方法生成构造函数声明类 .通过前面我们得知 , 此时会调用
NativeConstructorAccessorImpl.newInstance()
方法在该方法中会调用
newInstance0()
方法进入sun.reflect.annotation.AnnotationInvocationHandler.java
的构造方法.AnnotationInvocationHandler
构造方法中会对this.type
和this.memberValues
两个参数进行赋值 , 根据调试信息可以看出 ,this.type
对应前面的注解类 ,this.memberValues
对应 LazyMap 实例对象.
这里我们拿到了又一个关键点 : 我们使得
AnnotationInvocationHandler
类的this.memberValues
参数值为 LazyMap 实例对象 . 那么只要下面能够调用LazyMap.get()
方法 , 就可以拼接 CommonsCollections1 利用链.答案就在眼前 ! 我们只需要想法调用
AnnotationInvocationHandler.Invoke()
方法即可 . 那么该如何调用呢 ? -
ysoserial.payloads.util.Gadgets.createProxy()
这里首先会生成一个数组实例对象 allIfaces , 然后将传入参数 iface 值赋给数组 allIfaces 的第一项 .
我们根据赋值顺序来看 , 这里的 iface 应该是 Map.class
然后会判断变量 iface 是否为空 , 如果不为空则通过 Proxy.newProxyInstance()
创建 JDK 原生动态代理.
这里创建 JDK 原生动态代理的过程不是我们的重点 , 因此我们不进入调试了 , 有兴趣的师傅可以跟进研究一下 . 这里你只需要知道一个知识点 :
Gadgets.class.getClassLoader() 的返回值是加载动态代理类的类加载器 .
allIfaces 是被代理的接口数组 .
ih 是动态代理关联的调用处理器 .
所有对被代理接口( allIfaces )中方法的调用都会被转发到调用处理器( ih )的
Invoke()
方法中 .
上述内容是 JDK 原生动态代理的核心机制 , 即拦截对被代理接口中方法调用 , 转发到关联调用处理器的 Invoke()
方法中
也就是说 , 这里所有对 Map 接口中方法的调用都会被转发到 AnnotationInvocationHandler.Invoke()
方法中 .这恰好符合我们的需求 : 通过调用 AnnotationInvocationHandler.Invoke()
方法来拼接利用链.
综上所述 , YSoSerial CommonsCollections1 Payload 中通过调用 Gadgets.createMemoitizedProxy()
方法生成了一个动态代理实例对象 .
ysoserial.payloads.util.Gadgets.createMemoizedInvocationHandler( mapProxy )
Gadgets.createMemoizedInvocationHandler()
方法我前面已经讲过一次了 , 这里函数调用类似 , 所以不再跟踪调试 . 这里同样是为了对 AnnotationInvocationHandler.memberValues
变量进行赋值 . 值为刚刚创建的动态代理实例对象.
这一步操作是为了目标服务器在反序列化时会调用 AnnotationInvocationHandler.readObject()
方法 , 从而调用动态代理对象的方法( 被代理的方法 ) , 从而触发拦截与转发 , 从而执行 AnnotationInvocationHandler.invoke()
方法.
空谈可能比较难理解 , 我后面讲反序列化时截个图您就明白了!
ysoserial.payloads.util.Reflections.setFieldValue()
这里就是通过 Reflection.setFieldValue()
方法覆盖 Transformer[]
数组 , 植入真正的恶意代码
我依旧没有找到这里数组中多加一项的原因 , 看上去
Transformer[4]
这一项是可有可无的 .
至此 , 整个 YSoSerial CommonsCollections1 Payload 就已经构造完毕了 . 但是我们还可以再看一下这段恶意代码是如何在目标系统上执行的 .
CommonsCollections1 Payload 利用原理
这里我们就一笔带过了 , 关于 "后半段" 的分析过程可以参考 Java 反序列化漏洞(4) – Apache Commons Collections POP Gadget Chains 剖析 .
直接定位到 LazyMap.get()
方法上 , 你就能通过函数调用栈了解整个利用过程 .
就说一个重点 :
解析 final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(mapProxy)
这里实际上是为了反序列化时会调用 sun.reflect.annotation.AnnotationInvocationHandler.readObject()
方法.
AnnotationInvocationHandler.readObject()
中会调用 this.memberValues.entrySet()
. 而此时 this.memberValues
已经被赋值为动态代理对象 , 而 entrySet()
方法恰好是 Map 接口中的方法 , 即被代理的方法 .
所有对被代理接口中方法的调用都会被转发到调用处理器的
invoke()
方法中
因此你会看到下面这一步 :
调用了 AnnotationInvocationHandler.invoke()
方法 . 此时 , this.memberValues
参数会被赋值为 LazyMap
对象
程序最终调用了 LazyMap.get()
方法 , 后面的故事不用多说 , 网络上都已经讲烂了~
整个 YSoSerial CommonsCollections1 Payload 的分析就到这里.
总结
YSoSerial CommonsCollections1 Payload 构造的非常绝妙 , 前辈们非常厉害 . 个人认为整个 Apache CommonsCollections 利用链的分析都应该结合 "服务端调试" 和 "YSoSerial调试" 两部分来进行 . 通过分析 YSoSerial 我学到了很多东西 , 也对 CommonsCollections 利用链有了更深的理解 .
如果您对上述内容还有什么疑问 , 欢迎留言
关于Transformer部分那里,初始化 TransformerChain,后面反射赋值还是有意义的。如果在一开始就把transformers这个数组放入transformerChain,就会导致构造链的时候(而不是(调用readObject的时候)自己弹自己计算机=_=
不过师傅讲的真的详细,有一些点豁然开朗233,tql
明白啦~ 谢谢(●′ω`●)