2. 从原理到绕过:SQL注入(二)进阶攻防实录
SQL注入这个老话题,到今天依然是OWASP Top 10的常客。很多人觉得它“过时了”,但过去一年我依然能在授权测试里看到大量真实的注入点,甚至包括某些号称“数字化”的餐饮服务系统、后台管理面板,一个not_out_depot参数就直接把整个库存表暴露了。所以这一篇,我们不谈“什么是SQL注入”这种入门话题,而是把重点放在进阶维度:注入类型的准确判断、绕过思路的底层逻辑、以及从靶场到实战的一条完整测试路径。顺便把大家搜得最多的几个问题——万能密码到底怎么来的、sqlmap的tamper脚本怎么用、为什么现在还有人中招——一次性讲透。
这篇文章适合三类人:准备面试的安全新人、在甲方做代码审计的开发同学、以及想把自己从“只会跑sqlmap”提升到“能手工判断绕过思路”的渗透测试者。如果你是零基础,我建议先把联合查询和报错注入的基础原理过一遍再往下读,但也别担心,全文会尽量用“人话”把关键点说清楚。
1. 先搞清楚SQL注入的形态,再谈别的
1.1 注入类型分类,远不止“数字型/字符型”这么简单
很多新手教程喜欢把SQL注入简化为“数字型”和“字符型”,然后给你一个万能公式:数字型直接拼、字符型加个引号闭合。这套分类在靶场里够用,但到了真实环境,你会发现绝大多数注入点都是躲在复杂的查询逻辑里的——比如WHERE子句里同时有多个条件、ORDER BY后面拼接字段名、甚至LIMIT后面注入。我建议你把注入点按“获取数据的方式”重新分类,这样更贴近实战判断逻辑:
- 联合查询注入(Union Based):核心条件是页面上有回显位,而且能控制
SELECT语句的列数。判断方法就一句话:ORDER BY N逐步试探,直到报错或页面异常,N-1就是列数。这类注入效率最高,直接UNION SELECT拿数据。 - 报错注入(Error Based):页面会输出数据库报错信息,利用
updatexml、extractvalue这类函数把查询结果拼进报错消息里。一个前提:数据库得是MySQL且站点开了报错回显。很多生产环境早就把display_errors关了,所以这类注入在实战中越来越少见,但在靶场和CTF里依然是主力。 - 布尔盲注(Boolean Blind):页面不会回显数据,但会根据条件真假返回不同的页面内容(比如“查询成功”和“无记录”)。思路是构造
AND 1=1和AND 1=2对比,再用substr()配合二分法逐字符猜解。 - 时间盲注(Time Blind):连页面差异都没有,只能靠
sleep()或benchmark()让数据库延迟响应。比如AND IF(1=1, SLEEP(3), 0),响应时间差了3秒,说明条件为真。 - 堆叠注入(Stacked Injection):分号结束当前语句后,再拼一条新语句。它不依赖回显,能干的事也更多(比如
UPDATE、DELETE甚至INTO OUTFILE写文件),但很多数据库连接层会禁止多语句执行,所以能不能成功全看运气。
这个分类的价值在于:每一种注入形态都对应不同的“探测动作”和“利用工具”。你不可能指望sqlmap通吃一切——很多二次注入、堆叠注入场景,手工判断反而更快。
1.2 注入点藏在哪?从参数位置和数据库特性两个维度判断
“哪里可能有注入”这个问题,我用一个简单粗暴的标准回答:凡是用户输入能影响到SQL语句拼接的地方,都是潜在注入点。最常见的位置有几个:
- URL参数:
?id=1、?type=news、?page=2这类,明晃晃地拼进SQL。 - POST表单字段:登录框、搜索框、评论框,尤其是登录框,经典万能密码就诞生在这里。
- 请求头:
User-Agent、Referer、X-Forwarded-For,这些值有时候会被后端记录进数据库,比如审计日志功能,如果日志模块直接拼接SQL,就是一个隐藏注入点。 - Cookie:记住登录状态的用户ID如果直接查库,也是一个常见的二次注入入口。
数据库特性的判断也很关键。同样是报错注入,Oracle用CTXSYS.DRITHSX.SN,SQL Server用CONVERT(int, @@version),MySQL用updatexml——如果你不知道后端是什么数据库,payload就无从谈起。快速判断方法有两个:第一看报错页面里出现的函数名和错误格式;第二用数据库专属语法试探,比如MySQL的AND 1=1正常、AND 1=2页面异常,但SQL Server的;WAITFOR DELAY '0:0:3'如果生效说明是SQL Server。另外,information_schema是MySQL专属,ALL_TABLES是Oracle专属,看到这些关键字基本就能锁定数据库类型。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 从万能密码到WAF绕过:SQL注入的攻防博弈实录
2.1 万能密码的底层原理,其实是一个“永真条件”
“万能密码”大概是SQL注入里最出名的一个词,无数人第一次接触注入就是admin' or '1'='1。我这里把它的逻辑彻底拆开:
登录校验的SQL通常是SELECT * FROM users WHERE username='$user' AND password='$pass'。你输入的用户名是admin'、密码是 or '1'='1,拼接后变成:
sql复制SELECT * FROM users WHERE username='admin' AND password='' or '1'='1'
注意最后这个or '1'='1',它是永真条件,所以整个WHERE子句恒为真——不管你输入什么密码,都能直接命中第一条用户记录。如果表里第一条记录是管理员,那就直接以管理员身份登录了。这就是万能密码的全部秘密,没有任何高深的东西。
从防御角度看,这类漏洞的修复其实非常简单:参数化查询或预编译语句,用户输入永远只当“参数”传进去,而不是当成“SQL代码”去拼接。也就是说,你把' or '1'='1'传给一个参数化接口,它只会变成用户名里的普通字符,而不是SQL逻辑。这也是为什么我反复强调,修复注入的优先级永远大于“寻找更复杂的注入技巧”。
2.2 绕过思路背后,其实是对“过滤逻辑”的猜解
很多站点知道要过滤,但过滤逻辑写得不够严谨,这就有了绕过的空间。我归纳成几类典型的绕法,每一条的背后都是一次对过滤逻辑的推理:
- 注释符绕过:
--、#、/* */。过滤了--就换#,过滤了#试试/*注释*/。还有内联注释/*!50000SELECT*/,MySQL会执行括号里的内容,但普通关键字过滤却可能识别不出来,这是绕WAF的经典手法。 - 大小写与关键字混淆:
UNION被过滤了,试试uNiOn,前提是后端过滤时没有做统一小写处理;用了SeLeCt同理。这种方法现在基本过不了成品WAF,但在自写过滤逻辑的站里偶尔有奇效。 - 等价替换:
=被过滤时用LIKE、IN、BETWEEN替代;AND被过滤时用&&;逗号被过滤时在LIMIT 1,2这种场景里试试LIMIT 1 OFFSET 2。报错函数updatexml被过滤时,可以试试extractvalue、GTID_SUBSET、NAME_CONST等。这类替换说到底是“函数/符号的冗余性”,你对SQL语法越熟,替换思路越丰富。 - 编码绕过:URL编码是基础操作,比如
%27代表单引号。但真正高级的玩法是二次编码:后端先解码一次,再交给过滤函数,过滤函数查不出关键字,拼进SQL时又被数据库解码一次——这就是“双重解码”绕过。某些框架的自动解码机制天然存在这个缺陷。 - 参数污染(HPP):
?id=1&id=2,后端可能取第一个值去过滤,取第二个值去拼SQL,这样过滤就形同虚设。WAF层面经常用这种思路绕,但后端要支持重复参数且取数逻辑不一致,条件比较苛刻。
sqlmap的--tamper参数会自动套用大量此类脚本,比如between.py、space2comment.py、randomcase.py。实操建议是:开一个靶场,用--tamper="between,randomcase"跑一遍,再用Burp抓包看看实际发出的请求,能直观感受每种tamper做了什么改动。
2.3 一个真实案例的思考:餐饮系统的not_out_depot参数
2023年公开过一个某数字化餐饮服务系统的SQL注入漏洞,问题出在not_out_depot这个参数上。我专门去看过漏洞描述:它属于POST请求中的库存操作接口,参数直接拼接进SQL,攻击者可以通过构造特殊请求获取数据库中的敏感信息,甚至进一步尝试操作数据。这个案例给我最深的触动不是技术本身,而是它暴露的两个行业现状:
第一,很多所谓“数字化系统”在快速迭代时,开发人员对参数化查询的了解还停留在“听说过”的阶段。业务优先、工期压顶,能跑就行,基本不会做安全测试。第二,单点系统被攻破的后果往往不止一个库——餐饮系统的数据库里连带着会员手机号、地址、消费记录,甚至门店经营数据。数据一旦拖库,影响面远超你的想象。
所以这篇系列文章到第二篇,我特别想强调一个观念:所有绕过技巧的学习,最终目的都应该是“更好地修复”。你只有知道WAF是怎么被绕的,才知道自己的防护规则该怎么写;你只有清楚二次解码漏洞的原理,才会在框架选型和开发规范里提前避免。
3. 靶场到实战:一条可以照抄的SQL注入测试路径
3.1 五个经典靶场的定位与练习重点
想练SQL注入,靶场是最高效的路径,没有之一。我按练手顺序整理一下:
- DVWA(Dam Vulnerable Web Application):四个危险等级(Low/Medium/High/Impossible),从Low直接拼接到Impossible的参数化查询,你能直观感受同一个漏洞从“裸奔”到“彻底堵死”的完整过程。Medium级别引入了
mysqli_real_escape_string(),虽然能防字符型注入,但数字型依然能绕,这就是很好的“过滤不等于安全”教学案例。 - SQLi-Labs:从Less-1到Less-65,每个关卡都对应一类注入场景:单引号字符型、数字型、双引号+括号、报错注入、盲注、堆叠注入、二次注入。这个靶场是分类最细的,适合按章节系统刷。每个关卡看源码里的
$sql拼接方式,比闷头爆破更有收获。 - Pikachu靶场:中文界面,漏洞类型覆盖广,除了SQL注入之外还有XSS、CSRF、SSRF等,适合练习“多个漏洞串联”的思路。它的SQL注入章节里专门有宽字节注入,模拟了GBK编码环境下
%df%27闭合转义符的经典场景。 - CTFHub技能树:它的SQL注入模块是分阶梯的——整数型、字符型、报错注入、布尔盲注、时间盲注、堆叠注入、MySQL特性等等。每个题目一个容器,短平快,适合碎片时间刷。而且它的环境还原度不错,有些题目还顺便考了文件读写,练完能摸到CTF真题的影子。
- sqli-labs的增强版(sqli-labs-plus):适合已经刷完原版的人,它增加了JWT注入、搜索型注入、HEAD注入等进阶题目,很多场景更贴近真实业务接口。
练习节奏上,我的建议是:先拿DVWA的Low和Medium建立基础体感,然后用SQLi-Labs按类型逐个击破,再回CTFHub做限时挑战,最后用Pikachu练综合场景。整个过程配合Burp Suite抓包观察SQL拼接结果,不要只看页面返回。
3.2 完整测试路径:从探测到数据提取的六步走
一套干净利落的注入测试流程,应该是方法论驱动的。我按实际操刀顺序写下来,拿靶场或授权站点可直接照搬:
第一步:参数定位与初步探测
拿到一个目标后,先不要急着上工具。把URL和POST请求里的每个参数都列出来,用Burp或者直接用浏览器带着不同值多打几次,观察页面变化。最基础的探测动作就是“加单引号看报错”:?id=1',如果页面报数据库错误或行为异常,说明参数大概率注入了。再试?id=1 AND 1=1对比?id=1 AND 1=2,响应不一致,基本坐实了布尔盲注。这个阶段的目标不是拿数据,而是“确认可控点”。
第二步:判断注入类型与数据库指纹
在确认注入存在后,先别急着跑数据。用几个语句快速定位类型:
sql复制?id=1' AND '1'='1 -- 字符型假设验证
?id=1 AND 1=1 -- 数字型假设验证
?id=1 AND SLEEP(3) -- 判断是否支持时间盲注,响应慢说明可注入
同时尝试数据库指纹:报错信息里带MySQL、MariaDB字样,或者information_schema可访问,基本判定MySQL系;遇到ORA-开头就是Oracle。锁定数据库类型才能选对后续payload。
第三步:确定字段数量
联合查询的前提是知道列数。最稳的方法是二分法配合ORDER BY:
sql复制?id=1 ORDER BY 1 -- 正常
?id=1 ORDER BY 2 -- 正常
...不断累加
?id=1 ORDER BY 10 -- 报错,说明字段数小于10
报错那个数减1就是当前查询的列数,假设是5,下一步用UNION SELECT 1,2,3,4,5验证——页面上哪些位置显示了数字,哪些就是回显位。
第四步:基于回显提取数据
有回显就用联合查询直取information_schema。MySQL的常规套路:
sql复制?id=0 UNION SELECT 1,2,GROUP_CONCAT(table_name),4,5 FROM information_schema.tables WHERE table_schema=database()
-- 先拿所有表名
?id=0 UNION SELECT 1,2,GROUP_CONCAT(column_name),4,5 FROM information_schema.columns WHERE table_name='users'
-- 再拿目标表的列名
?id=0 UNION SELECT 1,2,GROUP_CONCAT(username,0x3a,password),4,5 FROM users
-- 最后提数据
注意id=0而不是id=1,是为了让前面的查询结果为空,避免干扰UNION返回的数据。
第五步:无回显情况转盲注
如果联合查询页面没有任何回显,那就得靠盲注。布尔盲注的核心是对比页面差异逐字符猜解,效率低但稳定:
sql复制?id=1 AND SUBSTRING((SELECT database()),1,1)='a'
-- 页面对比,逐字符试探库名
?id=1 AND SUBSTRING((SELECT table_name FROM information_schema.tables LIMIT 0,1),1,1)='a'
-- 模式一致,继续猜表名
如果连页面差异都没有,只能上时间盲注:
sql复制?id=1 AND IF(SUBSTRING((SELECT database()),1,1)='a', SLEEP(3), 0)
每次猜字符都等3秒,效率极低,所以时间盲注我强烈建议配合工具做。Burp的Intruder写一个自定义Payload,或者直接用脚本跑,都是可选方案。
第六步:利用方式升级
数据拿到之后,不要停在这个阶段。思考一下还能不能进一步利用:MySQL在root权限且secure_file_priv允许时,可以用INTO OUTFILE写WebShell;站库同服时可以试试LOAD_FILE()读配置文件;堆叠注入能执行UPDATE改数据——这些都取决于数据库权限和配置,但值得一一验证。当然,所有操作都必须控制在授权范围内。我的经验是,把“从注入点到控制服务器”这个链路的可能性探查清楚,比拿个库就交差更能体现测试深度。
4. 真正的攻守之道:为什么修复比发现更重要
4.1 从源码层面看注入的根源:拼接SQL的三种错误姿势
到目前聊了很多“怎么打进去”,但作为一篇面向从业者的文章,我更想让你把一半注意力放在“怎么修”。我审计过不少代码,SQL注入的根源无外乎三种错误姿势:
第一种:直接字符串拼接。 这是我见过最多的写法,也是最原始的漏洞来源。典型代码:
java复制String sql = "SELECT * FROM user WHERE name = '" + name + "'";
这种写法等于把SQL注入了攻击者的手中。修复方案只有一个:换成参数化查询,例如Java里用PreparedStatement。
第二种:过滤函数当安全盾牌。 开发同学知道输入要过滤,于是用replace替换掉单引号、addslashes转义特殊字符——但这类黑名单思路总有漏网之鱼。比如addslashes能转义',但如果数据库字符集是GBK并且程序没有设置连接编码,宽字节注入就能直接吃掉转义符——Pikachu里的宽字节注入题目就是活例子。所以防御的有效性不能靠“拼运气式过滤”,要靠“彻底不拼SQL”的结构性防护。
第三种:存储过程与动态SQL。 有些系统用存储过程封装查询逻辑,如果存储过程内部仍然用EXEC拼接字符串,注入面只是换了个位置,不代表消失。代码审计时,不仅要看业务代码,还要排查数据库里的存储过程定义。
这三种错误姿势的修复优先级非常高:存储过程内的动态SQL、字符串拼接查询、无回显但可盲注的接口——遇到这几类,不管业务多紧急,都应该先提需求修复再谈上线。在甲方工作的人,最痛苦的就是“明明知道这里有洞,但业务死活不配合修”,这种情况我建议你在漏洞报告里写明利用链和数据暴露面,用事实推动修复。
4.2 参数化查询、白名单校验与最低权限的黄金组合
一个稳健的SQL注入防御方案,我倾向用三层组合,而不是单靠某一个技术点:
- 第一层:所有数据库操作强制走参数化查询/预编译语句(如Java的
PreparedStatement、Python的cursor.execute(sql, params)、PHP的PDO预处理)。这是根治法,用户输入永远作为参数传递,数据库不会把它解析成SQL代码。 - 第二层:输入校验白名单化。能用枚举白名单的地方绝不用黑名单——比如
type参数只允许news、article、notice三个值,就写死判断,其他一律拒绝。数字型参数强制转int;字符串参数限制长度和字符集。这不是为了防注入本身,而是为了减少注入面。 - 第三层:数据库账号最小权限。业务连接数据库的账号只赋予DML权限(
SELECT/INSERT/UPDATE/DELETE),绝不使用root或db_owner类账号。这样即使注入点被绕过,攻击者顶多拿数据,INTO OUTFILE写文件、LOAD_FILE()读文件这类高危操作会因为权限不足直接被拒。
这三层组合的威力在于:参数化兜底了90%的注入,白名单把剩下10%的歪路也堵上,最小权限则封死了“注入点被绕过后还能高权限提权”的路径。即使三层中某一层有缺陷,其他层也能缓冲。
还有一层很多团队忽略的是WAF(Web应用防火墙)。在阿里云、腾讯云上开WAF,或者用ModSecurity这类自建方案,本质是“外挂式防护”,能拦掉大多数自动化攻击。但它必须配合上面的代码修复,而不是替代——因为WAF规则总有绕过空间,参数污染、编码混淆、新型绕过姿势都可能击穿规则。把WAF当临时止血,把代码修复当最终目标,这个顺序走不偏。
4.3 修复完成之后:一次完整的验证闭环
修复漏洞不是改完代码就完事。我负责的每个漏洞在修复后,都会走一遍验证流程:
回归测试:用最初构造的注入payload再次尝试,确认注入点已被堵上。这一步看的是“老漏洞”是否真的修好。
绕过尝试:模拟攻击者在原注入点换上等价替换payload、内联注释、编码混淆等变体,确认修复不是“只堵了一个姿势,其他还能绕”。
业务功能验证:确认参数化改造后,正常查询、搜索、翻页逻辑没受影响,响应速度没有明显变慢。
数据库权限复查:核对业务账号权限是否已收敛,相关高危存储过程或触发器是否已排查。
很多开发团队在修完注入后,都会掉进“参数化查询已使用,所以安全了”的陷阱。但如果参数化查询的同时,其他接口仍然在拼接SQL,修了个寂寞。所以我特别建议在我上面说的测试流程里加一条——全站排查同类问题:用同样的拼接模式,去搜代码仓库里有没有其他相似写法,比如WHERE id = $id、AND user = '$user',“一处修复”永远不如“一类修复”实在。
5. 常见问题速查与排查技巧实录
5.1 高频问题速查表
平时在交流群里被问最多的问题,我整理成一个速查表,基本都是可以直接“抄作业”的排查路径:
| 问题 | 原因分析 | 排查与解决思路 |
|---|---|---|
order by N 一直正常,怎么都测不出字段数 |
当前查询不是SELECT *,且本身列数大于你的尝试上限;或参数被双引号包裹,拼的是另一种语法 |
增大N到20甚至50再试;检查引号闭合方式;尝试GROUP BY验证 |
| 报错注入页面什么都看不见 | 站点关闭了错误回显,或用了自定义错误页面 | 放弃报错注入,切布尔/时间盲注;观察响应码差异与响应时间差异 |
SQLSTATE[HY000]: General error |
多为数据库类型或SQL语法错误,常见于payload选错体系 | 确认后端是MySQL还是MariaDB还是PostgreSQL,更换对应函数与注释符 |
| sqlmap跑半天不出数据 | 目标存在WAF拦截,或注入点位于特殊位置(比如JSON参数、Header) | 先手工确认注入类型;给sqlmap加--tamper脚本、加--level=3 --risk=2;必要时放弃工具改手工 |
UNION SELECT回显位置没有数字 |
字段数判断失误或回显被过滤/转义 | 重新用ORDER BY确认字段数;检查页面是否有HTML实体转义,必要时GROUP_CONCAT一次汇出数据 |
| 单引号被直接吞掉,没有报错 | 可能被转义函数处理过(如addslashes);也可能是WAF直接拦了请求 |
尝试宽字节注入%df%27;测试数字型注入不带引号的方式;用编码或变形绕WAF |
5.2 从报错信息到定位漏洞的排查逻辑
实际排查中,报错信息是最珍贵的线索,但很容易被人忽略。我举个例子:访问?id=1%27,页面返回“You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''1''' at line 1”——这句话里至少有四个信息:
- 错误来自MySQL(“MySQL server version”);
- 单引号被解析进了SQL(“near ''1'''”);
- 后端没有预处理参数,直接把字符串拼进了SQL(因为单引号变成了SQL语法的一部分);
- 数据库连接用户有报错回显权限(说明权限配置很粗放,提权机会可能存在)。
这个信息价值比一个完整的数据库表名都高,因为它直接证明了注入点和数据库类型,还暗示了账号权限状态。我建议每个测试者都养成“逐字读报错”的习惯——很多注入点就是这么被顺藤摸瓜找到的。
反过来,如果在真实站点上探测时遇到没报错、没差异、没延迟的“三无”场景,也不要立刻下“不存在注入”的结论。我通常会再测两个特殊输入:一个是单引号和反斜杠混用(\'),另一个是超长字符串(2000个A),观察页面是否出现500状态码。这两种输入如果引起异常,说明底层SQL处理逻辑有特殊之处,可能藏着二次注入或者隐式转换的盲点。把目光从“报错”转移到“行为差异”,排查思路就打开了。
5.3 我的一些个人排查习惯
最后分享几个我平时落地的习惯,它们帮我少走了不少弯路:
凡事先看网络层,再看应用层。 如果测试目标延迟不稳、页面时不时超时,先确认是不是网络波动影响了时间盲注判断,否则很容易被假象误导,得出错误的注入结论。用固定设备、固定网络、多次请求取平均值,是时间盲注的基本素养。
所有payload先在本地靶场验证。 尤其是新学到的绕过姿势,先在SQLi-Labs的对应关卡打一遍,确认它能稳定复现,再去实际目标上尝试。这样能提高效率,也避免在目标上试出一种“看起来有效但其实没通”的假干净。
笔记比工具更重要。 每次测试,我都会记录下目标的参数位置、注入类型、数据库指纹、尝试过的payload变体以及耗时情况。这些信息在编写漏洞报告时非常宝贵——一个详实的过程记录能让漏洞报告更可信,也能让你在复测时快速切入,不用重新来过。很多人觉得做渗透测试就是“跑完工具截图交报告”,实际上测试过程的笔记质量决定了报告质量的90%。
尊重授权边界。 最后一点,也是最重要的一点。每次测试前确认自己拥有测试该目标的合法授权,明确哪些域名、哪些IP、哪些操作是被允许的。只测试授权范围内的资产,不下载不必要的高敏感数据,发现漏洞后第一时间按流程上报而不扩大影响面。安全从业者的价值在于保护和修复,而不是展示破坏力。这条底线,比任何技术都重要。
