前言
前面学习的cc链都是基于commons-collections:commons-collections
的3.1-3.2.1这几个版本的,但后面有了新的分支org.apache.commons:commons-collections4
的4.0版本。可以发现groupId
和artifactId
都发生了改变,也就是形成了两个分支。这是因为commons-collections4
不是用来替换commons-collections
的一个新版本,而是修复旧的commons-collections
的⼀些架构和API
设计上的问题的一个拓展。两者的命名空间并不冲突,都可以放在同一个项目中。
前面文章提到 cc1 利用链在 JDK8u71 版本以后的高版本下是无法使用的,而cc2链可以在有 commons-collections-4.0 的 jdk8u71 以后的高版本下使用,但commons-collections
3.1-3.2.1版本不能去使用
“老”cc链
基于commons-collections:commons-collections
的3.1-3.2.1这几个版本的cc链到了commons-collections-4.0
还能使用吗?
试试就知道了,首先在pom.xml文件导入依赖
1 | <dependency> |
这里拿强大的cc6链做实验,直接将包名一改,把import org.apache.commons.collections.*
改成import org.apache.commons.collections4.*
然后就会出现报错哩
这是因为在CommonsCollections-4.0
版本中,删除了LazyMap
的decorate
方法
但它还有一个构造方法
1 | protected LazyMap(Map<K, V> map, Transformer<? super K, ? extends V> factory) { |
它和3.2.1版本的构造方法略微不同,但都是可以让传进的Transformer
赋值给this.factory
decorate方法实际上就是创建了一个LazyMap对象
1 | public static Map decorate(Map map, Transformer factory) { |
4.0版本中LazyMap类也有一个同义不同名的 lazyMap
方法
1 | public static <V, K> LazyMap<K, V> lazyMap(Map<K, V> map, Transformer<? super K, ? extends V> factory) { |
因此将LazyMap.decorate()
改成LazyMap.lazyMap()
即可
运行即可弹出计算器
cc1和cc3链也是如此,他们都可以在CommonsCollections-4.0
中使用
TransformingComparator
TransformingComparator
是一个比较器comparator,实现了java.util.Comparator
接⼝
TransformingComparator
调用compare
方法时,就会调用传入transformer对象的transform
方法
1 | public int compare(I obj1, I obj2) { |
当ChainedTransformer
对象作为参数传入时就会调用ChainedTransformer#transform
反射链执行命令
这就是cc2链的尾巴,之所以commons-collections3.1-3.2.1版本无法使用是因为TransformingComparator在3.1-3.2.1版本中还没有实现Serializable接口,无法被反序列化
那么这里还需要一个可以连起来的头,而这个头就是java.util.PriorityQueue
PriorityQueue利用链
优先队列PriorityQueue
是Queue
接口的实现类,基于二叉堆实现,可以对其中元素进行排序,和先进先出(FIFO)的队列的区别在于,优先队列每次出队的元素都是优先级最高的元素,Java通过堆使得每次出队的元素总是队列里面最小的
二叉堆是一种特殊的堆,是完全二叉树或者近似于完全二叉树,二叉堆分为最大堆和最小堆
最大堆:父结点的键值总是大于或等于任何一个子节点的键值;最小堆:父结点的键值总是小于或等于任何一个子节点的键值
重点在于每次排序都要触发传入的比较器comparator的compare()
方法。并且这个类重写了readObject()
方法,跟进一下代码康康
1 | private void readObject(java.io.ObjectInputStream s) |
该函数中s.defaultReadObject()
调用默认的方法,利用readInt()
读取了数组的大小,接着通过s.readObject()
读取Queue中的元素,因为在反序列化的时候队列元素也被序列化了,接着调用heapify()
方法,跟进一下
1 |
|
该函数中会循环寻找最后一个非叶子节点 , 然后倒序调用 siftDown()
方法。>>>
是无符号右移,将第一个操作数向右移动指定的位数。向右移动的多余位将被丢弃,零位从左侧移入,其符号位变为 0
,因此其表示的结果始终为非负数。该函数将无序数组 queue 的内容还原为二叉堆( 优先级队列 )。继续跟进siftDown()
方法
1 | private void siftDown(int k, E x) { |
这里会判断是否拥有比较器comparator而进入不同比较逻辑。在PriorityQueue
的构造方法中是否拥有比较器是可控的,这里要注意当initialCapacity
的值小于1时会抛出异常,所以初始化时传入的值要大于或等于2。
1 | public PriorityQueue(int initialCapacity, |
然后跟进有比较器时调用的siftDownUsingComparator()
方法
1 |
|
该函数当k < half
时就能进入循环,调用到比较器的compare()
方法⽐较树的节点,这里half = size >>> 1
且k
来自heapify()
的循环变量 i 其最小值为0,所以推导出size>=2
,这里很容易理解,至少需要两个元素才能触发排序和比较。而size默认值是为0的,需要经过两次入队(offer)后变为2,即调用Queue#add()
方法
1 | public boolean add(E e) { |
总而言之就是,整个优先队列调用重写后的readObject()
方法反序列化,然后反序列化队列元素,并调用heapify()
方法,让队列中的元素保持优先级顺序,而排序过程就是二叉堆的树节点下移的过程,即调用siftDown()
方法,并调用compare()
⽅法⽐较树的节点
那么将比较器设置为TransformingComparator
就能实现利用链调用了
cc2 POC
1 | import java.io.*; |
为什么能从流中反序列化queue中的元素?
关于这个问题@zhouliu师傅做出了详细解释
PriorityQueue
的 queue
已经使用transient
关键字修饰,成员使用transient
关键字修饰,是为了序列化时不写入流中(该成员可能含有敏感信息,出于保护不写入)
1 | transient Object[] queue; |
但是,序列化规范允许待序列化的类实现writeObject
方法,实现对自己的成员控制权
1 | private void writeObject(java.io.ObjectOutputStream s) |
因此queue被写入到了反序列化流中,从而被readObject()
在反序列化流中读取队列元素
ysoserial 链的操作
类比CommonsCollections1
,通过ChainedTransformer
将InvokerTransformer
和ConstantTransformer
串起来感觉够用了,但ysoserial 链中使用了TemplatesImpl
类来承载payload,利用InvokerTransformer
来执行TemplatesImpl
类中的方法。虽然复杂,但开啃!
利用Javassist操作字节码
javassist( JAVA programming ASSISTant )是一个开源的分析,编辑,创建 Java字节码的类库。它允许开发者自由地在一个已经编译好的类中添加新的方法,或者是修改已有的方法。其主要优点在于简单快速,直接使用 java 编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构, 或者动态生成类。
Javassist中最为重要的是ClassPool, CtClass, CtMethod以及CtField这几个类
ClassPool: 一个基于Hashtable实现的CtClass对象容器, 其中键是类名称, 值是表示该类的CtClass 对象.
CtClass: CtClass表示类, 一个CtClass(编译时类)对象可以处理一个class文件, 这些CtClass对象可以从ClassPool获得.
CtMethods: 表示类中的方法.
CtFields: 表示类中的字段
在pom.xml文件中导入依赖
1 | <dependency> |
在 javassist 中,类 javaassit.CtClass
表示 class 文件。一个 CtClass (编译时类)对象可以处理一个 class 文件,ClassPool
是 CtClass
对象的容器。它按需读取类文件来构造 CtClass
对象,并且保存 CtClass
对象以便以后使用
下面给一个简单实例方便分析
1 | import javassist.CannotCompileException; |
运行后查看写入的文件
1 | // |
像这样我们就可以利用javassist修改字节码
命令执行点分析
命令执行点一般是构造Payload的起点,是反序列化的终点
javassist可以将类加载成字节码格式并能对其中的方法进行修改,这样就可以把这个序列化后的字符串给其他类的变量赋值了,如果那个类有将这个变量中的字节码给实例化成对象,那么就会触发其中的static的方法。
TemplatesImpl利用链
在TemplatesImpl
类中存在加载字节码并创建实例的函数
1 | private void defineTransletClasses() |
所以在调用TemplatesImpl#defineTransletClasses
方法时,会把 _bytecodes
里面的字节码文件加载成Class对象,Class对象在调用newInstance()
方法就会进行实例化,执行无参构造函数。
那么在哪里既调用到了defineTransletClasses
方法,也调用到newInstance
方法呢?
这个方法就是TemplatesImpl#getTransletInstance()
1 | private Translet getTransletInstance() |
因此这里首先需要满足TemplatesImpl
对象中_name
的值不为null,才会调用到defineTransletClasses
方法,而且调用newInstance
方法后进行了类型转换,所以这个类必须继承自AbstractTranslet
但这是个私有方法不能被外部调用,找找哪里调用了getTransletInstance
方法
不难发现newTransformer
方法中调用了,并且它是一个public
方法,那么这部分利用链就出来了
1 | // 通过反射注入bytes的值 |
我们只需要通过反射机制传入_name
和 _bytecodes
的值即可
接着就直接用InvokerTransformer#transform
反射调用TemplatesImpl#newTransformer
方法就可以了
这里就回到了开始cc2链的分析,TransformingComparator#compare
方法会调用参数中的transform
方法
生成带命令执行的Java字节码
那么我们就先利用javassist生成恶意Java字节码并填充在TemplatesImpl
对象的_bytecodes
属性
1 | // 创建CommonsCollections2对象,父类为AbstractTranslet,注入了payload进构造函数 |
POC链
1 | ObjectInputStream.readObject() |
1 | import javassist.CannotCompileException; |
终于结束哩,悲,可能部分地方讲的不是很清楚,欢迎大神指点批评捏
参考链接:
ysoserial CommonsColletions2分析 | Atomovo
PriorityQueue源码分析 | linghu_java
Java篇Commons Collections 2 | Arsene.Tang
Author: ph0ebus