简介:sqli-labs是一款经典的开源SQL注入漏洞练习靶场,面向网络安全初学者、渗透测试工程师及信息安全专业学生,可在本地或虚拟机环境快速搭建,用于系统学习Web渗透测试中的SQL注入技术。靶场内置从基础到进阶的多种注入场景,涵盖联合查询、报错注入、布尔盲注、时间盲注、堆叠注入等常见类型,并配有数据库连接配置与练习关卡,能够帮助使用者在实战化环境中直观理解漏洞原理与利用手法。压缩包体积仅3.48MB,轻量便携,共包含565个文件,以PHP源码作为核心场景文件,辅以大量PNG/JPG截图用于界面展示和解题参考,另有TXT说明、CSS样式、SQL数据文件等,目录结构清晰,便于按关卡逐个检索学习。目前已有67人学习,适合希望从入门到进阶掌握SQL注入漏洞发现、利用与防御技巧的学习者,是提升Web安全实战能力的优质实践平台。
1. sqli-labs 靶场:一个把各种注入场景排好队等你打的 SQL 漏洞学习环境
sqli-labs 靶场是一套开源的 SQL 注入漏洞训练环境,底层用 PHP + MySQL 把常见的注入场景拆成了一关一关的页面题。除了最基础的数字型、字符型注入,它把报错、布尔盲注、时间盲注、宽字节、堆叠注入这些分支全部做了独立关卡,每一关的拼接逻辑都能在源码里直接对比,适合想系统性过一遍注入路径的人,也适合给团队做内部技能评估。我拆这套资源的时候发现,它能解决一个很具体的痛点:新手不知道 payload 为什么非得拼成那样,老手想知道不同闭合方式在哪种环境组合下会翻车。它不讲空理论,只提供可操作的场景,而场景恰恰是最能长出动手能力的地方。
2. 部署与启动:从源码解压到第一关能通的三步路径
2.1 环境选型:集成环境与 Docker 的取舍
先从环境说起。sqli-labs 依赖 PHP、MySQL、Apache/Nginx 三件套。常见做法是装集成环境,这样 PHP 版本和 MySQL 服务都有人帮你管着,适合平时不搭 LNMP 的人。我的建议是,能手上有现成集成环境就用集成环境;如果机器上已经装了 Docker,用 Docker 反而更省事,因为可以把数据目录挂出来,复盘的时候把整个堆栈重启一遍也只要几秒钟。
两个方案的取舍点主要在这里:
集成环境的好处是打开就能改代码,断点调试方便,适合边打关边看源代码逻辑;坏处是 PHP 版本和 MySQL 版本锁定得比较死,老靶场代码对高版本 PHP 不友好。这一点后面避坑章节会专门讲,先有个印象就好。
Docker 的好处是版本可控,关机和恢复都干净;坏处是如果你不熟悉数据卷挂载,打完一关想保留数据库现场,还得额外做一次导出。我一般会先把集成环境跑通,再用 Docker 做隔离复现,这样两边的坑都能摸到。另外部署前记得把端口占用的检查做了,Apache 默认 80、MySQL 默认 3306,任何一个被本机服务占着,后面都会出现能装不能跑的尴尬。
2.2 改数据库连接配置:一条退路与四个参数
我假设你已经把源码包下载解压到了 Web 根目录下,下面直接说配置。整个靶场最关键的配置文件是数据库连接文件,里面填数据库地址、账号、密码。先看解压和备份这一步:
# 解压源码到 Web 根目录,注意不要带中文路径 unzip sqli-labs.zip -d /var/www/html/ # 进入项目目录,拷贝一份环境配置并打开 cd /var/www/html/sqli-labs/sql-connections/ cp sql-connect.php sql-connect.php.bak vi sql-connect.php为什么建议先备份一份再改?因为后面踩坑排查时,很可能需要把错误的配置改回去,保留备份等于留了后悔药。实际项目里我遇到过改错参数找不到原样的情况,改之前随手 cp 一下,成本几乎为零,收益却很大。
数据库连接文件里最重要的几项配置是这些:
$dbuser = 'root'; // 数据库用户名 $dbpass = 'root'; # 数据库密码改成你自己的 $dbname = 'sqli-labs'; // 靶场库名,保持不变 $host = '127.0.0.1'; // 数据库地址,默认本机参数说明在这里:数据库地址如果不是本地,要把 $host 改成 MySQL 所在机器的 IP;密码必须和 MySQL 实际设置的 root 密码一致,否则后面导入初始化表会直接报权限错误。字段名如果长得不一样,看你自己下载的版本,但含义大体是这三样。有的版本把连接信息写在一个数组里,有的版本用常量,找到形如 $host/$dbuser/$dbpass 的三处改掉就行,别动 $dbname。
改完配置后,接着初始化数据库。常见做法是在项目根目录找到初始化 SQL 文件,用命令行导入,不同版本文件名可能不同,但一定是 sql 结尾的脚本:
# 进入目录,用重定向导入初始化脚本 mysql -u root -p < db.sql # 导入完成之后,用 PHP 自带服务器启动靶场 php -S 127.0.0.1:8080 -t /var/www/html/sqli-labs/如果手头不是命令行环境,也可以用数据库管理工具导入,效果一样。导入完成后访问首页,页面如果出现一张牌桌一样的关卡列表,说明初始化脚本已经成功执行。这里我一般会多看一步:把数据库里已存在的表列出来,确认一下里面有几个表,免得后面打关时脚本报“找不到数据表”。
mysql -u root -p -e "USE sqli-labs; SHOW TABLES;"正常情况下能看到若干张以 users 为核心的表,users 表里的数据就是各关卡的注入目标。
2.3 初始化数据库与 curl 验证
配置完成后不要急着打开浏览器,先用 curl 打一发,确认 PHP 解析和数据库连接同时没问题。第一关的注入点是 GET 的 id 参数,请求长这样:
curl -i "http://127.0.0.1:8080/Less-1/?id=1"返回结果里如果有 HTTP 200、页面上能解析出用户名和密码,就说明环境 up 起来了。如果返回 500 或空页面,优先查两处:数据库连接参数是否填对,以及 PHP 版本是否兼容。这个检查动作很便宜,每次重新部署后我都会强制做一遍,因为只看首页能打开完全不够,首页根本没有走到 SQL 查询那一步。
3. 从 Less-1 开始:闭合方式与字段数判定的实战规律
3.1 关卡命名里的规律:从闭合方式看懂题型
sqli-labs 的关卡命名是有规律的,前半段是基础注入,中间开始考注释符过滤和绕过,再往后进入宽字节和二次注入,最后是堆叠注入和 cookie、User-Agent 头注入。这些编号不是随便排的,它就是在模拟一个从“啥过滤都没有”到“啥都过滤一点”的对抗过程。我拆开源码看的时候,觉得最有价值的其实是每个关卡页面上方那一段注释,它直接写明了本关卡的 payload 类型和闭合方式。先打开一个关卡,看 PHP 源码里的 SQL 拼接:
// Less-1: GET - Single Quote - String based // 核心 SQL 拼接方式 $sql = "SELECT * FROM users WHERE id='$id' LIMIT 0,1";看到没有,$id 外面包了一层单引号,这就是字符型注入的标志。如果你照搬数字型注入的 payload 进去,SQL 大概率拼出语法错误,页面会报错但不会展示数据。这个观察习惯很重要,后面对付每一关前,我都建议先打开源码确认闭合符号是什么。下面是前几个关卡的闭合方式对照表,可以用来快速对题型:
| 关卡 | 闭合方式 | 注入入口 | 提交方式 |
|---|---|---|---|
| Less-1 | 单引号 ' | id 参数 | GET |
| Less-2 | 无闭合(数字型) | id 参数 | GET |
| Less-3 | 单引号加括号 ') | id 参数 | GET |
| Less-4 | 双引号加括号 ") | id 参数 | GET |
| Less-5 | 单引号 '(无回显) | id 参数 | GET |
参数说明:这里“闭合方式”的意思是,你在参数里拼进去的那段内容,需要把一个完整的 SQL 表达式闭合,后面才能跟上自己的注入语句。比如 Less-1 你输入 1,实际执行的是 WHERE id='1',如果你要让它变成长度可控的表达式,得先用一个单引号把前面的引号补上,这就是闭合的来源。Less-2 是数字型,没有引号包着,判断起来反而更直接。
3.2 第一关完整注入流程:试探、数字段、联合查询
现在拿 Less-1 走一遍标准流程。目标就一句话:确认注入点存在,然后绕过整个查询条件的限制。第一步是试探,传入单引号让 SQL 语法出错,出错说明特殊字符进入了 SQL 拼接:
curl -i "http://127.0.0.1:8080/Less-1/?id=1'"如果页面报出 MySQL 语法错误,并且错误信息里带着你的输入,那就说明代码把用户输入直接拼进了 SQL,注入点实锤。这里要强调的是,报错本身不是漏洞利用,而是利用的地图。你得靠报错信息里暴露的语句结构,去设计后面的 payload。
第二步是判断字段数。使用 order by 逐一加列,直到页面出错为止:
curl -i "http://127.0.0.1:8080/Less-1/?id=1' order by 3--+ " curl -i "http://127.0.0.1:8080/Less-1/?id=1' order by 4--+ "第一次请求正常,说明表的字段数大于等于 3;第二次报错,说明字段数就是 3。--+ 在这里是把后面的引号和语句注释掉,+ 在 URL 里表示空格,防止注释符号后面没空格导致闭合失败。这个细节新手最容易踩:-- 后面没有空格在 MySQL 里不算注释,而 URL 传参时直接发空格又会被吃掉,所以用 + 代替。我一般从 order by 3 开始试,因为用户表最少也会有三四个字段,从 1 开始只是徒增请求。
第三步是联合查询回显。把 id 改成不存在的 -1,让前面的查询结果为空,再并上我们的注入查询:
curl -i "http://127.0.0.1:8080/Less-1/?id=-1' union select 1,2,3--+"页面上出现 2 和 3 两个回显位置,说明第二列和第三列能直接展示查询结果。到这之后,就可以把 2 的位置替换成 database(),把 3 的位置替换成 group_concat(table_name) 之类的查询去拖数据。整套流程下来只用了四五个请求,但这套判定逻辑在后面的关卡里能反复复用。
3.3 不同闭合的适配:数字型、字符型与括号闭合
Less-2 是数字型,SQL 拼接:
$sql = "SELECT * FROM users WHERE id=$id LIMIT 0,1";数字型的好处是不用闭合单引号,直接 order by 和 union 就行,payload 写起来短很多。而 Less-3 则是单引号加右括号的闭合,需要把括号也补上:
curl -i "http://127.0.0.1:8080/Less-3/?id=1') order by 3--+"注意看,这里在单引号后面多了一个右括号,因为原始 SQL 里 id 参数被包在 ('$id') 里面,你不补右括号,后面的 order by 会被当成字符串的一部分,根本不会执行。很多人卡在 Less-3 和 Less-4,就是只在补引号,忘了把括号一起闭合。Less-4 同理,只是把单引号换成双引号,payload 开头从 1' 变成 1",结尾变成 1")。我一般会一边看源码一边改 payload,拼不出来了就先把原始 SQL 打印出来贴到编辑器里数括号。
到这一步,前三关的核心玩法已经说得差不多了。后面遇到复杂闭合,比如多个括号嵌套,我的习惯是逐个字符去还原代码里的拼接位置,在纸上画一条引号归属线,哪半边还没闭合一目了然。
4. 进阶注入:报错、盲注、宽字节与堆叠的利用边界
4.1 报错注入:updatexml 携带查询结果的原理和限制
报错注入适用于页面没有正常数据回显,但会把数据库报错信息打印出来的场景。原理很简单:人为构造一个能让数据库报错的表达式,让报错信息里带上我们想查的数据。sqli-labs 的 Less-5 就是这个题型的标准样本,SQL 执行结果不回显,但出错时 MySQL 会把报错拼进页面。
常见做法是用 updatexml 函数报错:
curl -i "http://127.0.0.1:8080/Less-5/?id=1' and updatexml(1,concat(0x7e,(select database()),0x7e),1)--+"逻辑说明:updatexml 的第二个参数要求是一个合法的 XPath 路径,传入 concat 拼接出来的内容会因为以波浪号开头而触发“XPath 格式错误”的报错,数据库版本和当前库名就跟着报错信息一起出来了。参数说明里,0x7e 是波浪号的十六进制,用来区分报错内容边界;1 和第三个位置上的值只要不是合法路径就行,不用管它具体是什么。
这个手法的边界有两个:一是 MySQL 高版本里,报错函数输出长度被限制,只能报出大概 32 个字符,超过的部分会被截断,所以拖长数据要分段,一次拖一个字段或者用 group_concat 控制长度;二是部分版本对 updatexml 做了处置,但 extractvalue 是同一批函数的另一个选择,payload 结构完全一样,无非是把函数名换一下。手工打的时候,报错信息里经常混着 HTML 标签,眼睛容易看花,我习惯把报错内容复制到本地格式化,只截取波浪号之间的片段。
4.2 盲注脚本与延时判定的自动化写法
盲注场景下页面不显示数据,也不报错,只能靠页面真伪和时间延迟去猜。布尔盲注思路是逐个字符比较,先判断当前库名的长度,再一位一位用二分法去猜字符,请求量通常很大。我一般会用 Python 写个脚本循环去试,而不是在浏览器里手动点几百次。下面是一个基于布尔盲注猜库名长度的脚本骨架:
import requests url = "http://127.0.0.1:8080/Less-5/?id=1' and length(database())=%d--+ " # 枚举长度,哪个长度下返回页面正常,数据库名就是几 for length in range(1, 20): r = requests.get(url % length) if "Your Login name" in r.text: # 正常查询后的页面特征 print(f"database length: {length}") break注释和参数说明:这个脚本的判定特征不能直接照搬,你要打开 Less-5 的正常响应和错误响应各看一眼,挑一个只出现在正确响应里的字符串来当特征。这里我用的是登录名区域,不同环境要现场看。length(database()) 是 MySQL 内置函数,返回数据库名的字符长度。这个骨架只是第一步,第二步才是真正一个字符一个字符地猜,把 length 换成 ascii(substr(...)) 和具体数值做比较,请求量会成倍增加,但逻辑完全一样。
时间盲注则是在布尔盲注基础上多加一层判断:如果条件成立就 sleep 3 秒,利用响应时间差确认条件结果。典型 payload:
curl -i "http://127.0.0.1:8080/Less-9/?id=1' and if(ascii(substr(database(),1,1))>100,sleep(3),0)--+"这里用 ascii 和 substr 取当前库名第一个字符的 ASCII 码,和 100 做比较。网络请求时间如果接近 3 秒,说明条件成立;如果秒回,说明条件不成立。注意 sleep 的秒数设到 2 到 3 就行,设太大会拖慢整个猜解流程,扫太多慢响应也会有超时。
4.3 宽字节与堆叠注入:字符集和多语句的边界
宽字节注入利用的是数据库编码转换的漏洞。PHP 对入参做了 addslashes 转义,在单引号前自动加反斜杠,所以常规的单引号闭合会被挡掉。但当数据库字符集是 GBK 这类宽字节编码时,前面补一个 %df,转义过程中 %df 和反斜杠一起被解析成一个宽字符,反斜杠就失去了转义作用,后面的单引号顺理成章逃逸出来。典型 payload:
curl -i "http://127.0.0.1:8080/Less-32/?id=1%df' union select 1,2,3--+ "这个题型的坑在于字符集。sqli-labs 的 Less-32 在有些版本下默认连接字符集是 utf-8,宽字节直接不生效,页面还是给你返回空结果。我一般会先去代码里确认有没有设置 set names gbk,没有的话要在连接后补上指定字符集,否则这个 payload 只能当玄学看。
堆叠注入则是利用数据库连接驱动里允许多条语句一起执行的特性:分号后面再接一条语句,就可以绕过原来 SQL 的查询约束直接操作其他表。sqli-labs 的 Less-38 之后大量出现这种结构。注意堆叠注入能不能成,取决于连接驱动是否支持多语句,不是所有场景都行,盲打之前先看一眼代码里用的是不是多语句查询接口。
4.4 工具验证:手工判断后的二次确认
手工流程走通了,我一般会再用工具复核一次,不是图省事,是怕手误。常见的开源扫描工具可以直接指定注入类型和 URL,把手工找到的闭合方式填进去,跑一个快速验证。工具的价值在于批量导出数据,但前提是你已经确定了闭合方式和注入类型,否则拿默认 payload 一把梭,大概率什么都扫不出来。我的习惯是先手工把前几关打穿,再用工具的 -p 参数指定注入参数、用 --technique 指定注入类型,这样结果能互相印证。注意工具跑的时候响应时间要设置短一点,别把靶场环境打崩。
5. 踩坑集中营:环境、注释、字符集与回显的现场记录
下面这几条踩坑记录,是我见过、也自己踩过的频率最高的几类。每条都按现象、原因、解决的思路写,方便你在现场对照排查。
5.1 首页能开,点进第一关直接白屏
现象:首页能显示关卡列表,但打开 Less-1 后页面空白,状态码可能是 200,也可能是 500,浏览器控制台里看不到任何 SQL 报错。 原因:后端 PHP 报错被完全隐藏,白屏背后是 SQL 语句因为数据库连接失败执行异常。最常发生在 MySQL 8 以上的环境,老版本靶场代码用的连接密码哈希方式和新的认证插件不一致,数据库连接在握手阶段就被拒绝。 解决:先用命令行手动登录 MySQL,确认密码有效且账号权限足够;如果命令行能进,再用代码里同样的参数在 PHP 侧跑一次连接,定位到认证插件问题后,把 MySQL 的默认认证方式切换为旧版本兼容模式,或者干脆换回 PHP 5.6 加 MySQL 5.7 的组合。我从那以后部署这类老项目都固定走这个顺序,先命令行后代码,能省掉大量“以为配置了其实没生效”的时间。
5.2 前几关打得顺利,到 Less-5 突然没有回显
现象:Less-1 到 Less-4 正常回显用户名和密码,到了 Less-5 同样的 id=1 页面上什么都没了。 原因:Less-5 开始换了题型,查询结果不再直接打印,进入报错注入和盲注区域。很多人以为环境坏了,实际上只是这道题的输出方式不一样。 解决:先输入 id=1 看页面还有没有登录名和密码;再输入 id=1' 看是否出现 SQL 报错;没有正常回显但有报错,走报错注入;连报错都没有,走盲注。这个判断流程我建议做成肌肉记忆,每到一个新关都先问一句“回显在哪”。
5.3 order by 猜字段数时,前面正常,后面突然报错
现象:order by 2 正常,order by 3 正常,order by 4 直接 500,但源码里表结构明明是四个字段。 原因:不是猜错了字段数,而是 URL 里的参数在拼接时吞掉了空格或注释符。尤其是用 -- 注释时,后面必须跟空格,HTTP 请求里空格容易失效,导致注释没生效,后面的引号和符号还是被当成 SQL 的一部分。 解决:把注释符统一写成 --+ 或者 --%20,注意加号在 URL 查询里表示空格。如果用了 # 注释,老版本 MySQL 对 # 的解析和现代版本有差异,优先用 -- 加空格的形式。改完注释再试,报错位置会回到真正的字段数上。
5.4 宽字节注入打了十几遍都不生效
现象:Less-32 按教程拼 1%df' 开头的 payload,提交后返回空页面,也没有任何注入成功的迹象。 原因:数据库连接字符集不是 GBK,而是 utf-8。%df 加反斜杠的组合在 utf-8 下会原样传给 MySQL,反斜杠没有被合并成宽字符,加的还是转义反斜杠。 解决:先读源码看连接串里有没有 set names gbk,没有就把连接参数改掉,或者在复现环境里把整个数据库默认字符集调成 gbk 后重启 MySQL 服务。改完再去打,效果立竿见影。记住宽字节成立的前提是字符集在数据库和连接两个层面都对齐。
5.5 时间盲注请求全部秒回,payload 看起来没问题
现象:if(条件,sleep(3),0) 打进去,所有请求都瞬间完成,完全看不出时间差。 原因:sleep 延时依赖的条件判断结果恒为假,最常见的是注释符没闭合导致 if 条件被注释掉,或者 substr 的起始位置写错,从 0 开始的取值习惯在 SQL 里直接错位。 解决:先改为布尔盲注验证条件本身,把 if 的条件单独拿出来打,看页面是否有正确响应的特征;确认条件有效后再加 sleep。我习惯把这种判断拆成两步,能排除大量“看起来是对的”的假象。时间参考值也要先测正常请求的响应时间,再决定 sleep 秒数。
6. 一晚上推进到后半段的技巧:健康检查脚本与源码優先
写脚本批量验证是最划算的投入。我的做法是,用 Python 写一个健康检查脚本,给定一个关卡路径列表,逐一请求并把响应状态、响应时间和页面长度记录下来。这样一眼就能看出哪些关卡环境没起,哪些关卡换了题型。
import requests import time paths = ["Less-1/?id=1", "Less-5/?id=1", "Less-9/?id=1", "Less-32/?id=1"] for p in paths: url = "http://127.0.0.1:8080/" + p start = time.time() try: r = requests.get(url, timeout=5) print(f"{p}: status={r.status_code} " f"time={time.time()-start:.2f}s len={len(r.text)}") except requests.RequestException as e: print(f"{p}: error={e}")输出里每行会显示状态码、耗时和页面长度。Less-9 正常请求的时间和 Less-1 正常请求的时间不会差太多,如果某一关请求时间异常偏高,优先怀疑是数据库连接超时或者被 sleep 拖住了。页面长度如果是 0 并且状态码 500,直接去源码里排查数据库参数,别再浪费时间看 payload。
打每关前,我强烈建议先看代码,顺序定死:先看连接文件确认字符集和数据库参数,再看关卡自身的 PHP 文件确认闭合方式和是否过滤,最后再构造 payload。很多人喜欢拿通用 payload 一把梭,梭完卡住又不知道从哪下手,问题就出在没有建立源码優先的习惯。我自己的固定流程是:每换一个新关卡,先用 curl 看正常响应,再打开编辑器读关卡源码,把其中的 SQL 拼接语句抄出来,圈出闭合符,然后再决定注入手法。这套流程做完,十有八九能一晚上推进到后半段。它比我当初直接拿工具扫的效率高太多,原因很简单:你已经知道每一关的代码长什么样,剩下的只是在验证自己的判断。从那以后我每次部署新的靶场环境,都强制自己先写一遍健康检查脚本再开打,这套习惯帮我省掉过无数次环境问题的排查时间。希望帮到你,动手时遇到什么值得记的坑也欢迎自己补进笔记里。
本文还有配套的精品资源,点击获取