内容纲要

前言

上一章我分析了 CommonsCollections1 这条利用链 . 这一次我们来分析一下 CommonsCollections2 这条利用链.


CommonsCollections2

CommonsCollections2 利用链是 YSoSerial 中比较好玩的一条利用链 , 用了很多我之前没有接触过的知识点 . 本章我们就详细分析一下前辈们都用了哪些骚操作 .


基础知识

首先我们需要了解一些知识点 , 这些知识点在后面分析利用链时都会用到 .

JAVAssist 字节码增强类库

JAVAssist( JAVA Programming ASSISTant ) 是一个开源的分析 , 编辑 , 创建 Java字节码( Class )的类库 . 该类库位于 JBOSS 应用服务器项目中 , 用于为 JBOSS 实现动态 "AOP" .

该类库的优点在于简单 , 快速 , 直接使用 Java 编码格式就能动态改变类的结构或动态生成类 , 而不需要了解 JVM 指令 .

关于 JAVAssist 的技术问题可以参考 程序诗人前辈翻译的 Javassist中文技术文档 , 原文 Getting Started with Javassist .

这里我们的重点是如何通过 JAVAssist 动态修改类结构 来看下面这个例子

运行这段代码后 , 会在 /home/epicccal/Documents/idea/webdemotest/src/javatest/ 路径下生成新的字节码文件 .

可见 , 动态生成的类在我们原有类的基础上添加了静态代码块 , 并注入了新的内容 . 如果我们可以加载新生成的类 , 那么就会执行静态代码块中的内容 , 执行指定的恶意代码 .

来看一看上面这个例子是怎么构造的 .

  • ClassPool pool = ClassPool.getDefault();

    如果想要修改一个类 , 就得拿到这个类 , 因此我们需要先指定系统的类搜索路径 .

    ClassPool.getDefault() 方法会查找系统默认路径( JVM类搜索路径 )来搜索需要的类 .

  • CtClass cc = pool.get(javas.class.getName());

    ClassPool 中有一张保存 CtClass 信息的 HashTable . 在该 HashTable 中 key 为类名 , value 为类对应的 CtClass 对象

    CtClass( compile-time class , 编译时类信息 ) 是一个 Class 文件在代码中的抽象表现形式 , 用于处理类文件 .

    可以通过 ClassPool.get() 方法来创建 CtClass 对象 , 将该对象放入 ClassPool 的 HashTable 中 , 并返回创建的 CtClass 对象.

    有了 CtClass 实例对象后 , 我们就可以处理类文件 , 编辑或者修改类了 . 这里我们获取的是 javas.Class 的实例对象 , 因此我们可以修改 javas 类

  • String cmd = "java.lang.Runtime.getRuntime().exec(\"mate-calc\");";

    这行没什么好说的 , 调用 java.lang.Runtime.exec() 方法来执行系统命令.

    需要注意的是 : 我们添加的内容都是完整的 java 源代码 , 因此引号要进行转义处理 , 行末的分号也不能丢掉.

  • cc.makeClassInitializer().insertBefore(cmd);

    这里先通过 CtClass.makeClassInitializer() 方法在当前类( javas ) 中创建了一个静态代码块

    然后调用 CtBehavior.insertBefore() 方法在静态代码块的开头插入源代码

  • cc.setName("Epicccal" + System.nanoTime());

    通过 CtClass.setName() 方法来设置类名 , 这里 setName() 方法的参数是一个全限定名称 , 因此我们可以设置 a.b 这样的类名 .

    System.nanoTime() 方法提供纳秒级的精度 , 这里只是为了不让类名重复

  • cc.writeFile("/home/epicccal/Documents/idea/webdemotest/src/javatest/");

    可以通过 CtClass.writeFile() 方法将生成的类写入文件 , 也可以通过 CtClass.toClass() 方法拿到生成的类 , 再通过 newInstance() 方法获取实例对象.

    在类实例化前 , JVM会加载该类 , static{} 代码块的内容会在类加载时被执行 , 调用 java.lang.Runtime.getRuntime().exec() 方法执行系统命令 , 弹出计算器 .

    这个知识点比较重要 , 是 YSoSerial 构造 CommonsCollections2 Payload 的核心之一 .


