Java 模板注入(FreeMarker) · 02 · 模板引擎与 FreeMarker 语法

本篇回答四个问题:

  1. Apache FreeMarker 到底是什么?生产环境里哪些地方在用它?

  2. 模板引擎到底是干什么的?为什么需要它?

  3. FreeMarker 的模板语言(FTL)长什么样、怎么写?

  4. 哪些语法是"危险入口"(?new?interpret?eval?api)?


1. 模板引擎与 FreeMarker:它是什么、生产环境哪里用

1.1 模板引擎是什么

模板引擎 = 把"模板(HTML 骨架 + 占位符)"和"数据"合并,生成最终文本(通常是 HTML)。

复制代码
模板文件 hello.ftl:   <h1>Hello, ${name}!</h1>
数据模型:            { name: "world" }
        ↓  渲染(process)
输出:                <h1>Hello, world!</h1>

为什么要用?

因为页面大部分是固定 HTML,只有少数地方要填数据。把"骨架"和"数据"分开,前端能改样式、后端只管数据,比在 Java 里 out.println("<h1>Hello, " + name + "</h1>") 清爽得多。

那 SSTI 的问题出在哪?

出在模板引擎的能力远不止"填数据"。为了灵活,FTL 支持条件、循环、调用方法、实例化类 。当"模板文本"本身由用户输入决定时,用户就能使用这些能力------包括实例化一个能执行系统命令的类

这不是 FreeMarker 的 bug,而是"把用户输入当模板"这个用法错误的必然结果。

1.2 Apache FreeMarker 到底是什么

官方定义(Apache FreeMarker 官网原文):

Apache FreeMarker is a template engine: a Java library to generate text output (HTML web pages, e-mails, configuration files, source code, etc.) based on templates and changing data. (Apache FreeMarker 是一个模板引擎:一个 Java 类库,基于"模板 + 变化的数据"生成文本输出------HTML 网页、邮件、配置文件、源代码等等。)

逐条理解:

关键词 含义 安全意义
模板引擎 负责"模板 + 数据 → 文本" 它执行模板里的逻辑,所以模板等于代码
Java 类库(library) 它不是一个独立服务/软件,而是一个 jar 包,被引入到别的 Java 程序里调用 攻击面取决于"哪个系统引入了它"
生成任意文本 HTML、邮件、配置、源代码都能生成 不限于网页;后台系统、邮件服务也可能中招
FTL FreeMarker Template Language,模板用的语言 就是注入 payload 要写的"语言"
零依赖 官方称 zero dependencies,只依赖 JDK 极易被引入,传播广
Apache 项目 2015-09-02 起归 Apache 软件基金会(2.3.24-pre01);Apache License 2.0 主流开源组件,用户极多
诞生于 1999 年 官方版权标注 1999--2026 历史久,老系统里常见

它和"编程语言"一样吗?

官方特意强调 FTL 是"简单、专用的语言,不是像 PHP 那样的通用编程语言 "。但"简单"是给正常用法的------它依然支持赋值、循环、调用 Java 方法、实例化类。能力够用来 RCE。
它和 JSP、Thymeleaf 是什么关系?

都是 Java 的"视图层技术",互相竞争/替代。FreeMarker 的定位是"模板与逻辑分离 ",把数据准备交给 Java,模板只管展示。你只要记住:FreeMarker 是其中使用最广、也是 SSTI 最常出现的一个。

1.3 生产环境里,到底哪些地方在用 FreeMarker

FreeMarker 不是"冷门组件",它遍布 Java 生态。按类别看:

(1)Java Web 框架的视图层

  • Spring MVC / Spring Boot :官方提供 spring-boot-starter-freemarkerspring-boot-starter-freemarker 是标准依赖(可自行到 Maven Central 验证)。

  • Apache Struts2 :标签库与 freemarker result 类型都依赖它(Struts2 历史上多次因此爆 RCE)。

  • 许多其它 Java Web 框架/脚手架也提供 FreeMarker 视图支持(有的作为默认,有的作为可选项)。

