☰
SQL注入三种类型详解:数字型、字符型、搜索型原理与防御
2026/9/26 6:35:57 网站建设 项目流程

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传id=1 and 1=2,最终执行的SQL就变成了:

SELECT * FROM users WHERE id = 1 and 1=2

这就是典型的"代码与数据未分离":输入的部分内容被数据库当成了查询条件的一部分来解析执行。and 1=2是数据库能识别的表达式,不是用户数据,但程序已经无法区分了。

字符型和搜索型的问题模型完全一样,只是拼法不同。字符型多了引号包裹,搜索型多了百分号包裹。所以如果你理解了"拼接导致注入",后面所有的类型变化,都是在看怎么绕开那层包裹字符。

1.2 三种类型为什么必须分开讲

我见过不少新手,学会了数字型的注入payload之后,拿着and 1=1去测一个字符型参数,结果发现页面没有反应,就以为没有漏洞。这就是没有搞懂闭合逻辑导致的误判。

数字型参数不需要闭合任何符号,字符型参数需要闭合单引号,搜索型参数需要同时处理百分号和单引号。这三种情况分别对应了不同的SQL语句形态,如果不去理解参数在语句中的位置和上下文,只靠背payload是走不远的。下面我会用一个统一的留言板例子贯穿全文,讲清楚每一步测试的意图和依据。

2. 注入点识别:三分钟判断参数是数字型还是字符型

学习SQL注入的第一件事,不是学payload,而是学会判断一个参数到底是不是注入点,以及它属于哪种类型。很多教程直接给"万能密码"或者union select的最终利用语句,却没有解释为什么第一步要测1 and 1=1,导致读者完全不懂逻辑。

判断的逻辑其实很朴素:通过构造合法与非法两种输入,观察页面响应是否出现差异,从而推断当前参数是否被拼进了SQL语句,以及它被包裹的方式。

2.1 基础判断流程:先加引号,再看注释

假设有个查询用户信息的接口,访问地址是:

http://localhost/user.php?id=1

打开后正常显示id为1的用户。接下来按顺序测试以下几个请求:

http://localhost/user.php?id=1' http://localhost/user.php?id=1' -- - http://localhost/user.php?id=1 and 1=1 http://localhost/user.php?id=1 and 1=2

第一步,加一个单引号。如果后端SQL是字符型拼接,原来的语句会变成:

SELECT * FROM users WHERE id = '1''

多出一个单引号导致语句语法错误,页面会报错或者返回空白。如果后端是数字型拼接,语句变成:

SELECT * FROM users WHERE id = 1'

同样会报错。所以单引号能测出"参数可能在SQL中被使用",但还无法区分类型。

第二步,在单引号后面加注释符-- -。把请求改成id=1' -- -。如果页面恢复正常,说明后端是字符型拼接:单引号闭合了SQL中的左引号,注释符把SQL中原本的右引号注释掉,语句重归合法。数字型拼接不认引号,加注释符通常无法恢复。

第三步,用and 1=1和and 1=2做对比测试。如果id=1 and 1=1页面正常,而id=1 and 1=2页面无数据,说明参数大概率是数字型注入点。因为and 1=2恒为假,整个查询条件无法匹配到任何行。

2.2 搜索型参数怎么判断

搜索框的测试逻辑会略有不同,因为它通常长这样:

SELECT * FROM messages WHERE content LIKE '%$keyword%'

我先随便搜一个正常词,比如"你好",页面把包含"你好"的留言都列出来。接着搜索:

%

如果页面返回了全部留言,说明%没有被转义,直接被当成了SQL通配符。再搜索:

%' and 1=1 -- '

如果页面依然显示全部留言,且用%' and 1=2 -- '时页面空白,基本可确认搜索框存在字符型注入,且注入点位于LIKE子句内部。

2.3 一张表格总结三种参数的特征

参数类型典型SQL形态单引号测试注释符恢复and 1=1测试
数字型WHERE id = $id报错无法恢复页面正常
字符型WHERE name = '$name'报错可恢复需先闭合引号
搜索型WHERE content LIKE '%$kw%'结果异常但未必报错视闭合方式而定需配合百分号闭合

注意,判断过程中页面是否报错并不是唯一标准。有些程序员在代码里写了mysqli_error(),报错信息会直接显示出来;有些则隐藏了错误并返回空白页。空白页和报错页同样都是"异常",关键要对比正常输入与异常输入的响应差异,而不是只看有没有数据库报错文本。

2.4 一个经常被忽视的细节:参数位置决定判断难度

同一个接口的不同参数,可能是不同类型的注入点。比如user.php?id=1&name=admin,id可能是数字型,name可能是字符型。我见过有人用id测出注入,就默认整个接口只有一种类型,结果在另一个参数上漏掉了漏洞。判断注入点时,每个参数都要独立测试,尤其是POST请求中的参数。

3. 数字型注入:攻击面最小却最容易翻车

