SQLiLabs 这个靶场我在本地环境里反复搭过不下十次,每次带新人入门 SQL 注入都会用它。它跟 DVWA、Pikachu 这类综合靶场最大的区别,就是"专一"——整个靶场就是围绕 SQL 注入这一个主题设计的,从最初级的数字型、字符型注入,到后面难度很大的盲注、堆叠注入、二次注入,一共 65 个关卡,几乎把 SQL 注入在 Web 场景下能出现的形态都覆盖了。本文就以这套靶场为线索,把搭建过程和背后最核心的 SQL 语句原理一起讲清楚,目标读者是想系统学习 SQL 注入原理、或者准备渗透测试入门的新手,看完之后你不仅能自己把靶场搭起来,还能真正看懂每一关的 SQL 语句在干什么。
提示:本文所有内容仅限于在本地或授权环境中进行安全学习与防御研究,请勿对未授权目标实施任何测试。
1. 为什么选 SQLiLabs:靶场选型与整体思路
1.1 靶场在 SQL 注入学习中的角色
很多人学 SQL 注入直接去网上找实战案例,这个路径效率其实很低。实战目标不稳定、数据不可控、权限边界不清晰,最关键的是——你看不到服务端到底在执行什么语句,踩了坑也不知道错在哪。靶场的价值就在于它把服务端代码和数据库状态固定下来,让你能反复验证每一次请求背后的 SQL 逻辑。SQLiLabs 的每个关卡都有对应的 PHP 源码,你提交的参数进去,代码层面发生了什么是一目了然的。这对建立"注入过程的心理模型"特别重要。
1.2 SQLiLabs 相比其他靶场的优势
DVWA 和 Pikachu 都是好靶场,但它们属于"功能多而杂"的类型,SQL 注入只是其中的一个模块。SQLiLabs 的定位完全不同,它用 65 个关卡把 SQL 注入的各个分支拆得非常细,每三四个关卡就换一种过滤方式或闭合方式,练完一遍之后,你在真实场景里碰到各种奇怪的参数结构,脑子里会自动浮现对应的关卡样式。这个"对应感"是刷综合靶场练不出来的。
另外 SQLiLabs 的代码注释写得很清晰,每一关的 PHP 源码里都标注了当前关卡的注入点类型、闭合方式、过滤规则。我强烈建议读者在通关后一定要去看源码,这才是靶场的精华所在。比如 Less-21 那关是 base64 编码后的 Cookie 注入,如果你不看源码,光靠盲打很难理解为什么参数加了 base64 编码,看了代码一目了然。
1.3 环境整体架构:经典 LAMP 组合
SQLiLabs 是基于 PHP + MySQL 架构的,跑在 Apache 下最省心。搭建方式有两种主流选择:一种是直接用 phpStudy、XAMPP 这类集成环境,把源码丢进 web 目录就行;另一种是用 Docker 容器化部署,优势是干净、可重复、不污染宿主机环境。
注意:SQLiLabs 的很多关卡依赖 MySQL 的错误回显,PHP 版本建议使用 5.x 或者 7.x 的低版本。PHP 8 以上的版本对部分旧语法兼容性变差,有些关卡会直接报 Fatal Error,这一点后面会展开说。
我这里两种方式都会讲,先讲 Docker 的快速方案,再讲传统环境的详细步骤,你自己按需求选。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. SQLiLabs 搭建的完整过程
2.1 Docker 方式快速搭建
Docker 方式是最省事的,适合想立刻开始刷题的人。终端环境装好 Docker 之后,直接拉取镜像:
bash复制docker pull vulnerables/web-dvwa
这里要特别说明一下,常用的 SQLiLabs 镜像有两个,一个是 vulnerables/web-dvwa(这个主要是 DVWA,但部分镜像内置了 SQLiLabs),另一个是社区维护的专用镜像。更保险的做法是直接拉取 GitHub 上的源码,再用 Docker 配置 PHP 环境:
bash复制git clone https://github.com/Audi-1/sqli-labs.git
cd sqli-labs
docker run -d --name sqli-labs -p 8080:80 -v "$PWD":/var/www/html php:7.4-apache
然后进入容器安装 PHP 的 MySQL 扩展:
bash复制docker exec -it sqli-labs bash
docker-php-ext-install mysqli
docker-php-ext-install pdo pdo_mysql
a2enmod rewrite
docker restart sqli-labs
再用 docker 起一个 MySQL 容器:
bash复制docker run -d --name sqli-mysql -e MYSQL_ROOT_PASSWORD=root -e MYSQL_DATABASE=security mysql:5.7
然后用 --link 把两个容器连起来(或者用 docker-compose 更优雅),访问 http://localhost:8080,按提示初始化数据库就可以了。
2.2 传统方式搭建完整步骤
如果你用的是 Windows,建议直接装 phpStudy 或者 XAMPP,版本选择上注意 PHP 最好用 7.x 以内的版本。Linux 上则直接装 LAMP 环境。
第一步:准备 Web 环境。以 Ubuntu 为例:
bash复制sudo apt update
sudo apt install apache2 mysql-server php7.4 libapache2-mod-php7.4 php7.4-mysql
安装完成后把 SQLiLabs 源码复制到 /var/www/html/sqli-labs 目录下,给目录可写权限:
bash复制sudo chmod -R 777 /var/www/html/sqli-labs
第二步:创建数据库。SQLiLabs 自带了初始化 SQL 脚本,位于 sql-connections/sql-lab-setup.php。也有更直接的方式,就是在 MySQL 里手动执行它附带的 sql-lab.sql 文件:
bash复制mysql -u root -p < /var/www/html/sqli-labs/sql-lab.sql
该脚本会创建 security 数据库以及 users、emails、referers 等多个数据表,关卡里的注入点基本都是围绕这些表设计的。
第三步:修改数据库连接配置。打开 sql-connections/db-creds.inc 文件,根据你的 MySQL 配置修改用户名和密码:
php复制$dbuser ='root';
$dbpass ='root';
$dbname ='security';
修改完成后,浏览器访问 http://localhost/sqli-labs/,点击 "Setup/reset Database for labs" 链接初始化数据库,然后就可以开始刷关了。
实操心得:这个文件的名字很容易被忽略,很多人配完了数据库密码改的是
config.inc.php,结果连不上数据库,搞半天发现错了文件。SQLiLabs 的数据库连接配置就在sql-connections/目录下,不要找错地方。
2.3 目录结构与关卡机制解读
搭建完成后你会看到一个以 Less 开头的关卡列表,从 Less-1 一直到 Less-65。每个关卡在源码目录里对应一个以 Less 开头的 PHP 文件,比如 Less-1/index.php。每关的源码都很短,核心就是一段接收参数的代码和一条拼接 SQL 语句的代码。
这里有一个值得收藏的目录认知方式——SQLiLabs 的 65 个关卡不是乱排的,而是按注入类型分了组的:
| 关卡范围 | 注入类型 | 核心特点 |
|---|---|---|
| Less-1 ~ Less-4 | GET 型基础注入 | 数字型/字符型闭合差异 |
| Less-5 ~ Less-6 | 双查询报错注入 | 无回显但有报错信息 |
| Less-7 ~ Less-10 | 布尔盲注/时间盲注 | 无报错信息,靠页面差异判断 |
| Less-11 ~ Less-16 | POST 型注入 | 登录表单场景 |
| Less-17 ~ Less-22 | 更新/绕过类注入 | 结合更新语句和转义 |
| Less-23 及以后 | 过滤与绕过 | 注释符过滤、空格过滤、关键字过滤 |
| 靠后关卡 | 堆叠注入、二次注入、宽字节 | 进阶技巧 |
这个分组意识比单纯一个个刷重要得多,因为同一个组里的关卡,核心思路是相似的,变的是绕过姿势。练到后面你会发现,所谓"过关"不是背 payload,而是理解后台语句的逻辑结构,然后针对结构去调整输入内容。
3. 核心 SQL 语句与注入原理解读
3.1 从一条正常查询开始理解参数位置
SQL 注入的本质是"拼字符串"。你传进 URL 的参数最终会以字符串形式被拼接进 SQL 语句里执行。SQLiLabs 第一关的代码就是最经典的例子:
php复制$id=$_GET['id'];
$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";
如果你访问 ?id=1,后台执行的语句是:
sql复制SELECT * FROM users WHERE id='1' LIMIT 0,1;
这本来没什么问题,页面会正常显示 id=1 的用户名和密码。问题出在:id 这个参数没有经过任何过滤,直接进了 SQL 语句。如果你传的不是数字,而是一段精心构造的字符串,比如:?id=1' AND '1'='1,那实际执行的语句就变成了:
sql复制SELECT * FROM users WHERE id='1' AND '1'='1' LIMIT 0,1;
注意看,这里 '1'='1' 是一个恒真条件,所以整条 WHERE 语句的结果是 True,查询正常执行,返回的还是 id=1 的数据。同理,如果你传 ?id=1' AND '1'='2,那么恒假条件就会让查询返回空,页面就不显示数据。这就是布尔盲注最原始的原理。
理解这个例子要注意一个点:注入的不是"一段完整独立的 SQL",而是"能够改变原 SQL 语义的片段"。你的输入是拼在大 SQL 里的一部分,你要做的是通过引号、注释符和逻辑运算符,让拼接后的整体语句按照你想要的方向执行。
3.2 猜解列数与 ORDER BY 原理
在联合注入中,第一个关键步骤就是确认原查询的字段数量。SQLiLabs 里最常用的方式是 ORDER BY 语句:
code复制?id=1' ORDER BY 1-- -
?id=1' ORDER BY 2-- -
?id=1' ORDER BY 3-- -
如果 ORDER BY 5 没有报错,说明原表至少有 5 列;如果 ORDER BY 6 报错,说明列数就是 5。原理非常简单:ORDER BY 后面跟的数字是列序号,MySQL 在排序时会校验这个序号是否超过了列总数,一旦超出就会报错。
使用 -- - 注释符来注释掉原始 SQL 语句后面所有的内容,这是万能操作。这里要注意 -- 后面必须跟一个空格再跟一个 -,因为 MySQL 的注释符要求 -- 后面至少跟一个空格/换行字符。不少新手在这里翻车,用 -- something 却忘了空格,导致注释不生效。
另外一个需要搞清楚的问题是:ORDER BY 能猜列数,但它是基于"后端只执行一条查询"的前提下成立的。在某些堆叠注入场景下,允许你同时执行多条 SQL 语句,这时候 ORDER BY 猜列数就不再是唯一手段了,可以直接用 SELECT * FROM table INTO OUTFILE 之类的语句去拿结果。但这是后续进阶的话题,新手阶段先把 ORDER BY 练扎实。
3.3 UNION 联合注入的 SQL 结构
知道列数之后,下一步就是用 UNION SELECT 来构造联合查询。核心语法是:
sql复制SELECT * FROM users WHERE id='1' UNION SELECT 1,2,3-- -' LIMIT 0,1;
这里 UNION 会把前后两个查询的结果合并成一个结果集,前提是两者列数必须一致。如果你在 ?id=1' UNION SELECT 1,2,3-- - 时页面显示了某个数字(比如回显了 2 和 3),那就说明联合查询成功,2 和 3 所在的位置就是你可以用来查数据的位置。
举个实际的例子,SQLiLabs Less-1 中,如果 ?id=-1' UNION SELECT 1,2,3-- -,页面会显示 Your Login name:2 和 Your Password:3。注意这里为什么用 id=-1?因为要确保前面的查询结果为空,UNION 的结果才不会和新查询的数据混在一起,导致你看到的回显位置不干净。这个细节非常关键,我在带人的时候发现很多人直接用 id=1,结果回显里混了第一条记录的内容,反而看不懂了。
拿到回显位置后,就能把对应位置的数字替换成你想查的数据。比如查数据库名称:
code复制?id=-1' UNION SELECT 1,database(),3-- -
或者一次查多个信息:
code复制?id=-1' UNION SELECT 1,database(),user()-- -
这里的 database() 和 user() 都是 MySQL 内置函数,分别返回当前数据库名和当前用户。SQLiLabs 关卡的数据库名是 security,用户一般是 root@localhost。这个信息可以验证你的注入姿势是否正确。
3.4 万能密码与闭合方式的本质
SQLiLabs 里关于万能密码的经典关卡是 Less-11,这是一个 POST 型登录表单注入。后台的查询语句是:
php复制$sql="SELECT username, password FROM users WHERE username='$uname' and password='$passwd' LIMIT 0,1";
登录表单如果输入用户名 admin' OR '1'='1' -- -,密码随便填,拼接出来的语句就成了:
sql复制SELECT username, password FROM users WHERE username='admin' OR '1'='1' -- -' and password='xxx' LIMIT 0,1;
由于 '1'='1' 恒真,再加上注释符把后面的密码校验语句全部注释掉,所以无论密码是什么,这条查询都会返回第一行用户的数据,从而登录成功。这就是万能密码的核心逻辑。
要理解的是:万能密码能生效,是因为后台没有对输入做任何过滤,直接把字符串拼接进了 WHERE 条件。而闭合方式——单引号、双引号、数字型不带引号——决定了你需要在输入里加多少引号才能打断原来的语法,是注入最关键的一步。SQLiLabs 前面几关看似简单,其实就是在反复训练你对闭合方式变化的敏感度。
3.5 报错注入与双查询原理
到 Less-5 和 Less-6 这种情况,页面不直接回显查询结果,只返回"You are in..."之类的提示,用 UNION 注入行不通了。这时候需要用报错注入,让 MySQL 在报错信息里把你要查的数据带出来。
SQLiLabs 最常用的报错注入方式是 updatexml 和 extractvalue:
code复制?id=1' AND extractvalue(1, concat(0x7e, database(), 0x7e))-- -
这条语句会让 MySQL 的 XML 解析函数在处理第二个参数时碰到不合法格式,从而在报错信息中暴露 concat 函数拼接的结果。报错信息里通常会有 XPATH syntax error: '~security~',其中 ~ 包裹着的就是查询结果。
报错注入的原理是利用 MySQL 在特定的函数参数校验时抛出异常,而异常信息里包含了传入的参数内容。这种手法不需要回显位置,只需要页面能显示数据库报错信息。SQLiLabs 里这种场景非常多,建议把 extractvalue 和 updatexml 两种都练熟。
4. 从 Less-1 到 Less-11:典型关卡实战拆解
4.1 Less-1:字符型注入的教科书
Less-1 是整个靶场的入门关卡,代码结构如下:
php复制$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";
注意 id 被单引号包裹,所以这是一个字符型注入,需要先闭合前面的单引号,再处理后面的内容。完整的通关链路:
第一步,探测闭合方式:输入 ?id=1',页面报错 "You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''1'' LIMIT 0,1'",这说明多了一个单引号导致语法错误。
第二步,使用注释符闭合后面的单引号:?id=1' -- -,页面恢复正常,确认闭合有效。
第三步,用 ORDER BY 猜列数:?id=1' ORDER BY 4-- - 报错,说明只有 3 列。
第四步,使用 UNION 注入拿数据:
code复制? id=-1' UNION SELECT 1,2,3-- -
可以看到回显位置在 2 和 3,再把对应位置替换成查库名、查表名的语句就行了。
这套流程是后续所有显错注入的模板,我建议读者在 Less-1 上多练几遍,直到闭着眼睛都能写出完整的探测链路。这个关卡你可以反复改参数观察页面变化,体会每一步操作对后台 SQL 语句造成的影响。
4.2 Less-2:数字型注入的参数处理差异
Less-2 的核心代码:
php复制$sql="SELECT * FROM users WHERE id=$id LIMIT 0,1";
区别在于 id 没有用引号包裹,是数字型注入。你去输入 ?id=1',报错信息会比 Less-1 更直接:'', LIMIT 0,1' 之类,因为多余的引号直接破坏了数字上下文。
数字型注入的 payload 要简单一些:?id=-1 UNION SELECT 1,2,3,不需要闭合单引号,也不需要注释符。但实际开发中,数字型参数在外面套一层 intval() 基本就堵死了,反而不如字符型场景常见。
Less-2 给我们的启发是:判断注入类型,直接看报错信息就知道是数字型还是字符型。数字型报错通常会在语法错误提示里显示你输入的引号多了一个;字符型则通常会在报错中看到值后面有引号闭合的痕迹。
4.3 Less-5/6:无回显场景与报错注入
Less-5 的查询语句同样是字符型,但它只返回 "You are in ...",不返回任何具体数据。所以 UNION 注入无法直接看到数据,必须通过报错或者盲注来获取数据。
我最常用的方式是 extractvalue 报错注入:
code复制?id=1' AND extractvalue(1, concat(0x7e, (SELECT database()), 0x7e))-- -
如果页面返回 XPATH syntax error: '~security~',就说明拿到了当前数据库名。这里 0x7e 是波浪号 ~ 的十六进制表示,加它的目的只是让 XML 解析器识别出非法的 XPATH 路径格式,从而主动报错。后续把 (SELECT database()) 换成任意子查询即可。
Less-6 和 Less-5 唯一的区别是闭合方式变成了双引号:$id 被双引号包裹。把 payload 里的单引号换成双引号即可。这两个关卡放在一起的目的就是训练你读报错信息判断闭合方式的能力。
实操心得:报错注入的输出可能包含很多转义符,有时候数据库版本不同报错信息的格式会略有差异。建议在 SQLiLabs 里就用和真实环境接近的 MySQL 版本,不要用 MariaDB 跑靶场,MariaDB 的报错信息格式和 MySQL 有出入,少部分
extractvalue的 payload 表现不一致,容易产生误导。
4.4 Less-11:POST 注入与登录绕过
Less-11 是登录表单,后台代码用 POST 接收 uname 和 passwd,SQL 语句是:
php复制$sql="SELECT username, password FROM users WHERE username='$uname' and password='$passwd' LIMIT 0,1";
这里的注入点在用户名或者密码字段。使用万能密码:
code复制用户名: admin' OR '1'='1' -- -
密码: 任意
登录成功后页面会显示用户名和密码在页面里,因为它会把查询结果渲染上去,所以这实际上是一个 POST 型的显错注入。也可以用联合注入:
code复制用户名: admin' UNION SELECT 1,2-- -
密码: 任意
Less-11 的实战价值很高,因为真实 Web 应用的登录接口是重灾区。而且 POST 注入不能用 URL 直接改参数,需要用 Burp Suite 抓包改包,或者用浏览器的开发者工具手动改请求体。
5. 实操中的常见问题排查
5.1 环境搭建阶段的问题
问题一:数据库连接失败。表现为初始化页面打不开或者点击 Setup 按钮报 "Incorrect connection"。
排查思路:
- 检查
sql-connections/db-creds.inc里的账号密码是否正确,而不是检查config.inc.php。 - 在终端里用同样的账号密码试一下
mysql -u root -p是否能连接。 - 检查 MySQL 是否只绑定了
127.0.0.1,如果 Apache 和 MySQL 在同一台机器上一般没问题,但如果是容器间互联,需要设置好网络。
问题二:访问页面出现 500 错误。最常见原因是 PHP 版本太高,代码里的旧语法不兼容。SQLiLabs 比较古老的源码在 PHP 7.4 以下兼容性最好,PHP 8 直接就会报 mysql_* 函数未定义的错。解决方案是切换到 PHP 7.x 或者给容器打补丁。
5.2 关卡执行中的问题
问题三:页面有报错,但 UNION 注入不生效。
先检查列数猜对没有。如果 ORDER BY 猜的列数和实际不回显的数字位置不一致,UNION 就会失败。另外确认前查询是否被置空——回显位置不够干净的话,即使 UNION 成功,也会被前面的数据污染。使用 id=-1 或 id=0 让前查询返回空集是标准姿势。
问题四:注释符不生效。
MySQL 的 -- 注释符必须跟空格。很多人在 payload 里写 -- 不加空格,结果后面的单引号没有注释掉,SQL 依然报错。我个人的习惯是统一用 -- -(两个短横加空格再加一个短横),这样既满足 MySQL 注释要求,也能保证在某些后端把空格过滤掉的情况下不失效。另一常用的是 # 注释符,但 URL 里 # 需要编码为 %23,不然会被浏览器当锚点吃掉。
问题五:报错注入不报错。
确认后台是否开启了错误显示。SQLiLabs 默认是开启的,但如果你后来是在其他环境部署了别人二改的版本,有可能错误显示被关闭了。在 PHP 源码里检查 mysqli_report 或者 error_reporting 的设置。如果错误信息被隐藏,就要考虑转盲注。
5.3 用 SQL 语句直接调试的高效方法
在刷 SQLiLabs 的过程中,我经常在 MySQL 终端里手动执行拼接好的 SQL 语句来验证逻辑,这会极大帮助你理解注入语句的构造过程。比如在 Less-2 的数字型注入中,我会手动执行:
sql复制SELECT * FROM users WHERE id=1 UNION SELECT 1,2,3;
然后在终端里看结果。如果你能先在 MySQL 里把语句跑通,再回到浏览器里去 URL 编码发送,排错效率能提升一倍以上。
另外一个调试技巧是:把 PHP 代码里的 SQL 语句复制出来,替换掉参数变量,手动在 MySQL 里执行,这样可以清楚看到注释符是否生效、闭合是否正确、拼接出来的语句是否存在语法错误。这种方式比反复猜要高效得多。
6. 最后再分享一些经验
说了这么多,SQLiLabs 最核心的价值并不是那 65 个 payload,而是你通过它建立起的"SQL 语句结构感"。我在实际测试中遇到过形形色色的注入点,很多和靶场里的关卡长得并不一样,参数名更乱、过滤更复杂、后端框架封装的层次更多,但只要你能在脑海中对"后台到底在拼什么 SQL"有一个清晰的判断,剩下的都是套路的变体。靶场练的不是 payload 记忆,是对查询结构拆解的反应速度。
另外就是一定要自己动手在 MySQL 里跑一跑那些拼接后的 SQL 语句,把这个动作变成习惯。我见过不少只刷靶场不看数据的人,上了实战环境遇到报错就懵。原因很简单:靶场是确定性的,实战环境是非确定性的。你在靶场里如果连 MySQL 手动执行语句都做不利索,遇到一个页面有多重判断、回显位置被框架模板控制住的场景时,基本就卡住了。
最后一句话:搭建靶场五分钟,理解一条 SQL 语句慢慢练、反复练是一个星期的事。SQLiLabs 是真的值得认真刷两遍的靶场——第一遍跟着教程走,第二遍只看源码自己逆推 payload。这个过程走完,你对 SQL 注入的认知会有一个质的提升。