(2)企业级 / 内容类产品(从真实 CVE 反推)

  • Apache OFBiz (ERP)、Liferay (门户)、Alfresco(ECM)

  • Magnolia CMS、Crafter CMS、Halo、PublicCMS、MCMS、OneBlog(各类 CMS/博客)

  • JFrog Artifactory (制品库)、JetBrains YouTrack (项目管理)、ThingsBoard(IoT)

  • FusionAuth、OpenAM (认证系统)、OpenMetadata(元数据平台)

  • XDocReport (文档导出)、Datart (BI)、金蝶云星空(ERP)

(3)集成 / 中间件

  • Apache Camelcamel-freemarker 组件(用模板生成消息体/配置)。

(4)非 Web 场景(同样重要)

  • 代码生成器 / 低代码平台:用户在线编辑模板,生成 Java/XML/前端代码。

  • 系统通知邮件模板:注册、找回密码、告警邮件。

  • 配置文件、静态页面、报表文本的生成。

作为安全从业者,最该警惕哪一类? "用户可以编辑模板"的功能。 例如:

  • 后台的"自定义邮件模板 / 通知模板";

  • 多租户的"自定义主题 / 页面模板";

  • CMS 的"在线编辑 .ftl 模板 / Web Script";

  • 低代码/代码生成平台的"模板编辑"。这些功能本意是"灵活",但等于把模板代码的执行权交给了用户

1.4 FreeMarker SSTI(CVE 案例)

下面这些是真实公开的 CVE,几乎都是"用户可控内容进入 FreeMarker 模板 → RCE"。

