MySQL 最小权限法:从数据库账户到系统服务,双重收紧攻击面
最小权限法
引言
为了最大限度地降低 SQL 注入攻击成功后可能造成的损失,您应该尽可能减少环境中每个数据库帐户的权限。首先要确定应用程序帐户需要哪些访问权限,而不是事后才去想需要撤销哪些访问权限。
所以我们只分配能完成相应任务的最小权限。切勿为应用程序帐户分配 DBA 或管理员类型的访问权限。
任何攻击方式(不仅是 SQL 注入)都存在一个底层逻辑:攻击者利用的是系统本身赋予连接的权限——那个持有权限的账户不是攻击者,但攻击者的语句通过它被执行。因此,限制连接的权限,就是把攻击者的"天花板"压到最低。
来源:http://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html#least-privilege
两层权限体系
最小权限法要管的不是"一层",而是两层相互独立的权限系统同时收紧:
情况 A:SQL root + OS SYSTEM = 数据库删光 + 文件系统随便逛 最差
情况 B:SQL 写权限 + OS NETWORK SERVICE = 能改业务表,但碰不到系统文件
情况 C:SQL 只读 + OS SYSTEM = 数据库安全,但 LOAD_FILE 能偷系统文件
情况 D:SQL 只读 + OS NETWORK SERVICE = 炸了也只能看自己的表 最安全
第一层:MySQL 内部账户权限
系统账户 & MySQL 账户的区别
| 操作系统账户 | MySQL 内部账户 | |
|---|---|---|
| 叫什么 | NETWORK SERVICE / SYSTEM |
root@localhost |
| 在哪定义的 | Windows 用户管理器 / 服务属性 | MySQL 的 mysql.user 表 |
| 怎么登录 | 服务控制面板 | mysql -uroot -p |
| 管什么 | 读写文件、绑定端口、启动进程 | 创建数据库、查表、授权、改密码 |
| SQL 注入能不能利用 | 能——LOAD_FILE() 的结果取决于 OS 账户 |
能——攻击者通过应用连接的身份操作数据库 |
为什么要分开说
任何程序执行前,Windows 都先决定"以谁的身份运行",然后给进程挂一个令牌(token)。mysqld.exe 之后所有操作(LOAD_FILE、写日志、读写文件),都受这个令牌的限制。
然而以什么身份运行不是 MySQL 自己能决定的——是安装程序在注册 Windows 服务时写的配置。安装器默认填 SYSTEM(旧版 MySQL 5.x 很常见)。
SYSTEM 身份运行意味着什么
mysqld.exe(进程本体)
↓
运行之前,Windows 给它挂一个"令牌"
↓
令牌上写着:● 你是 SYSTEM
● 你能读:C:\Windows\System32\ ✓
● 你能写:C:\inetpub\wwwroot\ ✓
● 你能关停其他进程 ✓
● 你能加载驱动 ✓
↓
mysqld.exe 的所有操作(LOAD_FILE、写日志……)
都受这个令牌的限制:SYSTEM → 全机器为所欲为
第二层:更改 MySQL 的 OS 运行账户
现在的 MySQL 默认
MySQL 8.0 安装器默认使用 网络服务(NETWORK SERVICE),而非本地系统(SYSTEM)。这是 MySQL 自己做的安全加固。
如何验证 / 更改(GUI 操作)
Win + R→ 输入services.msc→ 回车- 找到 MySQL80 → 右键 → 属性 → 登录 选项卡
- 当前显示的账户就是 mysqld 的 OS 运行身份
如果显示"本地系统账户"(即 SYSTEM),改成 NETWORK SERVICE:
- 先给新账户加 Data 目录权限:右键
C:\ProgramData\MySQL\MySQL Server 8.0\Data→ 属性 → 安全 → 编辑 → 添加 → 输入NETWORK SERVICE→ 检查名称 → 勾选"完全控制" → 确定 - 回到
services.msc→ MySQL80 属性 → 登录 → 选此账户 → 输入NT AUTHORITY\NETWORK SERVICE→ 密码框留空(内置账户不需要密码) → 确定 - 右键 MySQL80 → 重新启动
两种账户的能力对比
| 能力 | SYSTEM | NETWORK SERVICE |
|---|---|---|
| 读写 MySQL Data 目录 | ✅ | ✅ |
| 绑定 3306 端口 | ✅ | ✅ |
读 C:\Windows\System32\SAM |
✅ | ❌ |
写 Web 目录(如 C:\inetpub\) |
✅ | ❌ |
| 改注册表系统项 | ✅ | ❌ |
| 关闭其他进程 | ✅ | ❌ |
Windows 自身也在保护 SAM
SAM 文件不仅被 MySQL 的 SYSTEM/NETWORK SERVICE 权限管控——Windows 自己还额外加了三层保护:
第 1 层:文件管理器隐藏 SAM(连看都看不到)
第 2 层:NTFS ACL 只允许 SYSTEM 和 TrustedInstaller 读取
第 3 层:文件被操作系统锁定,无法删除/替换
第 4 层:密码不是明文——存的哈希 + 盐
你把 MySQL 改成 NETWORK SERVICE 后,
SELECT LOAD_FILE('C:/Windows/System32/config/SAM')返回NULL——这条 2005 年的经典注入攻击路径在 2026 年已经死透了。
有一句话概括得很好——不需要改一行 SQL 配置,改一个 Windows 服务属性就能把攻击面砍掉大半。
MySQL 权限基础
权限的三个级别
| 级别 | 范围 | 示例 |
|---|---|---|
| 全局 | 所有库所有表 | GRANT SELECT ON *.* TO ... |
| 数据库 | 指定库下所有表 | GRANT SELECT ON runoob.* TO ... |
| 对象级 | 指定库的某张表 | GRANT SELECT ON runoob.users TO ... |
ON 后面的语法
| 写法 | 意思 |
|---|---|
*.* |
所有数据库的所有表 |
mydb.* |
mydb 库下所有表 |
mydb.users |
mydb 库的 users 这一张表 |
mydb.user% |
mydb 库内以 user 开头的所有表 |
原则:能 mydb.* 就别 *.*,能 mydb.users 就别 mydb.*。范围越小,注入后损失越小。
静态权限 vs 动态权限
| 静态权限 | 动态权限(启动时定义) | 动态权限(组件定义) | |
|---|---|---|---|
| 定义者 | 服务器源码写死 | mysqld 启动时注册 | 组件/插件安装时注册 |
| 存储位置 | mysql.user 固定列 |
user_attributes JSON 列 |
user_attributes JSON 列 |
| 是否总可用 | 总是 | 总是 | 组件卸载即消失 |
| 例子 | SELECT/INSERT/SUPER |
BACKUP_ADMIN / SET_USER_ID |
FIREWALL_ADMIN |
误区:“动态权限 = 插件卸载就消失”。实际上大多数动态权限是启动时定义的,永远可用。真正随组件消失的只有企业版那几个(防火墙、审计、脱敏)。
常用权限管理命令
-- 查看所有用户
SELECT user, host FROM mysql.user;
-- 查看某个用户的完整授权
SHOW GRANTS FOR 'test'@'localhost';
-- 查看当前登录用户
SELECT USER();
-- 创建用户(自动带 USAGE)
CREATE USER 'test'@'localhost' IDENTIFIED BY 'strong_password';
-- 授予库级别只读
GRANT SELECT ON runoob.* TO 'test'@'localhost';
-- 撤销权限
REVOKE SELECT ON runoob.* FROM 'test'@'localhost';
-- 使权限变更生效(GRANT/REVOKE 后通常会自动,但手动刷新无害)
FLUSH PRIVILEGES;
USAGE 的含义
USAGE = 没有任何权限,但允许连接。CREATE USER 自动授予:
能做什么 mysql -utest -p 连上 3306
SELECT USER() -- 看自己是谁
查表、建库、SHOW DATABASES
实践
参考 MySQL 官方文档:添加账号、分配权限、删除账号
创建一个受限账户
CREATE USER 'test'@'localhost' IDENTIFIED BY '1111111';
GRANT USAGE, SELECT ON runoob.* TO 'test'@'localhost';
SHOW GRANTS FOR 'test'@'localhost';
输出:
GRANT USAGE ON *.* TO `test`@`localhost`
GRANT SELECT ON `runoob`.* TO `test`@`localhost`
用 test 账户连接测试:
SELECT * FROM users; -- 能查
INSERT INTO users VALUES (null, 'Aim', 21, '2005-6-30', 1);
-- ❌ ERROR 1142: INSERT command denied
表级权限 + USE 的问题
如果你只给一张表的权限:
GRANT SELECT ON runoob.users TO 'test'@'localhost';
此时 USE runoob 会失败——USE 是库级别操作,需要该库上至少有一条数据库级权限记录。两种解法:
-- 方案 A:直接给库级 SELECT(推荐)
GRANT SELECT ON runoob.* TO 'test'@'localhost';
-- 方案 B:保持表级,加一个无害的库级权限"开门"
GRANT CREATE TEMPORARY TABLES ON runoob.* TO 'test'@'localhost';
PHP 连接时如果不在 new mysqli() 里指定库名(第四个参数留空),用 SELECT * FROM runoob.users(带库名前缀)也能绕过这个问题。
验证系统权限隔离
-- NETWORK SERVICE 应该读不到系统文件
SELECT LOAD_FILE('C:/Windows/System32/config/SAM');
-- 预期:NULL
-- 对比:MySQL 自己的日志能读到(NETWORK SERVICE 有 Data 目录权限)
SELECT LOAD_FILE('C:/ProgramData/MySQL/MySQL Server 8.0/Data/DESKTOP-F1IJQQ4.err');
-- 预期:返回内容(可能被 secure_file_priv 限制)
攻击路径的历史变迁
2005 年:LOAD_FILE('C:\boot.ini') → 经典注入教学案例
2010 年:secure_file_priv 默认非空 → 路径开始收窄
2018 年:MySQL 8.0 默认不给 FILE 权限 → 默认安全
2020 年:云数据库 / 容器化 → 根本没有 SAM 可读
2026+:NETWORK SERVICE 默认 → 连系统目录都进不去
这条攻击路径从"入门教程必讲"变成了"只有 CTF 比赛才出现的极端场景"——不是因为注入少了,是因为防御层把最值钱的系统提权路径堵到没人走了。
核心思想:纵深防御
安全设计的原则不是写出没有漏洞的代码——那是不可能的。而是:
系统安全性 ≠ 漏洞数量为 0
系统安全性 = 攻击成功所需的最短链长 × 攻破后的最大收益
链越长、收益越小 → 系统越安全。两者的乘积趋近于零,就是一个工程上等价于"完美"的系统。
每一层防御单独拿出来都不完美。四层垒在一起(参数化 + 白名单 + 存储过程 + 最小权限),攻击链长到攻击者放弃——他会去试其他攻击面(XSS、越权、CSRF),而不是死磕 SQL 注入。
注意事项(实践中的坑)
-
USE runoob权限拒绝:给用户GRANT SELECT ON runoob.users(表级别)之后,USE runoob需要库级别权限。可以用SELECT * FROM runoob.users(带库名前缀)绕过,或者加一个无害的库级权限(如CREATE TEMPORARY TABLES)来"开门"。 -
文件管理器找不到 SAM / SYSTEM 文件:Windows 默认隐藏受保护的系统文件。需要勾选"查看 → 隐藏的项目"并取消勾选"隐藏受保护的操作系统文件"。但即使显示出来,普通账户
luck也打不开——NTFS ACL 拒绝访问。 -
Data 目录权限拒绝:
C:\ProgramData\MySQL\MySQL Server 8.0\Data默认只允许SYSTEM/NETWORK SERVICE访问,普通用户 shell 没有读取权限。需要用管理员权限的终端才能直接查看日志文件。 -
"只有 DBA 能连接"不能代替 OS 权限:SQL 注入走的是 Web 应用的合法连接通道——攻击者不需要
mysql -uroot -p,他的恶意 SQL 通过应用账户发进来。DBA 管控的是"入口",堵不住"已经进门的合法连接被注入"。 -
改 MySQL 服务账户时密码框不要填:
NETWORK SERVICE是 Windows 系统内置账户,不由用户管理密码——密码框留空即可,Windows 会自动处理认证。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)