数字型注入通常出现在ID、年龄、数量、价格这类数值字段上。它的攻击条件最简单,因为SQL语句中不需要任何引号包裹,只要后端没有对数据类型做校验,输入的数字就会直接拼接进查询语句。

3.1 数字型注入的完整攻击过程

回看开头的代码:

$id = $_GET['id']; $sql = "SELECT * FROM users WHERE id = $id";

攻击者访问:

http://localhost/user.php?id=1 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,3

id=-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 数字型注入的进阶干扰:加减乘除也能当探针

分享一个判断数字型注入的小技巧:传id=2-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 '1'='1

最终SQL变成:

SELECT * FROM users WHERE name = 'admin' and '1'='1'

这里的关键是:输入的admin'闭合了SQL语句中name =后面的左引号,and '1'='1补了一个新的恒真条件,'闭合了语句末尾的右引号。整个语句结构被改写,原本必须匹配name的条件不再生效,因为后面还多了一个恒真的'1'='1。

所以字符型注入的玩法核心就是三件事:闭合前面的引号,注入新的SQL片段,处理后面残余的引号。处理残余引号有两条路,一是用注释符把后面的内容全部注释掉,二是再补一个引号让语句结构重新合法。两种思路延伸出了两个方向的payload家族。

4.2 注释符的使用差异:--、#、/* */

很多数据库方言里,注释符的写法不同。MySQL里#和-- -都常用,但--后面必须跟一个空格或控制字符,否则不生效。所以我更推荐写成-- -,也就是两个短横线加一个空格加一个短横线,这样在不同环境下都容易生效。

经典字符型注入payload通常长这样:

' or 1=1 -- - ' or 1=1# ' or '1'='1

以登录为例,输入用户名admin' or 1=1 -- -后,SQL变成:

SELECT * FROM users WHERE username = 'admin' or 1=1 -- -' AND password = 'xxx'

注释符把后面的AND password判断给消掉了,而or 1=1恒真,这个查询就会返回数据库里所有用户记录。很多旧系统用mysqli_fetch_array只取第一行结果,于是攻击者就以第一个用户的身份登录成功了,这就是"万能密码绕过"的原理。

4.3 字符型注入的union利用:先闭合再探测

用union select拖数据前,字符型参数也需要先解决闭合问题。以sqli-labs靶场的第一关为例:

http://localhost/less1/?id=1' 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 1=1 -- '

整个SQL变成:

SELECT * FROM messages WHERE content LIKE '%xxx%' and 1=1 -- '%%'

在这里,攻击者输入的xxx%'做了三件事:先用xxx作为搜索词,然后用%闭合了SQL中LIKE '%的百分号,再用'闭合了左引号。紧接着加入and 1=1把查询条件改为恒真,最后-- '把原语句末尾的%'注释掉。

对比字符型的闭合,搜索型多了"百分号闭合"这一步。所以很多直接套用字符型payload的人,输入' or 1=1 -- -时,SQL变成:

SELECT * FROM messages WHERE content LIKE '%' or 1=1 -- -%''

这其实也能触发注入,因为'闭合了左引号后,or 1=1让整个查询变为恒真。不过在测试阶段,我更推荐用%' and 1=1 -- '和%' and 1=2 -- '这一组做对比,能够更明确地确认注入点位于LIKE子句的上下文中,而不是普通的where条件。

5.3 搜索型注入的利用与限制

确定搜索型注入后,union select也能用,典型的payload:

%' union select 1,2,database() -- '

由于搜索语句通常查询多条记录,union结果会追加在搜索结果后面。但要注意,搜索型查询往往返回多条数据,用id改为负数在搜索型里不太适用。如果想只显示union的结果,可以这样构造:

xxx%' and 1=2 union select 1,2,database() -- '

先用and 1=2让前面的查询结果为空,再通过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 1=1,数据库也只会把它当作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 1=1,过滤掉or之后还是可以写成|| 1=1;过滤了单引号,还能用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注入确实不算新攻击技术,但热门搜索词不会骗人,真实世界里的漏洞报告也不会骗人。只要还有系统在用字符串拼接拼SQL,SQL注入就会一直存在。学完这三种类型之后,我建议你按这个顺序去巩固:先在sqli-labs里分别找到Less-1、Less-2和搜索型相关关卡,把闭合、注释符、union流程各练三遍,再用pikachu靶场做一遍完整复现,最后再看一遍真实漏洞报告中涉及的业务接口参数。

我个人在实际测试中最深的体会是:SQL注入的判断能力,比背payload重要得多。一个参数到底有没有注入、属于哪种类型、需要闭合几个符号,这些信息都能通过构造输入和观察响应来获得。当你不再依赖记忆payload,而是能根据SQL拼接形态现场推演出正确构造方式时,才算真正理解了这个漏洞的原理。

最后分享一个排查清单:加引号看报错,补注释看恢复,用and 1=1和and 1=2看页面差异,再用order by探测字段数,最后用union select验证回显位置。把这条流程跑顺了,数字型、字符型、搜索型,你都能在几分钟内给出明确的结论。

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

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

立即咨询