1. 为什么SQL注入还能被反复提起从一次靶场复现说起前段时间我在本地重新搭了一遍pikachu靶场原本只是想给身边入门安全的朋友录一套实操视频结果发现一个挺有意思的现象即使到了2025年SQL注入这个被讨论了几十年的老话题依然在搜索热词里常年霸榜。无论是CTFHub技能树里的sql注入模块还是DVWA、pikachu靶场甚至真实世界的漏洞报告数字型、字符型、搜索型这三类注入依然是最常见的入口。很多人一听到SQL注入原理就觉得是一道面试八股题但如果真的去翻真实漏洞库会发现大量业务系统的问题依然出在最基础的拼接查询上。比如之前公开的某餐饮数字化服务系统not_out_depot接口SQL注入漏洞问题就出在参数直接拼进了SQL语句攻击者不需要任何高级技巧就能拖库。这说明一个事实看起来过时的原理恰恰是攻击面最广、影响最直接的问题。这篇内容我会把三种最常见的注入类型拆开讲清楚。为什么有的参数加个单引号就报错有的参数加了反而正常还有的参数在搜索框里怎么测都测不出问题这些现象背后的SQL拼接逻辑、闭合方式、注释符用法以及对应的修复手段我会结合实际测试过程逐个展开。适合刚接触Web安全的初学者也适合那些在开发岗位但没系统看过注入原理的朋友。先说一个核心认知SQL注入的本质是程序把用户输入的内容当成SQL代码的一部分来执行。后端代码里如果存在字符串拼接查询的场景同时又没有对输入做任何约束那么用户就可以通过构造特殊字符改变SQL语句原本的执行逻辑。数字型、字符型、搜索型只是三种最常见的拼接形态它们的攻击原理同源但判断方法、利用技巧、修复方式各有差异。1.1 注入的根源只有一个没有分隔数据和代码拿PHP加MySQL来举例一段最简单的数字型注入代码如下$id $_GET[id]; $sql SELECT * FROM users WHERE id $id; $result mysqli_query($conn, $sql);问题在于程序员本来希望$id是一个数值数据但代码直接把它拼进了SQL字符串。如果用户在URL传id1 and 12最终执行的SQL就变成了SELECT * FROM users WHERE id 1 and 12这就是典型的代码与数据未分离输入的部分内容被数据库当成了查询条件的一部分来解析执行。and 12是数据库能识别的表达式不是用户数据但程序已经无法区分了。字符型和搜索型的问题模型完全一样只是拼法不同。字符型多了引号包裹搜索型多了百分号包裹。所以如果你理解了拼接导致注入后面所有的类型变化都是在看怎么绕开那层包裹字符。1.2 三种类型为什么必须分开讲我见过不少新手学会了数字型的注入payload之后拿着and 11去测一个字符型参数结果发现页面没有反应就以为没有漏洞。这就是没有搞懂闭合逻辑导致的误判。数字型参数不需要闭合任何符号字符型参数需要闭合单引号搜索型参数需要同时处理百分号和单引号。这三种情况分别对应了不同的SQL语句形态如果不去理解参数在语句中的位置和上下文只靠背payload是走不远的。下面我会用一个统一的留言板例子贯穿全文讲清楚每一步测试的意图和依据。2. 注入点识别三分钟判断参数是数字型还是字符型学习SQL注入的第一件事不是学payload而是学会判断一个参数到底是不是注入点以及它属于哪种类型。很多教程直接给万能密码或者union select的最终利用语句却没有解释为什么第一步要测1 and 11导致读者完全不懂逻辑。判断的逻辑其实很朴素通过构造合法与非法两种输入观察页面响应是否出现差异从而推断当前参数是否被拼进了SQL语句以及它被包裹的方式。2.1 基础判断流程先加引号再看注释假设有个查询用户信息的接口访问地址是http://localhost/user.php?id1打开后正常显示id为1的用户。接下来按顺序测试以下几个请求http://localhost/user.php?id1 http://localhost/user.php?id1 -- - http://localhost/user.php?id1 and 11 http://localhost/user.php?id1 and 12第一步加一个单引号。如果后端SQL是字符型拼接原来的语句会变成SELECT * FROM users WHERE id 1多出一个单引号导致语句语法错误页面会报错或者返回空白。如果后端是数字型拼接语句变成SELECT * FROM users WHERE id 1同样会报错。所以单引号能测出参数可能在SQL中被使用但还无法区分类型。第二步在单引号后面加注释符-- -。把请求改成id1 -- -。如果页面恢复正常说明后端是字符型拼接单引号闭合了SQL中的左引号注释符把SQL中原本的右引号注释掉语句重归合法。数字型拼接不认引号加注释符通常无法恢复。第三步用and 11和and 12做对比测试。如果id1 and 11页面正常而id1 and 12页面无数据说明参数大概率是数字型注入点。因为and 12恒为假整个查询条件无法匹配到任何行。2.2 搜索型参数怎么判断搜索框的测试逻辑会略有不同因为它通常长这样SELECT * FROM messages WHERE content LIKE %$keyword%我先随便搜一个正常词比如你好页面把包含你好的留言都列出来。接着搜索%如果页面返回了全部留言说明%没有被转义直接被当成了SQL通配符。再搜索% and 11 -- 如果页面依然显示全部留言且用% and 12 -- 时页面空白基本可确认搜索框存在字符型注入且注入点位于LIKE子句内部。2.3 一张表格总结三种参数的特征参数类型典型SQL形态单引号测试注释符恢复and 11测试数字型WHERE id $id报错无法恢复页面正常字符型WHERE name $name报错可恢复需先闭合引号搜索型WHERE content LIKE %$kw%结果异常但未必报错视闭合方式而定需配合百分号闭合注意判断过程中页面是否报错并不是唯一标准。有些程序员在代码里写了mysqli_error()报错信息会直接显示出来有些则隐藏了错误并返回空白页。空白页和报错页同样都是异常关键要对比正常输入与异常输入的响应差异而不是只看有没有数据库报错文本。2.4 一个经常被忽视的细节参数位置决定判断难度同一个接口的不同参数可能是不同类型的注入点。比如user.php?id1nameadminid可能是数字型name可能是字符型。我见过有人用id测出注入就默认整个接口只有一种类型结果在另一个参数上漏掉了漏洞。判断注入点时每个参数都要独立测试尤其是POST请求中的参数。3. 数字型注入攻击面最小却最容易翻车数字型注入通常出现在ID、年龄、数量、价格这类数值字段上。它的攻击条件最简单因为SQL语句中不需要任何引号包裹只要后端没有对数据类型做校验输入的数字就会直接拼接进查询语句。3.1 数字型注入的完整攻击过程回看开头的代码$id $_GET[id]; $sql SELECT * FROM users WHERE id $id;攻击者访问http://localhost/user.php?id1 order by 3执行的SQL变为SELECT * FROM users WHERE id 1 order by 3没有报错说明查询结果存在3列。这里order by是用来探测字段数量的通用技巧适用于任何需要union select的场景。接着用http://localhost/user.php?id-1 union select 1,2,3id-1是为了让前面的查询结果为空union select才能把我们自定义的1、2、3填充到结果集中。页面上如果显示了数字2和3说明第2和第3列是数据回显位置之后把2替换成database()就能获取当前数据库名。整个过程的SQL拼接逻辑非常直观不需要考虑引号、注释符、闭合方式所以我把数字型称为攻击面最小的一种。攻击者付出的构造成本最低漏洞存在与否几乎一目了然。3.2 数字型为什么容易翻车强转函数与参数化翻车指的不是攻击者翻车而是审计方和开发者容易产生错觉。很多开发者会用intval()过滤$id intval($_GET[id]);这种写法下任何非数字字符都会被截断或转为0注入直接失效。但问题在于并非所有看似数字的参数都走了统一过滤。有些接口为了省事直接写$_GET[id]有些则是$_POST[count]、$_GET[page]这类辅助参数。我在测试一些系统时发现主参数防护做得很到位但分页参数page、排序参数sort这类不起眼的数值参数反而没过滤最终整个查询还是被攻破了。对开发者来说最稳的修复其实不是过滤而是把所有的数值参数统一做类型校验或使用参数化查询。对测试者来说数字型参数即使看起来有intval也要注意是否存在其他入口没有过滤的情况。3.3 数字型注入的进阶干扰加减乘除也能当探针分享一个判断数字型注入的小技巧传id2-1如果页面显示的是id为1的记录那基本可以断定参数是纯数字拼接因为数据库执行了WHERE id 2-1并且把结果算成了1。字符型拼接则不会出现这种计算效果因为2-1被当成字符串处理。这个技巧在加了intval时的区别也很明显intval(2-1)的结果是2页面显示的还是id为2的记录说明过滤已经开始工作了。4. 字符型注入引号和注释符的攻防游戏字符型注入是安全课上出镜率最高的一种也是许多万能密码传说的来源。它的SQL语句形态通常是SELECT * FROM users WHERE username $name AND password $pass这种写法常见于早期登录功能。因为用户名和密码都是文本所以后端必须用单引号把变量包起来。如果变量未经过滤攻击者构造的用户名就能提前截断SQL语句改变整个查询逻辑。4.1 闭合引号的底层逻辑假设用户名字段是字符型拼接$name $_GET[name]; $sql SELECT * FROM users WHERE name $name;当我输入name admin and 11最终SQL变成SELECT * FROM users WHERE name admin and 11这里的关键是输入的admin闭合了SQL语句中name 后面的左引号and 11补了一个新的恒真条件闭合了语句末尾的右引号。整个语句结构被改写原本必须匹配name的条件不再生效因为后面还多了一个恒真的11。所以字符型注入的玩法核心就是三件事闭合前面的引号注入新的SQL片段处理后面残余的引号。处理残余引号有两条路一是用注释符把后面的内容全部注释掉二是再补一个引号让语句结构重新合法。两种思路延伸出了两个方向的payload家族。4.2 注释符的使用差异--、#、/* */很多数据库方言里注释符的写法不同。MySQL里#和-- -都常用但--后面必须跟一个空格或控制字符否则不生效。所以我更推荐写成-- -也就是两个短横线加一个空格加一个短横线这样在不同环境下都容易生效。经典字符型注入payload通常长这样 or 11 -- - or 11# or 11以登录为例输入用户名admin or 11 -- -后SQL变成SELECT * FROM users WHERE username admin or 11 -- - AND password xxx注释符把后面的AND password判断给消掉了而or 11恒真这个查询就会返回数据库里所有用户记录。很多旧系统用mysqli_fetch_array只取第一行结果于是攻击者就以第一个用户的身份登录成功了这就是万能密码绕过的原理。4.3 字符型注入的union利用先闭合再探测用union select拖数据前字符型参数也需要先解决闭合问题。以sqli-labs靶场的第一关为例http://localhost/less1/?id1 order by 3 -- -当输入1时参数闭合了SQL中id字段的左引号order by 3用来探测列数-- -注释掉语句末尾的右引号和后续内容。如果探测到3列能正常排序、4列报错下一步就能用http://localhost/less1/?id-1 union select 1,2,3 -- -这个过程中最容易被新手卡住的点在于union select前面的查询必须返回空结果否则我们自定义的联合查询内容会排在后面看不到。把id改成负数或者一个不存在的值就是为了让前段查询结果为空让union后面的内容直接显示在页面里。4.4 报错注入当页面不回显数据时怎么办有回显的字符型注入处理起来最方便但实际系统里可能页面只显示成功或失败不展示数据库内容。此时可以尝试报错注入比如MySQL里常见的updatexml和extractvalue。以updatexml为例and updatexml(1,concat(0x7e,(select database()),0x7e),1)原理是让updatexml的第二个参数传入非法的XPath表达式于是数据库在执行时报错错误信息里带上我们恶意拼接的查询结果。这种用法不需要union不需要回显位只需要页面上能看到数据库报错信息即可。报错注入在CTF、实战排错里都非常常用甚至在搜索型注入里也经常作为备用手段因为搜索类接口往往没有回显位。5. 搜索型注入那个被所有教程都忽略的LIKE子句陷阱搜索型注入是三类里最容易被忽略的原因很简单等值查询大家都认识但模糊搜索的注入点在逻辑上绕了一层而且很多搜索功能的代码编写者会下意识觉得搜索嘛用户输入什么都行反正只查LIKE不出错。5.1 搜索结果的异常一个百分号引发的数据泄露拿一个留言板搜索功能举例后端可能是$keyword $_GET[keyword]; $sql SELECT * FROM messages WHERE content LIKE %$keyword%;正常情况下输入投诉查询内容里带投诉的留言全被搜出来。但如果攻击者输入的是%SQL变成SELECT * FROM messages WHERE content LIKE %%%一个百分号作为通配符匹配所有内容两个百分号实际上等于把所有留言全部返回。我在一次测试里用这个方式验证了一个系统的未授权数据泄露风险原本搜索功能是按用户权限过滤留言的但直接搜%把所有数据都带出来了包括不属于当前用户的内容。这个问题在代码层面看就是一行like拼接危害却比等值查询的注入更隐蔽。5.2 搜索型注入的闭合过程详解搜索型注入之所以难判断是因为引号和百分号都要处理好。假设输入xxx% and 11 -- 整个SQL变成SELECT * FROM messages WHERE content LIKE %xxx% and 11 -- %%在这里攻击者输入的xxx%做了三件事先用xxx作为搜索词然后用%闭合了SQL中LIKE %的百分号再用闭合了左引号。紧接着加入and 11把查询条件改为恒真最后-- 把原语句末尾的%注释掉。对比字符型的闭合搜索型多了百分号闭合这一步。所以很多直接套用字符型payload的人输入 or 11 -- -时SQL变成SELECT * FROM messages WHERE content LIKE % or 11 -- -%这其实也能触发注入因为闭合了左引号后or 11让整个查询变为恒真。不过在测试阶段我更推荐用% and 11 -- 和% and 12 -- 这一组做对比能够更明确地确认注入点位于LIKE子句的上下文中而不是普通的where条件。5.3 搜索型注入的利用与限制确定搜索型注入后union select也能用典型的payload% union select 1,2,database() -- 由于搜索语句通常查询多条记录union结果会追加在搜索结果后面。但要注意搜索型查询往往返回多条数据用id改为负数在搜索型里不太适用。如果想只显示union的结果可以这样构造xxx% and 12 union select 1,2,database() -- 先用and 12让前面的查询结果为空再通过union引入自定义数据这样就能控制显示内容。限制方面搜索型注入遇到的过滤往往更麻烦。因为搜索关键字需要接受大量特殊字符很多系统会把%、_、等字符直接转义或者会限制搜索长度。在有转义的情况下闭合方式就要重新设计比如用宽字节绕过、双重编码等思路这已经属于进阶话题了。5.4 真实漏洞的启示搜索型洞比想象中常见回看热词里大家搜过的那条喰星云·数字化餐饮服务系统not_out_depot sql注入漏洞这类情况往往都是业务接口里的参数直接拼SQL但报漏洞时标的参数名很多人从没听过。现实中很多搜索接口不叫search而是一个个看着像业务逻辑的函数名目录扫描扫不出来白盒审计又容易被业务代码淹没。所以在测试任何搜索功能时不要只盯着带search字样的参数所有传到后端的参数都应该是排查对象。6. 防御侧从参数化查询到WAF绕过的那点事讲完了三种注入的原理和利用方式再讲防御。这块是我最想强调的单纯的过滤方案防不住所有类型而参数化查询能从根本上解决注入问题。6.1 参数化查询为什么能根治SQL注入参数化查询也叫预编译查询核心思路是把SQL语句的结构和参数值分开传送给数据库。数据库先解析并编译SQL语句结构再把参数值作为纯数据传入参数内容再特殊也不会被当成SQL代码解析。以PHP PDO为例$stmt $pdo-prepare(SELECT * FROM users WHERE id ?); $stmt-execute([$id]);这里?是占位符$id只作为值传入即便$id包含1 or 11数据库也只会把它当作id字段的一个字符串值去比较根本不会执行or后面的SQL逻辑。对于字符型参数$stmt $pdo-prepare(SELECT * FROM users WHERE name ?); $stmt-execute([$name]);对于搜索型参数$stmt $pdo-prepare(SELECT * FROM messages WHERE content LIKE ?); $stmt-execute([%$keyword%]);注意搜索型这里占位符传的是拼接好百分号的完整模式串而不是直接拼接SQL。参数化之后用户输入只是普通的字符串%、都只是字符本身不可能再影响SQL结构。我个人认为所有新代码都应该默认走预编译这是底线。遗留系统如果确实难以大改至少要做白名单校验或类型强转比如数字参数用intval()枚举参数用白名单数组匹配但这只是过渡方案。6.2 过滤函数为什么经常失效很多开发者喜欢写一个clean()函数把所有、、or替换成空字符串。这种做法我见过太多翻车案例了。过滤本身是黑名单思路总有绕过的方式大小写绕过、注释符绕过、URL编码绕过、宽字节绕过更不用说数据库函数、十六进制写法这些变体。比如or 11过滤掉or之后还是可以写成|| 11过滤了单引号还能用0x27这种十六进制表示。只要注入点是拼接型的过滤函数就是在跟整个数据库的函数库和语法集玩猫鼠游戏永远封不完。所以我的建议是开发侧用参数化查询把注入问题从根上掐掉安全侧再用WAF作为第二层保险处理那些漏网的、历史遗留的接口。顺序不能反依赖WAF而忽略预编译是最危险的心态。6.3 手工修复三种注入的代码对照注入类型不安全写法修复方式数字型$sql SELECT * FROM users WHERE id $id;PDO预处理 bindValue(:id, $id, PDO::PARAM_INT)字符型$sql SELECT * FROM users WHERE name $name;PDO预处理 bindValue(:name, $name)搜索型$sql SELECT * FROM messages WHERE content LIKE %$kw%;PDO预处理 bindValue(:kw, %$kw%)写完代码后还要做一件事验证。用注入payload重新测一遍确认报错现象消失了同时原功能不受影响。搜索功能在修复后尤其要检查正常关键字是否可以正常搜索、百分号搜出来的结果是否为空。6.4 对测试者的一句提醒写到这里还是要提醒一句SQL注入测试必须在授权范围内进行。靶场pikachu、DVWA、sqli-labs、CTFHub和本地环境是练手的好地方但挖洞测试只能在你拥有合法授权的目标上进行。能力越大越要守住边界这个行业能走多远靠的是技术更是分寸感。7. 写在最后给入门者的一些真心话回到开头那句话SQL注入确实不算新攻击技术但热门搜索词不会骗人真实世界里的漏洞报告也不会骗人。只要还有系统在用字符串拼接拼SQLSQL注入就会一直存在。学完这三种类型之后我建议你按这个顺序去巩固先在sqli-labs里分别找到Less-1、Less-2和搜索型相关关卡把闭合、注释符、union流程各练三遍再用pikachu靶场做一遍完整复现最后再看一遍真实漏洞报告中涉及的业务接口参数。我个人在实际测试中最深的体会是SQL注入的判断能力比背payload重要得多。一个参数到底有没有注入、属于哪种类型、需要闭合几个符号这些信息都能通过构造输入和观察响应来获得。当你不再依赖记忆payload而是能根据SQL拼接形态现场推演出正确构造方式时才算真正理解了这个漏洞的原理。最后分享一个排查清单加引号看报错补注释看恢复用and 11和and 12看页面差异再用order by探测字段数最后用union select验证回显位置。把这条流程跑顺了数字型、字符型、搜索型你都能在几分钟内给出明确的结论。