CVE 受影响产品 CVSS 场景
CVE-2017-12611 Apache Struts 2 9.8 FreeMarker 标签里用了非字面量表达式 → RCE
CVE-2017-14589 Atlassian Bamboo(Struts FreeMarker 标签) 9.6 通过 Struts FreeMarker 标签二次 OGNL 求值
CVE-2019-10074 Apache OFBiz 9.8 Form Widget 文本框输入 FreeMarker 标记 → RCE
CVE-2019-19999 Halo(博客) 7.2 没有启用 SAFER_RESOLVER 导致 SSTI
CVE-2020-7931 JFrog Artifactory 8.8 不安全的 FreeMarker 处理 → RCE(可改 authorized_keys
CVE-2020-13445 Liferay 8.8 模板 API 未限制对敏感对象的访问
CVE-2020-12873 Alfresco 8.8 可编辑 FreeMarker Web Script → RCE
CVE-2021-46361 Magnolia CMS 9.8 绕过 FreeMarker 过滤 → RCE
CVE-2022-24442 JetBrains YouTrack 9.8 通过 FreeMarker 模板的 SSTI
CVE-2023-45303 ThingsBoard 8.4 用户可改邮件模板 + freemarker.template.utility.Execute
CVE-2024-41667 OpenAM 8.8 登录 URL 模板注入
CVE-2025-64087 XDocReport 9.8 FreeMarker 组件 SSTI → RCE
CVE-2026-22244 OpenMetadata 7.2 邮件模板 SSTI → RCE
CVE-2026-84939 Apache FreeMarker 本体 9.1 模板加载机制路径穿越(恶意 locale 标识)
CVE-2026-59280 Spring Framework 的 FreeMarker 集成 4.3 视图名来自不可信输入时路径穿越

这些 CVE 的共同点是什么?

共同点不是 "FreeMarker 本身有洞",而是"把用户可控的内容当模板处理 ":用户能改邮件模板 / 主题 / 代码模板,或某个参数被拼进了模板。根因------漏洞在用法,不在引擎


2. FreeMarker 的工作流程(Java 侧)

复制代码
// 1) 创建配置
Configuration cfg = new Configuration(Configuration.VERSION_2_3_31);
cfg.setDefaultEncoding("UTF-8");
cfg.setTemplateLoader(new FileTemplateLoader(new File("/opt/templates")));
​
// 2) 加载模板
Template t = cfg.getTemplate("hello.ftl");
​
// 3) 准备数据模型
Map<String,Object> model = new HashMap<>();
model.put("name", "world");
​
// 4) 渲染输出
StringWriter out = new StringWriter();
t.process(model, out);
System.out.println(out.toString());
  • Configuration:全局配置(编码、模板加载器、各种安全开关)。

  • Template:一个已编译好的模板对象。

  • model:数据模型(Map)。

  • process(model, writer):执行模板,把结果写出去。

new Template("x", new StringReader(用户输入), cfg) 是什么?

这是"直接从字符串创建模板 "的构造器------不读文件,直接把一个字符串当模板编译。如果这个字符串来自用户,就是最典型的 SSTI。靶场 /vuln/direct 用的就是它。


3. FTL 基础语法

3.1 插值 ${...}

复制代码
${name}                    <#-- 输出变量 -->
${7 * 7}                   <#-- 输出表达式结果:49 -->
${name?upper_case}         <#-- 内建函数:转大写 -->
${missing!"默认值"}         <#-- 默认值运算符 ! :变量不存在时用默认值 -->
<#if name??>存在</#if>     <#-- 存在性判断 ?? -->

3.2 注释

复制代码
<#-- 这是注释,不会输出 -->

3.3 指令(以 # 开头)

复制代码
<#-- 条件 -->
<#if score >= 60>
  及格
<#else>
  不及格
</#if>
​
<#-- 循环 -->
<#list users as u>
  ${u.name}
</#list>
​
<#-- 赋值 -->
<#assign x = 10>
<#assign ex = "freemarker.template.utility.Execute"?new()>
​
<#-- 引入其它模板 -->
<#include "header.ftl">
​
<#-- 定义宏(可复用片段) -->
<#macro greet who>
  Hello, ${who}
</#macro>
<@greet who="world"/>

<#assign> 为什么危险?

它本身不危险,但它能?new 创建的对象存成变量,方便后续调用。经典 payload 就是:

复制代码
<#assign ex="freemarker.template.utility.Execute"?new()>${ex("id")}

assignExecute 对象,再像函数一样调用它执行 id
${...}<#...> 有什么区别?

  • ${...}插值,把表达式的值输出到页面;

  • <#...>指令 ,控制流程或做动作(if/list/assign/include/macro)。攻击时两者都可能用到:<#assign> 准备对象,${...} 触发调用。

3.4 常用内建函数(built-ins)

内建 作用 例子
?upper_case / ?lower_case 大小写 ${"abc"?upper_case}
?length / ?size 长度/元素数 ${"abc"?length}
?join 连接集合 ${["a","b"]?join(",")}
?first / ?last 首/尾元素 ${list?first}
?sort / ?reverse 排序/反转 ${list?sort}
?string 转字符串 ${123?string}
?c 计算机格式 ${123?c}
?html HTML 转义 ${userInput?html}(防 XSS 用)

4. 专家级内建函数:危险入口

FreeMarker 文档把这些叫 "seldom used and expert built-ins",意思是"一般业务用不到,但高级场景有用"。对攻击者来说,它们是宝库。

4.1 ?new ------ 实例化任意 TemplateModel

复制代码
<#assign ex = "freemarker.template.utility.Execute"?new()>
${ex("id")}
  • 语义:"全限定类名"?new(参数...) ≈ Java 的 new 全限定类名(参数...)

  • 限制:被实例化的类必须实现 freemarker.template.TemplateModel(或其子接口)。

  • 官方文档明确警告:"This built-in can be a security concern because the template author can create arbitrary Java objects... You can (since 2.3.17) restrict the classes accessible with this built-in using Configuration.setNewBuiltinClassResolver(TemplateClassResolver)."

  • 默认行为(重点): FreeMarker 2.3.x 的默认解析器是 UNRESTRICTED_RESOLVER(不限制) ;从 2.4.0 起默认改为 SAFER_RESOLVER。所以当前所有 2.3.x(含最新的 2.3.35)开箱即用就能 ?new Execute

为什么 Execute 能被 ?new 出来?

因为 freemarker.template.utility.Execute 实现了 TemplateMethodModelEx(属于 TemplateModel 体系)。它的功能就是"执行字符串参数代表的系统命令并返回输出"。FreeMarker 自带这个类,本来是为了在模板里做点运维小工具,结果成了 RCE 神器。
?new 的点号 . 和包名里的点号怎么区分?

"freemarker.template.utility.Execute"?new() 中,引号内整串是类名字符串 (包名 + 类名);引号外的 ?new() 是内建函数。引号是关键,没有引号会被当成变量路径。
为什么 payload 里要写 ?new() 的括号?

因为 ?new 是一个"构造器方法",?new() 才是"调用无参构造器";?new(40) 是"调用带一个参数的构造器"。漏了括号拿到的是方法本身,不会创建对象。
2.3.35 里 ?new Execute 还能用吗? 能。

靶场实测(FreeMarker 2.3.35,默认配置):

复制代码
<#assign ex="freemarker.template.utility.Execute"?new()>${ex("id")}
=> uid=0(root) gid=0(root) groups=0(root)

因为 2.3.x 的 newBuiltinClassResolver 默认仍是 UNRESTRICTED_RESOLVER
那 2.4.0 呢?

2.4.0 起默认 SAFER_RESOLVERExecute/ObjectConstructor/JythonRuntime 会被直接拦截。但 2.4.0 尚未正式发布(截至本笔记写作时,最新稳定版是 2.3.35)。

4.2 ?interpret ------ 把字符串当"模板"解析

复制代码
<#assign t = payload?interpret>
<@t/>
  • 语义:把字符串 payload 当作一个 FTL 模板解析,返回一个可执行指令。

  • 危险场景:用户输入经过 ?interpret,等于"用户上传了一个模板"。靶场 /vuln/interpret 演示。

4.3 ?eval ------ 把字符串当"表达式"求值

复制代码
${payload?eval}
  • 语义:把字符串 payload 当作 FTL 表达式求值。

  • "1+2"?eval3

  • 当模板是 ${payload?eval}payload = "freemarker.template.utility.Execute"?new()("id") 时,就是 RCE。

  • 官方警告:"FTL expressions can access variables, and call Java methods on them, so if you ?eval strings coming from untrusted source, it can become an attack vector."

  • 靶场 /vuln/eval 演示。

4.4 ?api ------ 访问对象的 Java API

复制代码
${myMap?api.myJavaMethod()}
${list?api.getClass().getName()}
  • 语义:绕过 FreeMarker 对值的"简化视图",直接调用底层 Java 方法。

  • 默认关闭api_builtin_enabled 默认 false(自 2.3.22 起),官方文档说"为了不降低已有应用的安全性"。

  • 即使打开,2.3.30+ 的默认成员访问策略也会拦截危险方法 (如 getClassLoaderforNameinvoke)。详见 04、05 篇。

?new?interpret?eval?api 四个到底哪个最常用?

  • 实战/CTF 里 ?new + Execute 出现频率最高,因为 2.3.x 默认不限制,一击致命。

  • ?interpret/?eval 取决于代码是否真的这么写(有些"自定义表达式/自定义模板"功能会用到)。

  • ?api 依赖配置,且现代版本有成员策略兜底,单独用往往不够,需要配合反射链。


5. 模板的解析过程:为什么"数据"不会变成"代码"

这是新手最该建立的心智模型。

复制代码
模板文本 --[解析/编译]--> AST(抽象语法树) --[结合数据模型执行]--> 输出
  • 解析只发生一次${name} 被解析成"取变量 name 并输出"这个指令。

  • 执行时,name (哪怕是 <#assign ...>)只是被当作字符串数据 输出,不会再次被解析

所以:

场景 结果
模板文件写死 <h1>Hello, ${name}!</h1>name = "${7*7}" 输出 <h1>Hello, ${7*7}!</h1>不执行
代码 new Template(..., "<h1>Hello, " + name + "!</h1>", ...)name = "${7*7}" 输出 <h1>Hello, 49!</h1>执行了

一句话:数据永远是数据,除非你把它放进了"模板文本"的位置。

?interpret / ?eval 不是又能把字符串变成代码吗?

对!它们是"显式地把数据当代码"的内建函数。所以:

  • 正常渲染 → 数据安全;

  • 一旦代码里用了 ?interpret/?eval 且参数可控 → 又变回危险。这正是它们被归为"专家级/危险"内建的原因。


6. 数据模型里能放什么

FreeMarker 通过 对象包装器(ObjectWrapper) 把 Java 对象暴露给模板。常见类型:

放进去的 Java 对象 模板里怎么用
String / 数字 / 布尔 ${s}${n+1}
Map ${map.key}${map["key"]}
List / 数组 <#list list as x>${x}</#list>${list[0]}
POJO(自定义对象) ${obj.property}${obj.method()}
Date ${date?string("yyyy-MM-dd")}

POJO 的方法能被模板调用,那不是很危险? 是的,这正是"数据模型污染 → 利用已有对象的方法"的来源。例如数据模型里有个对象带 getClass()(所有对象都有),配合 ?api 就能开始反射链。所以安全设计里有一条:不要往数据模型里塞不该暴露给模板的对象(比如包含敏感方法的内部对象)。


7. FreeMarker 与其它 Java 模板引擎对比

引擎 语法风格 SSTI 关注度
FreeMarker <#...>${...} ⭐⭐⭐⭐⭐ 企业系统极多
Velocity #set$var ⭐⭐⭐ 也曾是 SSTI 重灾区
Thymeleaf th:text[[...]] ⭐⭐⭐ Spring 生态常用,__${...}__ 预处理是坑
JSP/JSTL <%= %><c:...> ⭐⭐ 本质是 Servlet
Groovy Template GString ⭐⭐ 配合 Groovy 沙箱逃逸

8. 本篇小结

  1. Apache FreeMarker 是一个 Java 模板引擎类库(零依赖、Apache 项目),用 FTL 基于"模板 + 数据"生成任意文本。

  2. 它广泛用于 Spring Boot/MVC、Struts2、OFBiz、Liferay、Alfresco、各类 CMS、Artifactory、YouTrack、邮件模板、代码生成等场景。

  3. 真实 CVE 一长串(Struts2、OFBiz、Liferay、Artifactory、Magnolia、YouTrack、ThingsBoard......),共同点是"用户可控内容进了模板"。

  4. 模板引擎 = 模板 + 数据 → 输出;解析只发生一次,数据不会自动变代码。

  5. FTL 核心语法:${} 插值、<#...> 指令(if/list/assign/include/macro)。

  6. 四个危险内建:?new(实例化类)、?interpret(字符串当模板)、?eval(字符串当表达式)、?api(访问 Java API)

  7. ?new 只能实例化 TemplateModel 实现类,Execute 正是其中之一。

  8. 2.3.x 的 ?new 默认不受限 ,这是经典 RCE 成立的根本原因;限制它靠 TemplateClassResolver


相关推荐
IvanCodes1 小时前
Python 基础语法(七):推导式与常见遍历写法
开发语言·python
暮雨哀尘1 小时前
Java 基础练习(一):创建类并调用对象
java·开发语言
想要入门的程序猿2 小时前
回调函数学习
java·网络·学习
zhangzeyuaaa2 小时前
Python requests:raise_for_status() 与 4xx/5xx 异常层次详解
开发语言·python
泡海椒2 小时前
动态代理核心原理:JQuick-Java接口规则自动生成机制解析
java·开发语言·python
shirsl2 小时前
算法 Day 2 滑动窗口 + 栈 / 单调栈
开发语言·python·算法
步行cgn2 小时前
Spring 启动报错:BeanFactory not initialized 的原因与解决
java·python·spring
菜鸟~noob2332 小时前
【电子战】第08篇:多普勒测向的工程实现与优化【含matlab代码】
开发语言·matlab
步行cgn2 小时前
Spring 报错:No bean class specified on bean definition 的原因与解决
java·后端·spring