PriorityQueue 优先级队列

PriorityQueue 优先级队列是基于优先级堆的一种特殊队列 , 它满足队列 " 队尾进 , 队头出 " 的特点 , 但队列中每次插入或删除元素时 , 都会根据比较器( Comparator )对队列进行调整 .

缺省情况下 , 优先级队列会根据自然顺序对元素进行排序 , 形成一个最小堆( 父节点的键值总是小于或等于任何一个子节点的键值 ) . 当指定了比较器后 , 优先级队列会根据比较器的定义对元素进行排序.

PriorityQueue 优先级队列的函数定义比较简单 , 比较重点的方法解析如下 :

  • add() : 在优先级队列的队尾插入元素 , 插入失败则抛出异常.
  • offer() : 在优先级队列的队尾插入元素 , 插入失败则返回 null.
  • element() : 获取但不删除优先级队列的队首元素 , 获取失败时抛出异常.
  • peek() : 获取但不删除优先级队列的队首元素 , 获取失败时返回 null.
  • remove : 获取且删除优先级队列的队首元素 , 获取失败时抛出异常.
  • poll() : 获取且删除优先级队列的队首元素 , 获取失败时返回 null.

PriorityQueue 优先级队列的用法看下面这两个例子您就明白了 .

  1. 不单独定义比较器 , 使用默认的比较器

    缺省情况下 , 优先级队列每次弹出的元素都是队列中的最小值

  2. 单独定义比较器 , 优先级队列每次比较时会根据比较器返回值的正负进行判断 .

    通过定义比较器 , 使得优先级队列中的元素形成一个最大堆 , 堆顶为所有元素的最大值.


CommonsCollections2 Payload 简介

ysoserial.payloads.CommonsCollections1.java

看到了熟悉的 InvokerTransformer.transform() 方法 , 该方法中有一组可控的反射调用 , 可用于调用任何一个方法.

而其它的方法我们就都没有见过了 , 所以我们从 YSoSerial 开始学习 .


CommonsCollections2 Payload 复现

按照惯例 , 还是先复现一遍看看 .

复现环境 : JDK1.7_67 + Tomcat7 + CommonsCollections4-4.0

  1. 首先还是拿 YSoSerial 生成指定 Payload

    这里要执行的命令依旧是弹出计算器

  2. 在 Tomcat 服务器上 加载 CommonsCollections4-4.0 组件 , 并启动 Tomcat 服务

    可以在 MVNRepository 上直接下载 CommonsCollections4-4.0.jar 组件.

  3. 将保存在本地的恶意序列化数据发送到 Tomcat 服务器上

    curl "http://localhost:8080/webdemotest_war_exploded/demotest" --data-binary @/tmp/payload.ser

    成功执行恶意代码 , 弹出计算器 . 黄框内的内容为反序列化漏洞的核心 , 本章的重点就是研究这段方法调用链是怎么来的 .


CommonsCollections2 Payload 构造原理

在 YSoSerial CommonsCollections2 Payload 的第一行 , 会将我们要执行的命令带入 ysoserial.payloads.util.Gadgets.createTemplatesImpl() 方法中 .

这里会先通过 System.getProperty() 方法获取系统属性 properXalan 的值.

  1. 如果返回值为 true 则通过 Java 反射机制( class.forName()方法 )获取 TemplatesImpl , AbstractTranslet , TransformerFactoryImpl 三个类的全限定类名 , 将它们作为参数并调用 createTemplatesImpl() 的重载方法中 .

  2. 如果返回值为 false( 默认为 false ) 或者没有找到 properXalan 属性 , 则直接将上述三个类的非限定类名作为参数带入 createTemplatesImpl() 的重载方法中 .

