本篇回答四个问题:
-
Apache FreeMarker 到底是什么?生产环境里哪些地方在用它?
-
模板引擎到底是干什么的?为什么需要它?
-
FreeMarker 的模板语言(FTL)长什么样、怎么写?
-
哪些语法是"危险入口"(
?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-freemarker,spring-boot-starter-freemarker是标准依赖(可自行到 Maven Central 验证)。 -
Apache Struts2 :标签库与
freemarkerresult 类型都依赖它(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 Camel 的
camel-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")}先
assign出Execute对象,再像函数一样调用它执行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_RESOLVER,Execute/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"?eval→3。 -
当模板是
${payload?eval}且payload = "freemarker.template.utility.Execute"?new()("id")时,就是 RCE。 -
官方警告:"FTL expressions can access variables, and call Java methods on them, so if you
?evalstrings 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+ 的默认成员访问策略也会拦截危险方法 (如
getClassLoader、forName、invoke)。详见 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. 本篇小结
-
Apache FreeMarker 是一个 Java 模板引擎类库(零依赖、Apache 项目),用 FTL 基于"模板 + 数据"生成任意文本。
-
它广泛用于 Spring Boot/MVC、Struts2、OFBiz、Liferay、Alfresco、各类 CMS、Artifactory、YouTrack、邮件模板、代码生成等场景。
-
真实 CVE 一长串(Struts2、OFBiz、Liferay、Artifactory、Magnolia、YouTrack、ThingsBoard......),共同点是"用户可控内容进了模板"。
-
模板引擎 = 模板 + 数据 → 输出;解析只发生一次,数据不会自动变代码。
-
FTL 核心语法:
${}插值、<#...>指令(if/list/assign/include/macro)。 -
四个危险内建:
?new(实例化类)、?interpret(字符串当模板)、?eval(字符串当表达式)、?api(访问 Java API)。 -
?new只能实例化TemplateModel实现类,Execute正是其中之一。 -
2.3.x 的
?new默认不受限 ,这是经典 RCE 成立的根本原因;限制它靠TemplateClassResolver。