前言
本章来谈一谈两条 Apache Commons Collections
的 POP 利用链 , 分别是 TransformedMap
攻击链 与 LazyMap
攻击链 . 废话不多说 , 直接开搞 !
漏洞复现
复现环境是 IntelliJ IDEA
+ jdk1.7.080
+ Apache Commons Collections 3.1
,
环境搭建时我玩了一下 OBS
, 制作成动态图片后也方便没有接触过 Java 的师傅搭建测试环境.
-
首先我们需要一个 Apache Commons Collections » 3.1 的 Jar 包 , 可以从 MVNRepository 上获取到它.
-
然后打开
IntelliJ IDEA
, 创建一个普通的 Java 项目 , 注意 JDK 版本需要为1.7
-
导入之前下载的
commons-collections-3.1.jar
包 , 注意勾选File
->Project Structure
->Modules
->Dependencies
->JARs or directories
-
编写一个 POC , 网上很多地方都有现成的 , 例如 :
EvalObject
我的桌面环境是
Mate
, 因此想要弹出计算机需要执行命令mate-calc
如果您是其他操作系统环境 , 则需要修改要执行的命令 .
-
编译并执行
EvalObject
类 , 即可成功弹出计算机 , 漏洞复现成功
漏洞分析
上面视频中给出的POC是比较复杂的 , 而且是不完整的 , 废话不多说 , 我们来看一下这个利用链是怎么实现的 .
org/apache/commons/collections/functors/InvokerTransformer.class
-
InvokerTransformer.class
类中的Transformer()
方法中存在一组反射调用 , 这组反射调用是漏洞产生的根源.举例 : 利用 Java 反射机制执行系统命令 .
对比一下不难看出 , 只要我们能控制
input
,this.iMethodName
,this.iParamTypes
,this.iArgs
四个参数 , 那么就可以通过这组反射调用执行任意代码 ! -
那么这些参数是否可控呢 ? 回顾
InvokerTransformer.class
类的构造函数 , 可以发现this.iMethodName
,this.iParamTypes
,this.iArgs
三个参数都是直接可控的 , 我们可以直接传入参数值 , 而input
参数是transform()
函数调用方传入的 , 同样可控 .因此 , 这里的 transform() 方法是完全可以利用的 . 下面构造一个最简单的 POC , 用于弹出计算器 :
-
虽然可以成功执行命令 , 但是这种攻击方式肯定是没有意义的 , 我们构造代码是为了在远端服务上执行 , 而并非在本地服务器上执行 . 远程服务器上肯定不会出现像
t1.transform(Runtime.getRuntime());
这样的代码 . 所以我们必须对上述代码做优化 , 减少反序列化后的操作 .优化代码的第一步就是消除
transform()
方法的参数限制 :Runtime.getRuntime()
.
org/apache/commons/collections/functors/ChainedTransformer.class
-
由于
Runtime
类并没有继承Serializable
接口 , 因此我们无法直接传入实例对象 , -
所以 , 消除上述限制的最好方法就是通过 Java 反射机制来构建
java.lang.Runtime.getRuntime().exec()
方法调用链 . 由于Runtime.class
构造函数的特殊性 , 我们在编写 Java 反射代码时至少要调用getMethod()
,getRuntime()
,exec()
,invoke()
四个方法 . 因此 , 一组反射是完全不够用的 , 我们必须要找到一条链 , 来拼接多组反射 , 从而实现命令执行 .ChainedTransformer.transform()
方法恰好符合这个要求.ChainedTransformer
类的构造函数返回一个Transformer[]
类型的数组this.iTransformers
. 该类的transform()
方法会循环获取this.iTransformers
数组中的每一项 , 调用它的transform()
方法 , 并将返回结果作为下次循环调用的参数 .所以 , 我们可以编写多个
InvokerTransformer
实例对象 , 分别获取getRuntime()
,invoke()
,exec()
方法 , 然后将这些实例对象添加到this.iTransformers
数组中 , 从而获得一条完整的调用链 .如果不明白这里的代码是怎么来的 , 可以参考 Java反射机制 或者
getMethod()
,Invoke()
方法的定义 .那么如何获取
java.lang.Runtime
实例对象 , 来开启这个调用链呢 ?
org/apache/commons/collections/functors/ConstantTransformer.class
-
目前的问题是如何获取
Runtime
实例对象 . 而ConstantTransformer.transform()
方法满足我们的需求 .ConstantTransformer.transform()
方法恰好会返回Runtime()
实例对象 , 因此我们只需要将Runtime.class
传入ConstantTransformer
的构造方法中即可 .至此 ,
transform()
方法的参数限制已经被去除 , 此时无论transform()
方法的参数是什么 , 都会执行java.lang.Runtime.getRuntime().exec()
调用链 , 实现命令执行.
transform() 方法的利用条件
上一步 , 我们通过 java 反射机制消除了 transform()
方法的参数限制 . 但是依旧需要手动触发 transform()
方法 , 这样的场景是比较少的 .
况且根据 Java 反序列化漏洞的定义 , 我们更加期望后端程序在执行 readObject()
方法时就会自动执行 transform() 方法
.
综上所述 , 为了实现完整的利用链 , 必须要达成如下两个目标 :
- 找到一个
tansform()
方法 , 该方法所属的实例对象是可控的. - 找到一个重写的
readObject()
方法 , 该方法会自动调用transform()
方法.
这个限制是非常严谨的 , 目前研究人员共发掘出了两条攻击链 , 也就是经典的 TransformedMap攻击链
和 LazyMap攻击链
TransformedMap 攻击链
首先要知道什么是 TransformedMap
类 , 以及这个类是干什么的 . 官方文档关于这个类的定义非常少 , 个人认为下面这种说法是比较通俗易懂的 .
Apache Commons Collections
实现了一个TransformedMap
类,该类是对 Java 标准数据结构Map
接口的一个扩展 .该类可以在一个元素被加入到集合内时,自动对该元素进行特定的修饰变换 , 具体的变换逻辑由
Transformer
类定义,Transformer
在TransformedMap
实例化时作为参数传入.
TransformedMap.checkSetValue()
TransformedMap
类的 checkSetValue()
方法中调用了 Transform()
方法 .
只要我们能控制 this.valueTransformer
, 那么就可以利用该方法执行 ChainedTransformer.transform()
方法 , 进入构造好的函数调用链 . 根据上文可以得知 , this.valueTransformer
会在 TransformedMap
类被实例化时被传入 .
由于 TransformedMap
类的构造方法通过 protected
修饰符修饰 , 所以无法在外界获得 TransformedMap
实例对象 . 对此 , 该类提供了 decorate()
方法来返回 TransformedMap
实例对象 , 而 decorate()
方法通过 public
修饰符修饰 , 外界可以直接调用 .
综上所述 , this.valueTransformer
是完全可控的 . 我们可以通过这里调用 ChainedTransformer.transform()
方法
TransformedMap.decorate()
先来看一下函数定义 .
Map : 需要转换的 Map 对象
KeyTransformer : 用于转换键的转换器 , 如果为 null 则表示不进行转换
ValueTransformer : 用于转换值的转换器 , 如果为 null 则表示不进行转换
既然要调用 TransformedMap.decorate()
方法 , 那么这里 ValueTransformer
就应为 ChainedTransformer
. 此外 , 我们还需要一个 Map类型的变量 , 而获取 Map
最简单的方式就是构造一个 HashMap
, 然后将该 Map
实例对象传入 decorate()
方法中.
因此 , 我们需要构造如下代码 :
那么现在还剩一个问题 : 如何调用 checkSetValue()
方法呢? 要知道该方法同样通过 protected
修饰符修饰 , 外界是无法直接调用的 .
AbstractInputCheckedMapDecorator$MapEntry.setValue()
transformedMap
的父类 AbstractInputCheckedMapDecorator
中存在一个静态内部类 MapEntry
, 该类 setValue()
中调用了 checkSetValue()
方法.
我们现在的目标是调用 TransformedMap.checkSetValue()
方法 , 因此只需要令 this.parent
指向 TransformedMap
实例对象即可 . 查看 MapEntry
内部类的构造函数 , 可以确定 this.parent
参数值是作为参数传入的 . 是可控的 .
AbstractInputCheckedMapDecorator$EntrySetIterator.next()
我们对 this.parent = parent
打断点 , 查看 parent
参数值的函数调用栈 .
可以看到 , AbstractInputCheckedMapDecorator
类的静态内部类 EntrySetIterator
中的 next()
方法触发了 MapEntry
内部类的构造函数 , 并传入 parent
参数值 . 最后返回 AbstractInputCheckedMapDecorator$MapEntry
实例对象 .
AbstractInputCheckedMapDecorator$EntrySet.iterator()
这里 this.parent
参数依旧是可控的 ,我们继续跟踪 this.parent = parent
,
可以看到 , AbstractInputCheckedMapDecorator
类的静态内部类 EntrySet
中的 iterator()
方法触发了 EntrySetIterator
内部类的构造函数 , 并且传入 parent
参数. 最后返回 AbstractInputCheckedMapDecorator$EntrySetIterator
实例对象.
AbstractInputCheckedMapDecorator.entrySet()
依旧没看到 this.parent
的控制点 , 我们继续追踪 this.parent = parent
.
这里会根据 isSetValueChecking()
方法的返回值决定是否调用 AbstractInputCheckedMapDecorator.EntrySet()
方法 .
而抽象类 AbstractInputCheckedMapDecorator
恰好是 TransformedMap
的父类 , 因此这里我们可以直接将 this
指向 TransformedMap
. 使得最后调用 TransformedMap.checkSetValue()
方法 .
TransformedMap.isSetValueChecking()
那么现在还剩最后一个问题 : 我们需要让 isSetValueChecking()
方法的返回值为 True
TransformedMap
实现了抽象父类的 isSetValueChecking()
方法 , 来看一下函数定义
只需要让 this.valueTransformer
不会空即可 ! 这当然是成立的 , 我们在将 TransfromedMap.decorate()
方法时已经将 ChainedTransformer
赋值给了 this.valueTransformer
本地命令执行 => POC
现在你会惊奇的发现 , 我们上面所有提到的方法调用构成了一个闭环 , 我们只需要获取 AbstractInputCheckedMapDecorator$MapEntry
实例对象并手动触发 setValue()
方法 , 就可以执行任意代码 .
上面用到一些逆推的思想 , 说明了函数调用链是如何构造的 . 至此 , 本文开始时视频中出现的 POC 就构造完毕了 .
延长攻击链 => annotation/AnnotationInvocationHandler.readObject()
为什么本文分析前提到这个 POC 是不完善的呢 ? 因为这个 POC 压根没法投入使用 ! 我们最终的目标是让上文构造的恶意类在远程服务器上执行 , 也就是让恶意类经过序列化/反序列化后直接执行 .
按照前面几篇文章的思路 , 我们的目标是找到一个重写 readObject()
方法的地方 . 该方法中会调用可控的 setValue()
方法 . 那么是否存在这样的地方呢 ? 在 Jdk1.7 中 , annotation/AnnotationInvocationHandler
类的 readObject()
方法实现了我们的需求 .
这里的代码比较复杂 , 涉及非常多的变量 ,不过不要慌 , 我们一步步倒推分析 .
-
第一步
要想调用
AbstractInputCheckedMapDecorator$MapEntry.setValue()
方法 , 第一步要达成如下三个条件 .var5 = AbstractInputCheckedMapDecorator$MapEntry
!var7.isInstance(var8)
!(var8 instanceof ExceptionProxy) == True
var7
的值通过var3.get(var6)
返回 , 且不能为空 .var6
和var8
的值比较好看 , 分别通过var5.getKey()
方法和var5.getValue()
方法获取var5
的键名与值.var5
是var4.next()
方法返回的 , 根据上文的内容 , 我们希望var4
为AbstractInputCheckedMapDecorator$EntrySetIterator
实例对象. -
第二步
从第一步的结果来看 , 我们需要知道
var3
和var4
的赋值过程 .Iterator var4 = this.memberValues.entrySet().iterator()
Map var3 = var2.memberTypes();
var4
是this.memberValues.entrySet().Iterator()
方法返回的 , 对比前面的 POC , 我们期望this.memberValues
指向TransformedMap
.var3
是var2.memberTypes()
方法返回的 , 我们跟踪该方法 .通过几步跳转 , 可以确定这里
var3
是一个HashMap
, 因此上文var3.get()
就是调用HashMap.get()
方法. -
第三步
现在需要关注
this.memberValues
与var2
的值了 . 其中var2
的赋值如下 :为了确定
this.type
和this.memberValues
的值 , 我们来看一下当前类构造函数的定义 .AnnotationInvocationHandler
的构造函数第二个参数类型为Map
, 这点非常巧 , 我们可以直接传入TransformedMap
实例对象.关于
var1
, 我们目前只知道它是个注解类 . 网上很多分析文章中都使用了java.lang.annotation.Retention
这个注解类 . 笔者能力有限 , 就不去找其他的了 , 来看一看这个注解类是否满足下面各种if
条件吧 !注意 : 这里
var1
,var2
是构造函数的形式参数 , 并非readObject()
方法中的var1
和var2
. -
第四步
这里
var1
,var2
是readObject()
方法中的形式参数.-
构造函数
AnnotationInvocationHandler.AnnotationInvocationHandler()
这里
this.type
被赋值为java.lang.annotation.Retention
-
var2 = AnnotationType.getInstance(this.type)
经过这步赋值 ,
var2
实例对象中的memberTypes
参数变为一个HashMap
, 其中存在键值对 :{"value" : "java.lang.annotation.RetentionPolicy"}
-
Class var7 = (Class)var3.get(var6)
这里调用了
var3.get(var6)
, 并将结果赋值给变量var7
,var3
是一个HashMap
,var6
值为"value"
, 所以实际执行的是HashMap.get("value")
. 而var3
中恰好存在名为"value"
的键名 , 因此可以把"value"
的值"java.lang.annotation.RetentionPolicy"
, 赋值给变量var7
.这样变量
var7
就不为空了 , 自然通过了下文if (var7 != null)
的条件判断 .综上所述 , 由于变量
var6
的值为value
, 因此变量var7
可以获取到值并通过下面的条件判断 . 而var6
的值又是通过var5.getKey()
获取的 , 而var5
就是我们代码中创建的HashMap
因此这里也引出了该 POC 利用成功的一个核心要求 : 手工创建的
HashMap
的键名必须为Value
.相反 , 如果这里
HashMap
的键名被赋予其他值( 例如 "epicccal" ) , 那么此处将执行HashMap.get("epicccal")
, 哈希表中不存在这个键名 , 因此var7
会被赋值null
, 不会通过下面的 if 条件判断 .
-
-
第五步
后面就没啥好说的, 程序将执行到
var5.setValue()
, 即执行AbstractInputCheckedMapDecorator$MapEntry.setValue()
方法 , 进入恶意函数调用链 .至此 , 当我们构造的恶意类在远程服务器通过
readObject()
方法进行反序列化时 , 会自动调用AbstractInputCheckedMapDecorator$MapEntry.setValue()
方法 , 进入我们构造好的恶意函数调用链 , 最终执行任意代码 .
反序列化命令执行 => POC
现在可以构造完整的 POC 了 . 我们仅需要获取 AnnotationInvocationHandler
实例对象 , 并向构造函数中传入Retention
类 与 TransformedMap
实例对象 , 即可实现POP攻击链自动调用 .
需要注意的是 , AnnotationInvocationHandler
类的构造函数使用了默认修饰符 , 通过默认修饰符修饰的方法只能同包访问 , 因此这里无法直接访问 .
这里与获取 java.lang.Runtime
实例对象的思路类似 , 即通过反射来获取类 , 通过 getDeclaredConstructor()
方法获取构造器 , 通过 setAccessible()
方法来开放构造器访问权限.
最后写一个本地 Demo , 将当前类序列化后反序列化 , 即可得到完整的 POC.
LazyMap攻击链
LazyMap.get()
与分析 TransformedMap
攻击链类似 , 我们需要寻找其他可控且调用 transform()
方法地方 . LazyMap
类的 get()
方法同样满足我们的要求 .
查看当前类的构造函数 , 发现 this.factory
是可控的 , 因此这里的 get()
方法是可以利用的 .
这里恰好有一个重载构造函数的第二个参数为 transformer
类型 , 因此我们可以利用该函数 , 将 ChainedTransformer
实例对象给传进去 . 至于第一个参数 , 随便实例化个 HashMap
实例对象传进去就可以了 , 本身并不关键 .
该构造函数通过 protected
修饰符修饰 , 肯定无法直接调用 , 不过这里与 TransformedMap
类似 , 提供了 decorate()
方法来返回 LazyMap
实例对象 . 代码如下所示 :
本地命令执行 => POC
相比于构造 TransformedMap
函数调用链 , LazyMap
调用链实在过于简单 . 由于 LazyMap.get()
方法通过 public
修饰符修饰 , 所以根本不需要管其他的 , 直接调用就完事了.
延长攻击链 => TiedMapEntry.getValue()
与上文同理 , 这里的 POC 只能在本地执行 , 无法反序列化后执行 . 但安全研究前辈们暂未发现可控并调用 get()
方法的 readObject()
方法 .
通过跟踪 跟踪 get()
方法的调用栈 , 前辈们发现 TiedMapEntry
类的 getValue()
方法中调用了 get()
方法 .
这里简单很多 , 我们只要使得 this.map
指向 LazyMap
实例对象即可 .很明显这里是可控的 .
延长攻击链 => TiedMapEntry.toString()
但是 , 安全研究前辈们暂未发现可控并调用 getValue()
方法的 readObject()
方法 . 因此继续跟着 get()
方法的调用链往上找 . 最终 , 前辈们找到了 TiedMapEntry
类的 toString()
方法 .
toString()
函数通常在与字符串拼接时被自动调用 , 该方法中调用了 getValue()
方法并且可控 , 通过该方法可以间接控制 LazyMap.get()
方法
那么是否存在可控并调用 getValue()
方法的 readObject()
方法呢 ? 这一次 , 前辈们终于找到了想要的方法 !
延长攻击链 => javax/management/BadAttributeValueExpException.readObject()
在 BadAttributeValueExpException
类的 readObject()
方法中调用了 toString()
方法 , 来看一看该方法是否可以利用 .
要想利用该方法 , 只需要似的 valObj
指向 TiedMapEntry
即可 .
跟踪 valObj
变量可以确定 , 要想执行 toString
方法 , valObj
需要满足上面一堆条件 . 此外 , valObj
变量通过 gf.get("val", null)
赋值 .
val
是 BadAttributeValueExpException
类中的一个私有变量 , 其类型为 Object
. 因此我们可以直接通过反射对私有变量赋值 .
通过这一步赋值 , 我们先获取了 TiedMapEntry
实例对象 与 BadAttributeValueExpException
实例对象 , 并通过 class.getDeclaredField()
方法获取到 val
变量 . 又由于 val
变量为私有变量 , 因此我们通过 setAccessible()
方法去除限制 , 并通过 set()
方法进行赋值 .
通过这一步反射赋值 , valObj 变量的值最终会变为 TiedMapEntry
, 这样自然能通过后面几个判断 , 最终调用 TiedMapEntry.toString()
方法 , 进入构建好的函数调用链 .
反序列化命令执行 => POC
把剩下的序列化内容补充完整 , 即可得到完整的 POC . 我们构造的恶意类在服务端反序列化后将自动进入函数调用链 , 实现命令执行 .
注意点1 : 为什么在初始化 HashMap
时 , 键名不能为 null
?
针对这个问题 , 我们不妨将键名赋值为 null
后运行一下 POC , 并将断点打在漏洞触发点 : LazyMap.get()
方法 .
调试并注意下面这行代码 .
简单的说 , 这里会判断我们创建的 LazyMap
中键值对的键名是否为 null
, 如果为 null
则不会进入 if
语句 , 自然不会执行 ChainedTransformer.transform()
.
因此 , 这里 HashMap
初始化时不要将键名设置为 null
, 否则反序列化后不会进入函数调用链 . 或者干脆就不要初始化 HashMap
, 这样 LazyMap
中没有键值对 , 自然也就不存在键名的判断了 .
注意点2 : 无处不在的命令执行( 例如实例化 TiedMapEntry
时会触发命令执行 )
如果你有调试过代码就会发现 , 在很多莫名其妙的地方会触发命令执行 , 弹出计算器 . 来看下面这边
在 TiedMapEntry
构造函数中 , this.map = map
这一步赋值操作触发了命令执行 .
这个是为什么呢 ? 我们知道 LazyMap
攻击链的关键是 toString()
方法 , 来看一下官方文档中该方法的定义 .
也就是说 , 当涉及对象打印时 , Java编译器会在内部自动调用 toString()
方法 . 由于是内部自动调用 , 因此通过 IDEA 工具的 DEBUG 是看不到的 .
那么就有一种可能 , 会不会是在进行 this.map = map
时 由于传入的 map
参数非空 , 因此自动调用了 toString()
方法呢 ?
于是我对构造函数传入的 Map
对象进行了初始化赋值 , 赋值为 null
( 这里只是做实验 ). 然后执行 , 发现果然没有触发命令执行 , 说明我这个猜想很有可能是正确的 .
由于能力有限 , 我目前还不能肯定上述观点 , 如果您知道真正的原因 , 欢迎您的留言 . 不过这也不是关键点 , 我们的重点应该在反序列化后 .
注意点3 : 为什么在生成 BadAttributeValueExpException 实例对象时不传入 TiedMapEntry 对象作为参数
很多师傅在分析时会注意到下面这段
这里先将 null
作为参数来调用 BadAttributeValueExpException 类的构造方法,然后获取实例读对象的 val
参数,开启访问权限并将参数值指向 TiedMapEntry 对象.
问题出在 BadAttributeValueExpException 的构造方法上:
如果传入的 val 对象值不为 null , 则会调用该对象的 toString() 方法
这里很多师傅就有疑惑 , 我们本来就需要调用 TiedMapEntry.toString() 方法 , 这样不刚好符合我们的需求吗?
事实上,因为我们的目标是反序列化漏洞 , 因此我们期望代码在服务端调用
readObject()
后被执行 , 但是如果传入的对象值不为 null , 那么会在构造 Payload 时调用 toString() 方法 , 并触发命令执行.同时 , val 对象会被转换成 String 类型 . 在反序列化时,由于 valObj 参数值为 String 类型 , 就不会再调用
valObj.toString()
方法 , 不会再触发命令执行。
综上所述 , 只有通过反射机制获取 val 参数并手动赋值 ,才能在反序列化时进入正确的 if 条件语句 , 进而触发反序列化漏洞.
总结
本章主要是研究了下 ACC 中最基础的两条攻击链 : TransformedMap
攻击链 和 LazyMap
攻击链 , 内容比较多 , 也非常复杂 . 不过还是挺有意思的 .
文中可能会有不正确或者不精准的地方 , 如果您发现 , 欢迎指出 .
Java实在是太难了