内容纲要

前言

本章我们来谈一谈 YSoSerial 中的第一条 ACC 链 CommonsCollections1 Payload 的构造原理 .

Java 反序列化漏洞(4) – Apache Commons Collections POP Gadget Chains 剖析 一文中 , 我就已经分析过 Apache Commons Collections 这个利用链 . 但是当时我还没仔细看过 YSoSerial , 也没有去学习 Java 动态代理机制 . 所以分析重心放在了漏洞利用的 "后半段" , 即 TransformedMapLazyMap 两条 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

  1. 首先指定 YSoSerial 使用的 Payload 以及要执行的系统命令.

    通过 YSoSerial 生成指定 Payload 到文件中 , 关于文件写入的部分可以参考 <Java 反序列化漏洞(5) – 解密 YSoSerial : Java动态代理机制> .

  2. 然后开启 Tomcat 服务器 , 具体配置可以参考上面的链接 .

  3. 通过 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.classmap .

    很多不熟悉 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方法> 一文 .

    1. 在获取构造函数声明类时 , 首先会检查构造函数声明类是否为空 , 如果为空则会调用 acquireConstructorAccessor() 方法来获取构造函数声明类.

      当然这里肯定为空 , 不然怎么叫获取声明类呢 , 哈哈

    2. acquireConstructorAccessor() 方法中会先判断构造函数声明类 ConstructorAccessor 是否被创建 , 如果没有创建则通过 reflectionFactory.newConstructorAccessor() 方法来创建构造函数声明类

    3. newConstructorAccessor() 方法中一堆条件判断 , 不过判断条件都还算简单 , 可以看明白 . 我们仅需要知道 , 这里哪个 if 语句都不会进 , 直接进入 else 语句.

      具体的分析过程我这就不说明了 , 感兴趣的师傅可以参考 小李不秃前辈 的分析截图

    4. 接下来分别生成了 NativeConstructorAccessorImplDelegatingConstructorAccessorImpl 两个实例对象

      值得一提的是 , 在生成 DelegatingConstructorAccessorImpl 实例对象时 , 将参数 delegate 的值设置为 NativeConstructorAccessorImpl . 当调用 DelegatingConstructorAccessorImpl.newInstance() 方法时 , 会调用 NativeConstructorAccessorImpl.newInstance() 方法.

    5. 通过 setParent() 方法将 NativeConstructorAccessorImpl 的参数 parent 指向 DelegatingConstructorAccessorImpl 对象 , 返回 DelegatingConstructorAccessorImpl 对象 .

      这里都是 JDK 底层实现代码 , 看不明白没关系 . 我们仅需要知道到这里返回了形如上图的 DelegatingConstructorAccessorImpl 实例对象即可.


    如何使得 AnnotationInvocationHandler.memberValues 参数值为 LazyMap .
    1. 最后调用 DelegatingConstructorAccessorImpl.newInstance() 方法生成构造函数声明类 .

      通过前面我们得知 , 此时会调用 NativeConstructorAccessorImpl.newInstance() 方法

      在该方法中会调用 newInstance0() 方法进入 sun.reflect.annotation.AnnotationInvocationHandler.java 的构造方法.

      AnnotationInvocationHandler 构造方法中会对 this.typethis.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 利用链有了更深的理解 .

如果您对上述内容还有什么疑问 , 欢迎留言

最后修改日期:2020年8月12日

作者

留言

关于Transformer部分那里,初始化 TransformerChain,后面反射赋值还是有意义的。如果在一开始就把transformers这个数组放入transformerChain,就会导致构造链的时候(而不是(调用readObject的时候)自己弹自己计算机=_=

不过师傅讲的真的详细,有一些点豁然开朗233,tql

撰写回覆或留言

发布留言必须填写的电子邮件地址不会公开。