还债系列-Java反序列化链条分析补全(1)-CC2&CC4
前言
已经有很长一段时间没有写博客了,摆烂摆久了,正好趁着最近入职,来复建学习一波了,之前我们只分析过cc链的第一条利用链,接下来我们会将所有的利用链依次分析,来还Java安全的债。
适用范围:
CommonCollections2 :commons-collections4:4.0
CommonCollections4 :commons-collections4:4.0
以下分析可能会非常详细,显得有些冗长,请见谅。毕竟是复习
<!--more-->
调试相关
环境配置:
Java_8u72
ysoserial_0_6
调试ysoserial,我们可以通过查看pom.xml中的主类,来进行调试,如下图:
这里给出的主类是GeneratePayload,因此我们查看该类需要哪些参数,才能指定调用我们对应的利用链即可,最后配置idea中的运行配置很简单:
CC2分析
我们先查看ysoserial-cc2链的注释,查看整体的链条构成:
忽略掉第一个部分,ObjectInputSteam类表示字节流,一般序列化后的对象均会被转化为该类,所以这个利用链最关键的地方是PriorityQueue这个类,我们查看该类的readObject方法:
该类前面部分对字节流进行读取,然后利用readObject将字节流恢复为相应对象,这里我们能够猜想Queue这个类,应该是实现队列的类,一个队列里可能会存有多个字节流对象,因此后面的for循环遍历这个队列,对队列中的每个对象进行反序列化操作。
根据原程序注释内容,后面的触发点为TransformingComparator.compare方法,因此我们继续跟,关注heapify方法:
这里heapify方法,会将queue中的对象传入siftDown方法中,继续跟:
这里存在一个简单的判断,决定了队列中的对象将被传入那个方法中,但由于PriorityQueue这个类,我们在当初构造payload时,就已经进行了初始化comparator的操作了,因此我们这里会进入siftDownUsingComparator方法:
该方法体如下:
这里再判断时,就会调用comparator.compare方法了,这就将TransformingComparator类链接起来,我们继续查看TransformingComparator的对应方法:
这里使用了transformer对象的transformer方法,由类的整体构造,我们知道transformer属性是Transformer对象,该类是反序列化漏洞触发的关键类。
Transformer类分析
虽然前面有文章也分析过该类,但是这里还是再复习一下,Transformer这个类实际是一个接口,在调用时需要具体实现它,我们需要回到ysoserial中查看其实现类究竟为哪个类。
答案是InvokerTransformer这个类,我们查看该类实现的transformer方法:
实际上该类是实现了一个反射执行的调用过程,该操作我们已经非常熟悉了,通过getClass找到该类,然后getMethod找到对应的方法,套用我们反射的命令执行,这里已经很明显了,显然我们需要传入Runtime类找到其exec方法,然后传入对于参数进行命令执行即可。
到这里其实CC2链便已经分析完成了,总结下来这个链条是利用TransformingComparator类链接Transformer。
ysoserial构造分析
实际上我们回到ysoserial的实际代码中,他并不是直接使用InvokeTransformer中封装的反射执行的方式来调用Runtime类,实际上CC2中的源代码是调用newTransformer方法:
同样我们回到ysoserial整体构造过程,便可以找到答案了
再进行动态调试,查看最后得到queue对象辅助理解:
这里的queue,首元素其实是Templateslmpl,下面我们来阅读整体代码
public Queue<Object> getObject(String command) throws Exception {
Object templates = Gadgets.createTemplatesImpl(command);
//将需要执行命令包装成TemplatesImpl
InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);
//根据InvokerTransformer类的构造方法,三个参数分别为
//toString-方法名
//Class[0]- 参数类型 - 空的Class数组,也就是不需要参数
//Object[0]- 参数数组- 空的参数数组,不需要参数
PriorityQueue<Object> queue = new PriorityQueue(2, new TransformingComparator(transformer));
//根据该类构造函数
//第一个参数可以理解为队列长度且必须大于1
//第二个参数为比较器,这里就是TransformingComparator
queue.add(1);
queue.add(1);
//填充无意义队列内容
Reflections.setFieldValue(transformer, "iMethodName", "newTransformer");
// 反射设置方法名为newTransformer
Object[] queueArray = (Object[])Reflections.getFieldValue(queue, "queue");
// 反射得到PriorityQueue类属性queue值
queueArray[0] = templates;
queueArray[1] = 1;
// 修改queue属性的内容
//queue[1]的内容其实可以不更改
return queue;
// 返回构造好的PriorityQueue类
}
中间看起来有些没有太大意义的代码,但实际上都是有用的,主要是考虑到前面compare的逻辑以及必须提前填充一定长度的元素,否则无法直接修改元素值两个问题。
在进行填充元素,也就是调用add方法时,就会调用PriorityQueue中的比较器进行比较,这里实际上会运行填充元素.iMethodName(),所以我们在这里如果提前设置newTransformer方法,那么会导致运行1.newTransformer()
,便会抛出异常,无法序列化,所以这里的iMethodName,必须是比较元素都有的方法,ysoserial选择的是toString。
下面还剩下一个问题,这里为什么会选择TemplatesImpl类,作为命令执行的入口,而不是直接使用Runtime类,在这之前的文章并没有说清楚这点,我们可以进行一下测试,稍微修改一下ysoserial的代码,如下:
public Queue<Object> getObject(final String command) throws Exception {
final InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);
Runtime rt = Runtime.getRuntime();
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2,new TransformingComparator(transformer));
queue.add(1);
queue.add(1);
Reflections.setFieldValue(transformer, "iMethodName", "exec");
final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue");
queueArray[0] = rt;
queueArray[1] = 1;
return queue;
}
这里其实invoke设置有瑕疵,但不影响说明的问题,上述代码不会是这边猜测的抛出exec方法无参的异常,而是无法序列化Runtime类
我们知道反序列化的条件之一是,需要类实现java.io.Serializable接口,但是很显然Runtime类没有实现该接口,我们无法直接将Runtime类带入反序列化的过程中,所以我们需要其他类,进行命令执行的操作。
顺着这个思路,同时结合Java的语言特性,便还有一个方法那就是字节码,我们知道Java代码的运行,需要将.java文件,编译为.class,也就是转化成为字节码,才能在JVM中运行,因此我们需要找到某个类,能够恢复出字节码,并加载到JVM中加载运行,这里ysoserial选择的类为TemplatesImpl。
TemplatesImpl类有下面几个特点:
- TemplatesImpl实现java.io.Serializable
- TemplatesImpl有loadClass和defineClass方法,有做过内存马分析的同学,应该不会陌生这两个方法,简单来说loadClass可以通过名字加载类,并创建实例,而defineClass则是直接从字节码中加载并创建实例。
- TemplatesImpl从字节码中加载类时,调用的是属性,而非其他给定传入的值,也就是说字节码可控,这个入口便是newTransformer()方法
newTransformer方法体如下:
继续跟
这里直接取出_bytecodes属性中的内容,进行defineClass,从而导致字节数组中的类被恢复出来,并创建了对应实例,如果这里我们将Runtime.exec这个实例恢复出来,则可直接导致命令执行。
ysoserial是利用javaassist包进行字节码基本的操作,最主要的是后面前面部分可以当作javaassist使用范例,重点在于框起来的部分:
这里设置了三个属性:
- _bytecodes是defineClass函数的参数,类的字节码从里面取出来并恢复,所以必须设置
- _name是进入defineClass的条件,必须不等于null,否则会直接返回,不会进行字节码恢复的操作
- _tfactory是保证defineTransletClasses顺利运行的条件,_tfactory为null无法调用loadClass
总结
整体流程可以看作下面这个流程图:
CC4分析
首先先看这个链条,ysoserial是如何解释的,在源码中并没有将整个链条进行解释,只是说明了InvokerTransformer这个类,被替换掉了:
那么我们就来看,这个类被替换成了什么类,答案是InstantiateTransformer,根据前文分析的,InvokerTransformer类在整个链条的作用是什么,答案是创建字节码加载类TemplatesImpl,InstantiateTransformer同样具有此特性,我们来看看他的transform方法体:
最主要就是上面框起来的两行代码,这里直接通过获取input这个输入参数的构造方法,然后利用newInstance调用该构造方法进行类的实例化,因此这里显然我们的input参数必须是某个类,且构造函数的参数为TemplateImpl类,这个类为TrAxFilter类,我们后文再分析。
构造InstantiateTransformer时,需要考虑InstantiateTransformer的构造函数问题,此处需求两个参数,iParamTypes表示构造函数所需参数类型,iArgs为具体参数,先在InstantiateTransformer构造前填充好默认值,再使用反射进行设置对应值。
我们在分析整体ysoserial的构造流程时,自然会注意到下面一行代码:
还是同样的,先来看看背后的源码,ConstantTransformer.transformer方法很简单,就是给啥返回啥:
关键在于为什么需要TrAXFilter类,而非其他类,注意看TrAXFilter的构造函数:
TrAXFilter的构造函数,会直接将TemplateImpl类进行初始化,这刚好符合InstantiateTransformer的transformer方法所需,再回顾一下:
CC4链的构造问题
InstantiateTransformer怎么和TrAXFilter进行连接
我们会发现ysoserial在构造时,实际上利用类ChainedTransformer将两个不同的transformer进行连接,而非单纯的传入一个transformer数组进行比较,因此ChainedTransformer的transformer方法就是连接的关键了:
public T transform(T object) {
Transformer[] arr$ = this.iTransformers;
//iTransformers为transformer数组
int len$ = arr$.length;
for(int i$ = 0; i$ < len$; ++i$) {
Transformer<? super T, ? extends T> iTransformer = arr$[i$];
// 首先获得一个transformer,这里如果是第一位
// 则运行i.transform(null)
// 第一位时constTransformer,
// 此时object等于TrAXFilter
object = iTransformer.transform(object);
// 进入下一次循环object有值,则运行
// i.transformer(TrAXFilter)
}
return object;
}
这也是为什么我们经常可以看到chainedTransformer的原因了,该类除了首元素外,将前一个元素的transformer结果,作为下一个元素的transformer参数进行链式调用,如下图:
为什么不一开始填充TemplatesImpl
之所以不直接填充好需要构造的值,是因为在构造整个链条时,最外层的queue类需要将元素进行添加,而添加过程中会调用compare方法,导致transformer方法被调用,如果这里直接填充,则会导致以下错误:
填充时的源码如下:
public Queue<Object> getObject(final String command) throws Exception {
Object templates = Gadgets.createTemplatesImpl(command);
ConstantTransformer constant = new ConstantTransformer(String.class);
Class[] paramTypes = new Class[] { templates.getClass() };
Object[] args = new Object[] { templates };
InstantiateTransformer instantiate = new InstantiateTransformer(
paramTypes, args);
paramTypes = (Class[]) Reflections.getFieldValue(instantiate, "iParamTypes");
args = (Object[]) Reflections.getFieldValue(instantiate, "iArgs");
ChainedTransformer chain = new ChainedTransformer(new Transformer[] { constant, instantiate });
PriorityQueue<Object> queue = new PriorityQueue<Object>(2, new TransformingComparator(chain));
Reflections.setFieldValue(constant, "iConstant", TrAXFilter.class);
return queue;
}
这里不再粘贴大量源码,下面简单叙述一下过程:
- 必须保证queue中有元素,否则无法执行compare方法触发利用链条。
- compare方法必须传入两个对象,在这里一个为ConstantTransformer,另一个为InstantiateTransformer。
- compare方法会调用transformer方法,在这条利用链构造时,如果添加队列的话,则是ChainedTransformer.transformer(1)。
- 序列化时根据之前分析,会调用1.getConstructor(iParamTypes),所以如果直接将TemplatesImpl的构造参数,则会抛出无法找到构造方法的异常,因此这里只能先给定一个大家都有的默认值,待完成构造后再使用反射进行修改。
参考链接
Java安全之Jdk7u21链分析
Java反序列化利用链挖掘之CommonsCollections2,4,8
Java动态加载字节码
不错不错,我喜欢看 www.jiwenlaw.com
看的我热血沸腾啊https://www.237fa.com/
不错不错,我喜欢看
When using a SoundCloud to MP3 converter, I prefer platforms
that do not require registration or personal information. I find it useful to use converters that
offer direct download links, reducing the steps required to save a song from SoundCloud.