SQLi-Labs Less1-20通关指南:从联合查询到盲注的完整注入链路 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根目录改一下数据库配置文件就能用另一种是手动配ApachePHPMySQL适合想顺便练一下环境配置的人。我建议新手直接用集成环境省下来的时间花在理解注入逻辑上更划算。数据库配置文件一般在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语句不需要引号闭合字符型注入参数被引号包裹需要先闭合引号再注入。判断方法很简单输入?id1正常输入?id1报错输入?id1 --又正常基本就是字符型。联合查询的条件使用UNION注入时前后两个SELECT语句的列数必须一致。所以第一步永远是判断当前查询有几列用ORDER BY或者UNION SELECT 1,2,3...逐个试。回显位的重要性联合查询注入的核心是找到页面上哪些位置会显示查询结果这些位置就是回显位。只有找到回显位才能把想要的数据替换上去显示出来。提示练习时养成随手记录的习惯每关的注入类型、列数、回显位、最终Payload都记下来。二十关刷完回头看这份记录比任何教程都管用。3. Less1-4联合查询注入的入门四关3.1 Less1字符型注入的标准流程Less1是最基础的一关但它的流程最完整值得慢慢做一遍。输入?id1页面显示正常用户信息。输入?id1出现SQL语法报错说明参数被单引号包裹是字符型注入。接下来判断列数。用?id1 ORDER BY 3 --正常?id1 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_schemadatabase()查列名类似最后查具体数据。3.2 Less2数字型注入的差异Less2和Less1的Payload几乎一样唯一的区别是参数没有被引号包裹。输入?id1报错但输入?id1 --依然报错说明引号不是闭合用的参数本身就是数字型。这时候Payload去掉单引号即可?id-1 UNION SELECT 1,2,3 --。很多人做Less2的时候习惯性带上单引号结果一直报错以为是别的问题。判断注入类型这一步不能省字符型和数字型的Payload差一个引号但就是这一个引号决定成败。3.3 Less3和Less4括号与变形闭合Less3的参数被包裹在()这样的结构里输入?id1报错需要构造?id1) --来闭合。Less4则是双引号加括号()闭合方式变成?id1) --。这两关的意义在于告诉你闭合方式不只有单引号一种实际场景中可能是括号、双引号、甚至更复杂的组合。判断方法还是靠报错信息报错会告诉你哪里语法有问题根据报错反推闭合结构。关卡闭合方式典型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其实有报错信息只是需要触发它。用?id1 and extractvalue(1,concat(0x7e,database())) --页面会返回XPATH报错报错内容里就包含了数据库名。extractvalue和updatexml是两个经典的报错注入函数原理是构造非法的XPATH路径让MySQL在报错信息中带出查询结果。concat(0x7e,...)里的0x7e是波浪号作用是分隔符方便在报错信息中定位数据。报错注入适合页面有报错回显但无数据回显的场景Payload构造比盲注快得多。4.2 Less6双引号版的报错注入Less6和Less5逻辑一样只是闭合方式变成双引号。Payload改成?id1 and extractvalue(1,concat(0x7e,database())) --即可。这两关放在一起练目的是让你形成条件反射遇到无回显但有报错的场景先试报错注入。报错注入能出数据就优先用出不来再考虑盲注。盲注是最后的手段因为效率低。4.3 报错注入的常见函数对比函数适用版本特点extractvalueMySQL 5.1报错长度有限约32字符updatexmlMySQL 5.1同样有长度限制floorrandgroup by老版本构造复杂新版本可能失效expMySQL 5.5溢出报错长度也有限报错注入的长度限制是个坑。如果查询结果超过32个字符需要用substr或limit分段读取。我一般用limit 0,1逐行读配合substr截取虽然麻烦但稳定。5. Less7-10文件操作与时间盲注5.1 Less7outfile写入的利用Less7的闭合方式是)) --但它的特殊之处在于可以利用into outfile写文件。Payload类似?id1)) 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结构是?id1 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-16POST注入与HTTP头注入6.1 Less11-12POST表单注入从Less11开始注入点从GET参数变成了POST表单。用Burp Suite抓包或者浏览器开发者工具看请求体发现uname和passwd两个参数。注入点在uname上闭合方式是单引号。POST注入和GET注入的Payload构造逻辑完全一样区别只是传参方式。用Burp的Repeater模块改包测试最方便或者用curlcurl -X POST http://localhost/sqli-labs/Less-11/ \ -d unameadmin and extractvalue(1,concat(0x7e,database())) --passwd1submitSubmitLess12是括号加双引号的闭合Payload相应调整。6.2 Less13-14无回显的POST注入Less13和Less14是POST场景下的盲注页面只返回登录成功或失败。Less13用布尔盲注Less14用时间盲注。逻辑和Less8、Less9一样只是传参方式变了。这两关的难点在于判断注入是否生效。因为POST请求不像GET那样直接在URL里改需要抓包改包。我建议用Burp的Intruder或者写Python脚本手工在浏览器里改POST数据太费劲。6.3 Less15-16HTTP头注入的引入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-19User-Agent和Referer注入Less18的注入点在User-AgentLess19在Referer。这两关的POST表单本身没有注入但后端把User-Agent和Referer插入了数据库的日志表导致注入。判断方法在User-Agent里加单引号如果页面报错或行为异常说明存在注入。Payload构造和普通注入一样只是注入点藏在HTTP头里。# Less18 User-Agent注入 curl -H User-Agent: Mozilla and extractvalue(1,concat(0x7e,database())) -- \ -d unameadminpasswd1submitSubmit \ http://localhost/sqli-labs/Less-18/7.3 Less20Cookie注入的实战意义Less20的注入点在Cookie的uname字段。登录成功后后端会把uname写入Cookie下次请求时从Cookie读取并拼进SQL查询。如果在Cookie里加单引号就能触发注入。Cookie注入在实际场景中很常见因为很多应用用Cookie做身份识别如果直接拼接Cookie值到SQL语句就存在注入风险。这关的Payload和普通注入一样只是注入点变成了Cookie。curl -b unameadmin and extractvalue(1,concat(0x7e,database())) -- \ http://localhost/sqli-labs/Less-20/关卡注入点注入类型闭合方式Less17POST uname报错/盲注单引号Less18User-Agent报错注入单引号Less19Referer报错注入单引号Less20Cookie 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细节效率会高很多。