这里没有找到系统属性 properXalan , 因此if 条件取得默认值 false.

Xalan 是 Java 中一个 XSLT 处理器 , 可用于将 XML 文档转换为 HTML 文档 , 文本文档或其他XML文档类型.


ysoserial.payloads.util.Gadgets.createTemplatesImpl()

该方法中首先获取了 TemplatesImpl 实例对象 .

然后会通过 JAVAssist 技术获取并修改 StubTransletPayload 类.

  1. 通过 ClassPool.getDefault() 方法获取默认的类搜索路径 , 即 JVM 类搜索路径 .
  2. 通过 ClassPool.insertClassPath() 方法手工添加类搜索路径 . 这里起到一个保险作用 , 其实注释掉这两行后 , 同样能成功生成 Payload .
  3. 通过 ClassPool.get() 方法获取 StubTransletPayload 类并创建为 CtClass 实例对象 . CtClass 对象是可以被动态创建修改的 .
  4. 通过 CtClass.makeClassInitializer().insertAfter() 方法在 StubTransletPayload 类定义的最后添加静态代码块
  5. 将修改后的类重命名为 ysoserial.Epicccal + 系统时间.class

接着通过 ClassPool.get() 方法获取 AbstractTranslet 类 , 并将该类作为新建类的父类.

这一步实际上也是起到双保险作用 , 我们知道新建类是修改自 StubTransletPayload 类的 , 而 StubTransletPayload 类本身就继承 AbstractTranslet 类 , 所以即使没有这一步 , 新建类同样会继承 AbstractTranslet 类.

我还添加了一步写入文件的操作 , 新建类的内容和上文思路完全一致 !

不过 YSoSerial 在这里直接通过 CtClass.toBytecode() 方法获取恶意类的字节码 , 并通过 Java 反射机制将字节码填充到 TemplatesImpl 实例对象的 _bytecodes 属性数组中.

这里除了向 _bytecodes 数组中注入恶意类字节码外 , 还注入了一个 Foo.class 类的字节码 , 该类的定义如下 .

这是一个空类 , 应该没有什么作用 . 如果删除该类 , 同样可以生成 Payload 并攻击成功 .

博瑞的西门吹雪 前辈的 缩小ysoserial payload体积的几个方法 一文中同样提到了这个点

因此这里可能只是为了代码规范 , 所以多注入了一个 Foo.class 字节码

最后代码还填充了 TemplatesImpl 实例对象的两个字段 "_name" 和 "_tfactory" . 并将填充后的 TemplatesImpl 对象返回 .

这里 "_name" 字段是必须填充的 , 而 "_tfactory" 字段是可选填充的.

  1. 不填充 "_name" 属性字段 , 生成的 Payload 无法利用成功.

  1. 不填充 "_tfactory" 字段 , 生成的 Payload 可以利用成功.

具体的原因我放在后面调试 Tomcat 服务器时来说.

至此 , 我们得到了携带恶意类字节码的 TemplatesImpl 实例对象 .


new InvokerTransformer("toString", new Class[0], new Object[0])

接着获取了 InvokerTransformer 实例对象.

根据注释 , 我们可以猜测这里是为了模拟 iMethodName 字段 , 起到初始化的作用 .

看过那段可控反射调用的师傅应该知道 : 反射时调用什么方法取决于 this.iMethodName 字段值 . 因此这里的 toString() 只是起到占位的作用 , 没有实际意义.


new PriorityQueue<Object>(2,new TransformingComparator(transformer))

这里创建了一个优先级队列 , 指定了队列的初始容量与比较器 , 然后填充了两个 "1" 来占位初始化.

这里通过 new TransformingComparator(transformer) 来获取构造器实例对象 . 这个地方很有意思 .

刚才我们定义了一个 InvokerTransformer 类型的 transformer 实例对象 , 现在我们将该对象带入 TransformingComparator 的构造方法中 , 赋值给 this.transformer 参数 .

