1. 为什么我建议你从Less1到Less20完整刷一遍
SQL注入是Web安全领域最经典的漏洞类型之一,而SQLi-Labs这个开源靶场几乎是每个安全从业者绕不开的练习场。Less1到Less20这二十关,覆盖了联合查询注入、报错注入、布尔盲注、时间盲注、堆叠注入、HTTP头注入等多种核心手法,基本把MySQL环境下SQL注入的常见形态都串了一遍。很多人刷靶场喜欢跳着做,觉得前面几关太简单,直接跳到盲注或者堆叠。我试过这种玩法,后来发现基础不牢,到了后面连判断注入类型都要卡半天。Less1到Less20的价值不在于难度,而在于它帮你建立一套完整的判断链路:看到参数先测什么、根据回显差异怎么缩小范围、不同注入类型对应什么Payload结构。这套链路一旦形成肌肉记忆,后面遇到真实场景才不会慌。
这篇文章面向的是刚接触SQL注入、想系统练习但不知道从哪下手的人,也适合刷过几关但思路比较零散、想重新梳理一遍的朋友。我会按关卡类型分组拆解,把每关的核心判断逻辑、Payload构造思路、实操中容易踩的坑都讲清楚。不会只丢一串Payload让你复制,而是解释为什么这么构造、参数怎么算出来的。文章里涉及的所有操作都在本地搭建的靶场环境中进行,目的是理解漏洞原理和防御思路。
2. 环境搭建与前期准备
2.1 靶场部署的两种常见方式
SQLi-Labs的部署方式主要有两种:一种是集成环境直接跑,比如用PHPStudy或者XAMPP这类集成包,把源码丢进Web根目录,改一下数据库配置文件就能用;另一种是手动配Apache+PHP+MySQL,适合想顺便练一下环境配置的人。我建议新手直接用集成环境,省下来的时间花在理解注入逻辑上更划算。
数据库配置文件一般在sql-connections/db-creds.inc这个路径下,里面定义了数据库用户名、密码和库名。默认情况下用户是root,密码为空或者root,具体看你本地MySQL的设置。如果连接报错,先检查这个文件,再看MySQL服务有没有正常启动。
# 导入靶场数据库结构 mysql -u root -p < sqli-labs.sql导入完成后访问http://localhost/sqli-labs/,能看到关卡列表就说明环境通了。这里有个细节:PHP版本建议用5.x或者7.x的早期版本,太新的PHP版本对某些函数的处理有变化,可能导致部分关卡的报错信息显示不出来,影响练习效果。
2.2 练习前必须明确的几个概念
在开始之前,有几个概念必须先理清楚,不然后面会一直混淆。
字符型注入和数字型注入的区别:数字型注入参数直接拼进SQL语句,不需要引号闭合;字符型注入参数被引号包裹,需要先闭合引号再注入。判断方法很简单,输入?id=1正常,输入?id=1'报错,输入?id=1' --+又正常,基本就是字符型。
联合查询的条件:使用UNION注入时,前后两个SELECT语句的列数必须一致。所以第一步永远是判断当前查询有几列,用ORDER BY或者UNION SELECT 1,2,3...逐个试。
回显位的重要性:联合查询注入的核心是找到页面上哪些位置会显示查询结果,这些位置就是回显位。只有找到回显位,才能把想要的数据替换上去显示出来。
提示:练习时养成随手记录的习惯,每关的注入类型、列数、回显位、最终Payload都记下来。二十关刷完回头看这份记录,比任何教程都管用。
3. Less1-4:联合查询注入的入门四关
3.1 Less1:字符型注入的标准流程
Less1是最基础的一关,但它的流程最完整,值得慢慢做一遍。输入?id=1,页面显示正常用户信息。输入?id=1',出现SQL语法报错,说明参数被单引号包裹,是字符型注入。
接下来判断列数。用?id=1' ORDER BY 3 --+,正常;?id=1' ORDER BY 4 --+,报错。说明当前查询是3列。这里--+的作用是注释掉后面的引号和限制条件,--是SQL注释符,+在URL中代表空格,确保注释生效。
然后找回显位:?id=-1' UNION SELECT 1,2,3 --+。注意这里id用-1而不是1,目的是让前面的查询返回空结果,这样页面显示的就是我们UNION后面的内容。页面上会显示2和3的位置,这两个就是回显位。
最后拿数据:?id=-1' UNION SELECT 1,database(),version() --+,把回显位替换成想要的函数或字段。查表名用group_concat(table_name) from information_schema.tables where table_schema=database(),查列名类似,最后查具体数据。
3.2 Less2:数字型注入的差异
Less2和Less1的Payload几乎一样,唯一的区别是参数没有被引号包裹。输入?id=1'报错,但输入?id=1' --+依然报错,说明引号不是闭合用的,参数本身就是数字型。这时候Payload去掉单引号即可:?id=-1 UNION SELECT 1,2,3 --+。
很多人做Less2的时候习惯性带上单引号,结果一直报错,以为是别的问题。判断注入类型这一步不能省,字符型和数字型的Payload差一个引号,但就是这一个引号决定成败。
3.3 Less3和Less4:括号与变形闭合
Less3的参数被包裹在('')这样的结构里,输入?id=1'报错,需要构造?id=1') --+来闭合。Less4则是双引号加括号(""),闭合方式变成?id=1") --+。
这两关的意义在于告诉你:闭合方式不只有单引号一种,实际场景中可能是括号、双引号、甚至更复杂的组合。判断方法还是靠报错信息,报错会告诉你哪里语法有问题,根据报错反推闭合结构。
| 关卡 | 闭合方式 | 典型Payload前缀 |
|---|---|---|
| Less1 | 单引号 | ?id=-1' UNION SELECT ... |
| Less2 | 无引号 | ?id=-1 UNION SELECT ... |
| Less3 | 单引号加括号 | ?id=-1') UNION SELECT ... |
| Less4 | 双引号加括号 | ?id=-1") UNION SELECT ... |
注意:每关的列数可能不同,不要死记Payload,每次都要重新用ORDER BY判断列数。我见过有人拿着Less1的3列Payload去做Less3,结果列数不对一直报错。
4. Less5-6:报错注入与盲注的分水岭
4.1 Less5:没有回显位怎么办
Less5输入正常id页面显示"You are in...",输入错误id显示同样的话,没有数据回显,也没有明显报错。这时候联合查询就失效了,因为没有回显位可以放数据。
但Less5其实有报错信息,只是需要触发它。用?id=1' and extractvalue(1,concat(0x7e,database())) --+,页面会返回XPATH报错,报错内容里就包含了数据库名。extractvalue和updatexml是两个经典的报错注入函数,原理是构造非法的XPATH路径,让MySQL在报错信息中带出查询结果。
concat(0x7e,...)里的0x7e是波浪号,作用是分隔符,方便在报错信息中定位数据。报错注入适合页面有报错回显但无数据回显的场景,Payload构造比盲注快得多。
4.2 Less6:双引号版的报错注入
Less6和Less5逻辑一样,只是闭合方式变成双引号。Payload改成?id=1" and extractvalue(1,concat(0x7e,database())) --+即可。
这两关放在一起练,目的是让你形成条件反射:遇到无回显但有报错的场景,先试报错注入。报错注入能出数据就优先用,出不来再考虑盲注。盲注是最后的手段,因为效率低。
4.3 报错注入的常见函数对比
| 函数 | 适用版本 | 特点 |
|---|---|---|
| extractvalue | MySQL 5.1+ | 报错长度有限,约32字符 |
| updatexml | MySQL 5.1+ | 同样有长度限制 |
| floor+rand+group by | 老版本 | 构造复杂,新版本可能失效 |
| exp | MySQL 5.5+ | 溢出报错,长度也有限 |
报错注入的长度限制是个坑。如果查询结果超过32个字符,需要用substr或limit分段读取。我一般用limit 0,1逐行读,配合substr截取,虽然麻烦但稳定。
5. Less7-10:文件操作与时间盲注
5.1 Less7:outfile写入的利用
Less7的闭合方式是)) --+,但它的特殊之处在于可以利用into outfile写文件。Payload类似?id=1')) UNION SELECT 1,'<?php ... ?>',3 into outfile '/var/www/html/shell.php' --+。
这关的意义是展示SQL注入的延伸危害:不仅能读数据,还能写文件。当然实际环境中能否写入取决于数据库用户的文件权限和secure_file_priv配置。练习时如果写入失败,先检查MySQL的secure_file_priv设置,这个参数限制了可以写文件的目录。
-- 查看当前配置 show variables like 'secure_file_priv';5.2 Less8-10:布尔盲注与时间盲注
Less8是布尔盲注,页面只有两种状态:有结果和无结果。通过构造真/假条件,观察页面变化来逐字符推断数据。Payload结构是?id=1' and substr(database(),1,1)='s' --+,如果页面正常说明首字母是s,否则不是。
布尔盲注的效率取决于字符集大小和猜解策略。我一般先用二分法确定字符范围,再逐个字符猜。手工做太慢,通常写脚本跑。Python的requests库配合二分查找,一个数据库名几分钟能出来。
Less9是时间盲注,页面无论真假都返回一样的内容,只能靠响应时间判断。Payload用if(condition,sleep(3),0),条件为真时延迟3秒。时间盲注最慢,但适用场景最广,因为只要有SQL执行就能用。
Less10是双引号版的时间盲注,逻辑一样,闭合方式不同。
提示:时间盲注的sleep时间不要设太短,网络波动容易误判。我一般设3秒,连续测两次结果一致才确认。另外可以用
benchmark函数替代sleep,但benchmark消耗CPU,不如sleep稳定。
6. Less11-16:POST注入与HTTP头注入
6.1 Less11-12:POST表单注入
从Less11开始,注入点从GET参数变成了POST表单。用Burp Suite抓包或者浏览器开发者工具看请求体,发现uname和passwd两个参数。注入点在uname上,闭合方式是单引号。
POST注入和GET注入的Payload构造逻辑完全一样,区别只是传参方式。用Burp的Repeater模块改包测试最方便,或者用curl:
curl -X POST http://localhost/sqli-labs/Less-11/ \ -d "uname=admin' and extractvalue(1,concat(0x7e,database())) --+&passwd=1&submit=Submit"Less12是括号加双引号的闭合,Payload相应调整。
6.2 Less13-14:无回显的POST注入
Less13和Less14是POST场景下的盲注,页面只返回登录成功或失败。Less13用布尔盲注,Less14用时间盲注。逻辑和Less8、Less9一样,只是传参方式变了。
这两关的难点在于判断注入是否生效。因为POST请求不像GET那样直接在URL里改,需要抓包改包。我建议用Burp的Intruder或者写Python脚本,手工在浏览器里改POST数据太费劲。
6.3 Less15-16:HTTP头注入的引入
Less15和Less16虽然还是POST表单,但它们的特殊之处在于可以尝试HTTP头注入。比如User-Agent、Referer、X-Forwarded-For这些头,如果后端把它们拼进SQL语句,就可能存在注入。
判断方法:在User-Agent里加单引号,看是否报错。如果报错,说明User-Agent被带入了SQL查询。Payload构造和普通注入一样,只是注入点变成了HTTP头。
curl -H "User-Agent: Mozilla' and extractvalue(1,concat(0x7e,database())) --+" \ http://localhost/sqli-labs/Less-15/注意:HTTP头注入在实际场景中很常见,因为很多应用会记录User-Agent或Referer到数据库。但靶场里这两关主要还是POST注入,HTTP头注入是额外练习。不要混淆主次。
7. Less17-20:密码修改、头部注入与Cookie注入
7.1 Less17:密码修改场景的注入
Less17是一个密码修改功能,注入点在uname参数上。但它的特殊之处在于,修改密码前会先查询原密码,如果原密码不对就不会执行更新。所以注入需要绕过这个检查。
Payload思路是用and构造条件,让查询返回真,同时利用报错或盲注带出数据。这关的闭合方式是单引号,但查询语句里可能有and password='...'这样的结构,需要注释掉后面的部分。
7.2 Less18-19:User-Agent和Referer注入
Less18的注入点在User-Agent,Less19在Referer。这两关的POST表单本身没有注入,但后端把User-Agent和Referer插入了数据库的日志表,导致注入。
判断方法:在User-Agent里加单引号,如果页面报错或行为异常,说明存在注入。Payload构造和普通注入一样,只是注入点藏在HTTP头里。
# Less18 User-Agent注入 curl -H "User-Agent: Mozilla' and extractvalue(1,concat(0x7e,database())) --+" \ -d "uname=admin&passwd=1&submit=Submit" \ http://localhost/sqli-labs/Less-18/7.3 Less20:Cookie注入的实战意义
Less20的注入点在Cookie的uname字段。登录成功后,后端会把uname写入Cookie,下次请求时从Cookie读取并拼进SQL查询。如果在Cookie里加单引号,就能触发注入。
Cookie注入在实际场景中很常见,因为很多应用用Cookie做身份识别,如果直接拼接Cookie值到SQL语句,就存在注入风险。这关的Payload和普通注入一样,只是注入点变成了Cookie。
curl -b "uname=admin' and extractvalue(1,concat(0x7e,database())) --+" \ http://localhost/sqli-labs/Less-20/| 关卡 | 注入点 | 注入类型 | 闭合方式 |
|---|---|---|---|
| Less17 | POST uname | 报错/盲注 | 单引号 |
| Less18 | User-Agent | 报错注入 | 单引号 |
| Less19 | Referer | 报错注入 | 单引号 |
| Less20 | Cookie uname | 报错注入 | 单引号 |
8. 常见问题与排查技巧实录
8.1 报错信息不显示怎么办
有时候输入单引号页面没有报错,只是显示空白或跳转。可能原因有几个:一是PHP的display_errors关闭了,需要在php.ini里打开;二是代码里用了@抑制错误;三是查询本身没有报错,只是返回空结果。
排查顺序:先看php.ini的display_errors和error_reporting设置,确保错误能显示。如果还是不行,尝试用时间盲注的Payload,通过响应时间判断注入是否生效。
8.2 联合查询列数判断不准
用ORDER BY判断列数时,有时候报错信息不明确,不知道是列数超了还是语法错了。这时候可以用UNION SELECT 1,2,3...逐个增加数字,直到页面正常显示。两种方法结合用,互相验证。
另外注意,ORDER BY后面跟数字表示按第几列排序,如果数字超过列数会报错。但有些版本的MySQL对ORDER BY的错误处理不一样,可能不报错只是排序无效。所以UNION SELECT更可靠。
8.3 盲注脚本跑不出数据
盲注脚本常见问题:一是URL编码没处理好,Payload里的特殊字符被转义;二是sleep时间太短,网络延迟导致误判;三是字符集判断错误,比如大小写没区分。
我的做法是先用单个字符测试,确认脚本能正确判断真假,再跑完整流程。sleep时间设3秒,连续两次结果一致才记录。字符集先用substr配合ascii函数转成数字比较,避免大小写和编码问题。
8.4 文件写入失败的原因
into outfile写入失败通常是因为secure_file_priv限制。这个参数如果是NULL,表示禁止文件写入;如果是空字符串,表示不限制目录;如果指定了路径,只能写入该路径。
show variables like 'secure_file_priv';另外,数据库用户需要有FILE权限,目标目录需要MySQL进程有写权限。这几个条件缺一不可。
8.5 HTTP头注入不生效
HTTP头注入不生效的原因可能是:后端没有把该头拼进SQL,或者头被过滤了。判断方法是在头里加单引号,看是否报错。如果不报错,换个头试试,比如从User-Agent换到Referer或X-Forwarded-For。
有些应用会对HTTP头做长度限制或字符过滤,这时候需要编码绕过。比如把单引号换成URL编码%27,或者用双写、大小写混合等方式绕过简单过滤。
9. 从靶场到实战的思维转换
刷完Less1到Less20,你手里应该有一套完整的判断流程:先测闭合方式,再判断注入类型,然后根据回显情况选择联合查询、报错注入还是盲注。这套流程在靶场里是线性的,但实战中往往更复杂。
实战中遇到的注入点可能没有明显的报错,回显也不规律,甚至参数经过多层编码。这时候需要更耐心地测试,用不同的Payload组合去探测。另外,实战中要注意注入点的上下文,比如参数是在WHERE条件里还是在ORDER BY后面,不同的位置Payload构造方式不同。
还有一个重要的思维转换:靶场里我们追求的是拿到数据,实战中更重要的是理解漏洞的成因和影响范围。一个SQL注入能读到什么数据、能不能写文件、能不能执行命令,这些决定了漏洞的严重程度。刷靶场的时候多问自己几个为什么,比单纯追求通关更有价值。
我个人在实际操作中的体会是,Less1到Less20这二十关,真正需要反复练的是Less1、Less5、Less8、Less9这四关,它们分别代表了联合查询、报错注入、布尔盲注、时间盲注四种核心手法。把这四关的Payload构造逻辑吃透,后面的关卡都是在这四种手法上做变形。另外,每关做完后试着不看笔记重新构造一遍Payload,能独立构造出来才算真正掌握。最后分享一个小技巧:把每关的Payload按类型整理成一个速查表,遇到新场景时先对照速查表判断类型,再调整Payload细节,效率会高很多。