很多人把SQLi-Labs前面几关打通之后会陷入一种错觉SQL注入不就是 or 11#、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查询资料的接口正常请求是?id1拼入SQL后是SELECT first_name, last_name FROM users WHERE user_id 1;当输入变成1 or 11时SELECT first_name, last_name FROM users WHERE user_id 1 or 11;or 11让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模块实测DVWADamn 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 11正常显示用户信息。输入1 and 12页面无数据。输入1 or 11查出了所有用户。输入1 union select user, password from users#能把数据库账号和密码哈希直接打出来。操作方法是在?id后面直接拼上这些payload比如http://127.0.0.1/dvwa/vulnerabilities/sqli/?id1 union select user, password from users#SubmitSubmit注意DVWA需要登录Cookie否则会被重定向到登录页。用浏览器正常登录后再访问这个URL就行。#在浏览器地址栏里会被当作锚点所以如果要手输URL最好用%23代替#或者用Burp Suite改包发送。2.2 判断数字型还是字符型的标准方法无论是什么靶场判断数字型还是字符型都有一套固定验证方法。不要因为参数值看起来像数字就默认它是数字型要看闭合效果。判断数字型注入输入?id1 and 11页面正常。输入?id1 and 12页面异常或无数据。如果两种情况有明显差异说明1 and 11被拼进SQL后参与运算了这是数字型。判断字符型注入输入?id1 and 11页面正常。输入?id1 and 12页面异常或无数据。如果这两种情况有差异说明你输入的单引号闭合了SQL语句中的引号这是字符型。两者本质区别就是闭合符号。数字型不需要闭合字符型需要闭合单引号、双引号有时候还涉及括号。可以用一个表格来对比类型SQL拼接示例判断payload常见位置数字型WHERE id $id1 and 11ID、文章编号等整数参数字符型WHERE name $name1 and 11用户名、搜索关键词、标题等字符串参数搜索型WHERE title LIKE %$key%1 and 11或%通配符搜索框这个判断方法适用于所有数据库。MySQL、SQL Server、Oracle里SQL语法差异不小但and 11和and 12的判定逻辑是通用的。2.3 一个经典误判DVWA不是数字型回到DVWA。网络上大量教程把DVWA的SQL Injection模块写成数字型理由是输入框要求填ID数字。但看源码就知道查询直接用了WHERE user_id $id单引号包着的。用数字型的payload1 and 11去测实际上试不出来因为它会被当成字符串的一部分根本不会参与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/?id1页面报错You have an error in your SQL syntax... near 1 LIMIT 0,1。这是字符型注入的标志。然后按顺序执行?id1 ORDER BY 3--页面正常。继续?id1 ORDER BY 4--页面报错。说明原查询只有3列。接着?id1 UNION SELECT 1,2,3--页面显示出2和3两个数字在页面上说明第2列和第3列是可以回显的。接下来把2替换成数据库名3替换成版本号?id1 UNION SELECT 1,database(),version()--拿到数据库名security。继续爆表?id1 UNION SELECT 1,group_concat(table_name),3 FROM information_schema.tables WHERE table_schemadatabase()--会看到所有表名。然后爆字段、爆数据思路完全一样。Less-2是数字型注入直接?id1 ORDER BY 3--不需要单引号。如果Less-1你用的是闭合Less-2再用同样的payload就会报错。这个对比能很好地强化“先判断类型”的意识。3.2 页面没有回显时报错注入与布尔盲注联合注入很舒服但现实中很多页面不会显示SQL查询结果。这时候就要换方案了取决于页面还有什么“反应”。报错注入前提是数据库报错信息会显示在页面上。SQLi-Labs Less-5就是这个类型。访问http://127.0.0.1/sqli-labs/Less-5/?id1页面不会显示数据库内容但会输出SQL错误。用updatexml()函数可以人为制造报错并让报错信息里带上我们想要的数据?id1 AND updatexml(1,concat(0x7e,database(),0x7e),1)--0x7e是~符号的十六进制把它和查询结果拼在一起updatexml第二参数不是合法的XPath表达式就会触发报错报错信息里包含了~security~。这样数据库名就出来了。布尔盲注适用于页面不显示报错、也没有任何数据回显但正确和错误条件页面表现不同。SQLi-Labs Less-8是典型?id1 AND ascii(substr(database(),1,1))100--如果数据库名第一个字符的ASCII码大于100页面显示You are in...........否则什么都不显示。通过不断改变ASCII值逐字符猜出数据库名。手工操作很慢可以用二分法或者写脚本但核心逻辑就是“利用页面差异传递信息”。时间盲注适用于页面完全没有任何差异的情况。SQLi-Labs Less-9就是无论条件真假页面都显示You are in...........。这时候只能用SLEEP()制造时间差?id1 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-93.3 为什么报错注入和盲注不能用同一套payload很多人学会联合注入之后在报错注入场景里还是习惯性打UNION SELECT结果页面没反应就以为不存在注入。实际上不是没有注入而是数据没有回显。联合查询依赖查询结果被输出到页面。报错注入依赖数据库错误信息被输出。布尔盲注依赖页面状态变化。时间盲注只依赖时间差。这四种方式的“信号通道”完全不一样所以测试的时候是一层层往下走的先看页面有没有数据位 → 有用联合查询。没数据位但报错信息还在 → 用报错注入。报错信息被吞了但正确和错误页面不同 → 用布尔盲注。页面完全一样只剩时间这个变量 → 用时间盲注。在靶场里可以按SQLi-Labs的关卡顺序练习Less-1到Less-4是联合查询Less-5是报错Less-8是布尔盲注Less-9是时间盲注。一路打下来这种“由浅入深”的判断逻辑就会变成肌肉记忆。4. 容易忽视的两个分类维度注入位置与堆叠/万能密码场景4.1 从GET到POSTSQLi-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 ... unameadminpasswd123把请求包发给Repeater修改uname参数unameadmin AND 11#passwd123页面提示登录成功但回显正常改成unameadmin AND 12#passwd123页面显示用户名不正确。说明存在字符型注入。后面就是正常的联合查询流程只不过payload的位置在POST请求体里。Pikachu靶场也有一个“数字型注入”模块同样是POST方式。开Burp抓包就能看到参数以POST形式提交然后按数字型注入的方式测试id1 AND 11 id1 AND 12为什么说这个维度容易被忽视因为很多漏洞扫描工具默认只测GET参数如果目标站点的主要入口是登录框和搜索框盯着URL测等于漏掉了最大的攻击面。手动测试时把GET、POST、Cookie、HTTP头都测一遍才算是完整。4.2 堆叠注入与万能密码的靶场场景堆叠注入Stacked Queries和其他类型不太一样它不是“查询结果怎么展示”的问题而是能不能执行多条SQL语句的问题。原理是很多后端的数据库接口允许一次发送多条SQL语句用分号分隔。这种注入的危害比普通注入更大因为可以执行增删改查。SQLi-Labs Less-38是堆叠注入的例子。在?id1后面加一条插入语句?id1;INSERT INTO users(id,username,password) VALUES (88,test,123456)--执行之后去数据库里查会发现多了一行数据。注意不是所有PHP接口都支持堆叠注入PDO的某些配置会禁止多条语句执行而老式的mysql_query()则支持。所以堆叠注入能不能打成功完全取决于后端环境。和堆叠注入经常一起出现的还有一个词万能密码。万能密码本质上是登录场景下的逻辑绕过利用SQL条件判断的特性让验证语句恒真。最常见的payload是admin OR 11 -- OR 11#登录框SQL通常是SELECT * FROM users WHERE username$user AND password$pass;把用户名填成admin OR 11 --SQL就变成SELECT * FROM users WHERE usernameadmin OR 11 -- AND passwordxxx;--注掉了后面的密码判断整条语句恒真登录直接通过。在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 11拼接后变成SELECT * FROM news WHERE title LIKE %1 AND 11%;如果页面表现异常说明存在搜索型注入。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/?id1 --batch # 指定数据库类型和注入技术 sqlmap -u http://127.0.0.1/sqli-labs/Less-1/?id1 --dbmsmysql --techniqueEU --batch # 测试POST参数 sqlmap -u http://127.0.0.1/sqli-labs/Less-11/ --data unameadminpasswd123 --batch # 指定CookieDVWA这类需要登录的靶场 sqlmap -u http://127.0.0.1/dvwa/vulnerabilities/sqli/?id1SubmitSubmit --cookiePHPSESSID你的会话ID --batch--technique参数对应注入分类参数含义B布尔盲注E报错注入U联合查询注入S堆叠查询注入T时间盲注Q内联查询比如--techniqueBET就是只测布尔、报错和时间盲注。如果页面没有回显但sqlmap跑不出来可以考虑提高检测深度sqlmap -u http://127.0.0.1/sqli-labs/Less-1/?id1 --level3 --risk2 --batch--level越高测试的注入点越多包括Cookie和User-Agent--risk越高使用越激进的payload。在靶场里随便测但在授权测试里要小心--risk3因为它可能触发DELETE或UPDATE类的payload造成数据被改。但我想强调的是sqlmap输出“某某参数存在注入”不一定代表你真正理解了分类。我见过很多情况sqlmap跑不出结果一查发现是闭合方式特殊或者被WAF拦了一部分payload。这种时候手工判断能力才是兜底的。用靶场把手工判断练熟了再用sqlmap加速两条腿走路才稳。5.4 实战体会判断分类的三步法最后说一个我在靶场里反复验证过的三步判断法。第一步找输入点。看请求里的参数GET参数看URLPOST参数看请求体还有Cookie和请求头。每个参数都值得单独测一遍。第二步判断闭合方式。在参数后面加单引号、双引号、加括号观察页面是否报错或行为变化。报错信息会直接暴露SQL语法结构没有报错就用AND 11/AND 12去试探。第三步确认输出通道。如果数据直接显示在页面尝试联合注入如果只有报错尝试报错注入如果内容不显示但页面有差异走布尔盲注如果页面一模一样只能看时间差走时间盲注。这个顺序在DVWA、SQLi-Labs、Pikachu里都适用。每次遇到一个新靶场我都先按这三步走一遍很少再出现“不知道用什么payload”的情况。至于常见的翻车点我大概总结了几个不看源码直接猜类型、在无回显页面硬打联合注入、忽略了POST参数、在需要Cookie的靶场里不带Cookie直接测、--没理解成空格导致注释失效。这些都是可以靠反复练习避开的。把SQLi-Labs从Less-1打到Less-10再回到DVWA把所有模块重测一遍整个分类思路基本就定型了。