而优先级队列每次比较时都会调用比较器的 compare() 方法 , 当服务端执行到这里时 , 就会调用 TransformingComparator.compare() 方法 .进而调用 this.transformer.transform(obj1) 方法 , 即执行 : InvokerTransformer.transform() 方法 , 进入可控的反射调用.


Reflections.setFieldValue(transformer, "iMethodName", "newTransformer")

修改了transformer 实例对象的 iMethodName 字段值为 newTransformer . 这样服务端在执行 InvokerTransformer.transform() 方法时 , 就会反射调用 newTransformer() 方法

为什么要调用 newTransformer() 方法我们同样放在调试服务端时来说 .


Reflections.getFieldValue(queue, "queue")

这里获取了优先级队列的实例对象 , 并修改其字段值 . 将我们通过 JAVAssist 构造的恶意类注入其中.

这样修改后 , 优先级队列调用比较器的 compare() 方法时会去比较 "templates" 和 "1" 的值 , 即调用执行 InvokerTransformer.transform(templates) 方法.

templates 实例对象是 TemplatesImpl 类型的 , 所以可控反射调用链实际上会执行 TemplatesImpl.newTransformer() 方法


为什么 transient 修饰的变量 queue 会被序列化 ?

zhouliu前辈在 Ysoserial CommonsColletions2 两个问题 一文中提到了关于 queue 序列化的问题 , 这个点也需要注意.

首先我们需要知道 , 在 Java 中通过transient 修饰符修饰的变量是不会被序列化的 .

查看 PriorityQueue 类的定义 , 可以看到 queuemodCount 两个变量通过 transient 修饰符修饰 , 而 sizecomparator 两个变量没有 transient 修饰符修饰.

那么原则上序列化 PriorityQueue 类时只有 sizecomparator 变量会被序列化 , 通过 SerializationDumper 工具可以验证这一观点 .

究其原因 , 是因为 writeObject() 方法允许类控制其自身字段的序列化 . PriorityQueue 类中修改了 writeObject() 方法 , 因此变量 queue 会被写入序列化数据流中.


最终 YSoSerial 返回的优先级队列实例对象如下:

至此 , YSoSerial CommonsCollections2 Payload 的构造过程就分析完了 , 实际上思路还算清晰 , 没有什么特别复杂的点 .

不过上文我们还遗留下来很多问题 , 例如为什么要调用 TemplatesImpl.newTransformer() 方法 , 为什么不填充 _tfactory 字段 Payload 依旧可以利用成功 , 整体的调用链是怎么样的等等 . 下面我会一一解答这些问题 .


CommonsCollections2 Payload 利用原理

我们通过 YSoSerial 已经知道 CommonsCollections2 Payload 返回的是一个优先级队列( PriorityQueue )对象 . 因此我们直接定位到 PriorityQueue.readObject() 方法.


java.util.PriorityQueue.readObject()

PriorityQueue.readObject() 方法主要用反序列化必要的数据.

  1. 调用默认的 ObjectInputStream.defaultReadObject() 方法 , 反序列化数据流.

  2. 调用 ObjectInputStream.readInt() 方法读取优先级队列的长度

    这里读取后立刻丢弃 , 没人实际意义.

  3. 循环读取数组 queue 的内容

    这里与 PriorityQueue.writeObject() 方法对应 . 读取 queue 数组的内容 .

  4. 调用 PriorityQueue.heapify() 方法 , 将无序数组 queue 的内容还原为二叉堆( 优先级队列 )


java.util.PriorityQueue.heapify()

PriorityQueue.heapify() 方法用于构造二叉堆

该函数中会循环寻找最后一个非叶子节点 , 然后倒序调用 siftDown() 方法

关于构造堆的内容不在本文范围内 , 有兴趣的师傅可以参考 linghu_java 前辈的 PriorityQueue源码分析 一文 .


