前言
上一章我分析了 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 优先级队列的用法看下面这两个例子您就明白了 .
-
不单独定义比较器 , 使用默认的比较器
缺省情况下 , 优先级队列每次弹出的元素都是队列中的最小值
-
单独定义比较器 , 优先级队列每次比较时会根据比较器返回值的正负进行判断 .
通过定义比较器 , 使得优先级队列中的元素形成一个最大堆 , 堆顶为所有元素的最大值.
CommonsCollections2 Payload 简介
ysoserial.payloads.CommonsCollections1.java
看到了熟悉的 InvokerTransformer.transform()
方法 , 该方法中有一组可控的反射调用 , 可用于调用任何一个方法.
而其它的方法我们就都没有见过了 , 所以我们从 YSoSerial 开始学习 .
CommonsCollections2 Payload 复现
按照惯例 , 还是先复现一遍看看 .
复现环境 : JDK1.7_67
+ Tomcat7
+ CommonsCollections4-4.0
-
首先还是拿 YSoSerial 生成指定 Payload
这里要执行的命令依旧是弹出计算器
-
在 Tomcat 服务器上 加载 CommonsCollections4-4.0 组件 , 并启动 Tomcat 服务
可以在 MVNRepository 上直接下载 CommonsCollections4-4.0.jar 组件.
-
将保存在本地的恶意序列化数据发送到 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
的值.
-
如果返回值为
true
则通过 Java 反射机制(class.forName()
方法 )获取TemplatesImpl
,AbstractTranslet
,TransformerFactoryImpl
三个类的全限定类名 , 将它们作为参数并调用createTemplatesImpl()
的重载方法中 . -
如果返回值为
false
( 默认为 false ) 或者没有找到properXalan
属性 , 则直接将上述三个类的非限定类名作为参数带入createTemplatesImpl()
的重载方法中 .
这里没有找到系统属性 properXalan
, 因此if 条件取得默认值 false
.
Xalan 是 Java 中一个 XSLT 处理器 , 可用于将 XML 文档转换为 HTML 文档 , 文本文档或其他XML文档类型.
ysoserial.payloads.util.Gadgets.createTemplatesImpl()
该方法中首先获取了 TemplatesImpl
实例对象 .
然后会通过 JAVAssist 技术获取并修改 StubTransletPayload 类.
- 通过
ClassPool.getDefault()
方法获取默认的类搜索路径 , 即 JVM 类搜索路径 . - 通过
ClassPool.insertClassPath()
方法手工添加类搜索路径 . 这里起到一个保险作用 , 其实注释掉这两行后 , 同样能成功生成 Payload . - 通过
ClassPool.get()
方法获取StubTransletPayload
类并创建为 CtClass 实例对象 . CtClass 对象是可以被动态创建修改的 . - 通过
CtClass.makeClassInitializer().insertAfter()
方法在StubTransletPayload
类定义的最后添加静态代码块 - 将修改后的类重命名为
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
" 字段是可选填充的.
- 不填充 "
_name
" 属性字段 , 生成的 Payload 无法利用成功.
- 不填充 "
_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 类的定义 , 可以看到
queue
和modCount
两个变量通过transient
修饰符修饰 , 而size
和comparator
两个变量没有transient
修饰符修饰.那么原则上序列化 PriorityQueue 类时只有
size
和comparator
变量会被序列化 , 通过 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()
方法主要用反序列化必要的数据.
-
调用默认的
ObjectInputStream.defaultReadObject()
方法 , 反序列化数据流. -
调用
ObjectInputStream.readInt()
方法读取优先级队列的长度这里读取后立刻丢弃 , 没人实际意义.
-
循环读取数组 queue 的内容
这里与
PriorityQueue.writeObject()
方法对应 . 读取 queue 数组的内容 . -
调用
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 . 这两个知识点并不难理解 .整个利用链也是比较简单的 .
在分析这条利用链时 , 很多底层的东西我并没有深挖( 例如优先级队列的底层和构造 ) . 实际上这部分内容还是比较重要的 . 后面在分析其他利用链时还需要进一步研究一下.