java.util.PriorityQueue.siftDown()

PriorityQueue.siftDown() 方法会根据是否有自定义比较器来调用不同的方法.

此时参数x的值为 queue[0] , 即我们写入的恶意类 . 而变量 comparator 值为 TransformingComparator , 由于该值不为空 , 所以程序会调用 siftDownUsingComparator() 方法.


java.util.PriorityQueue.siftDownUsingComparator()

该方法主要用于形成最小堆

该方法会将插入的元素与其左右字节点的最小值进行比较 , 如果该元素比较小的元素值还小 , 那么该元素位置不变 , 否则会将该元素与较小的元素调换位置

具体的细节我们不用深究 , 这里最终会调用 TransformingComparator.compare() 方法.


org.apache.commons.collections4.comparators.TransformingComparator.compare()

该方法中会调用 this.transformer.transform(obj1) 方法来获取需要进行比较的变量 .

在构造 Payload 时 , 我们已经把 this.transformer 指向 InvokerTransformer 实例对象 , obj 指向 TemplatesImpl 实例对象.因此 , 这里实际会调用 InvokerTransformer.transform(TemplatesImpl) 方法


org.apache.commons.collections4.functors.InvokerTransformer.transform()

通过前面对 YSoSerial 的分析我们可以得知 , 这里实际上会反射调用 TemplatesImpl.newTransformer() 方法


com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.newTransformer()

TemplatesImpl.newTransformer() 方法主要用于获取 TemplatesImpl 实例对象 , 后面可以使用此实例处理来自不同源的XML文档 , 并对其进行转换等操作.

在构建 TemplatesImpl 实例对象时 , 会调用 TemplatesImpl.getTransletInstance() 方法 , 我们跟进该方法.


com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getTransletInstance()

该方法用于生成 translet 实例对象 . 这个实例对象随后会被封装在 Transformer 实例对象中.

为什么构造 Payload 时 _name 字段不填充会利用失败 ?

其实是因为 getTransletInstance() 方法会对 TemplatesImpl 对象的 _name 字段有一步判断 , 如果该属性值为 null , 则直接返回 null

而代码并没有对 _tfactory 字段进行判断 , 所以即使不填充 , 生成的 Payload 也可以利用成功.

接着代码会判断 _class 字段值是否为空 , 如果为空就会调用 defineTransletClasses() 方法 . 这里 _class 字段为空 , 因此我们跟进该方法.


com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.defineTransletClasses()

该方法会对 _bytecodes 字段进行解析 , 核心代码如下:

代码会通过 transletClassLoader.defineClass() 方法将字节码数组转换成类的实例 . 而唯一的条件就是该类的父类为 com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet

这个点我们在构造 Payload 时就已经实现了 . 而 Foo.class 没有继承 AbstractTranslet 类 , 因此变量 _transletIndex 只会被赋值一次 , 即为 " 0 ".


(AbstractTranslet) _class[_transletIndex].newInstance()

由于变量 _transletIndex 的值为 " 0 " , 因此 _class[_transletIndex] 实际上就是我们通过 JAVAssist 构造的恶意类 .

现在会对恶意类调用 newInstance() 方法 , 类会先被加载后再被实例化 .

类在加载时会调用静态代码块中的内容 . 因此服务端最终会进入 java.lang.Runtime.getRuntime().exec() 反射链 , 执行系统命令.

至此 , 整个 CommonsCollections2 的利用链就调完了 , 完整的利用链如下所示 :


总结

Apache CommonsCollections2 Payload 的核心其实就是 JAVAssist 与 PriorityQueue . 这两个知识点并不难理解 .整个利用链也是比较简单的 .

在分析这条利用链时 , 很多底层的东西我并没有深挖( 例如优先级队列的底层和构造 ) . 实际上这部分内容还是比较重要的 . 后面在分析其他利用链时还需要进一步研究一下.

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

作者

留言

撰写回覆或留言

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