sqli-labs靶场实战:从环境部署到SQL注入绕过手法详解

发布时间:2026/10/14 3:45:13
sqli-labs靶场实战:从环境部署到SQL注入绕过手法详解 简介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/?id1返回结果里如果有 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 参数GETLess-2无闭合数字型id 参数GETLess-3单引号加括号 )id 参数GETLess-4双引号加括号 )id 参数GETLess-5单引号 无回显id 参数GET参数说明这里“闭合方式”的意思是你在参数里拼进去的那段内容需要把一个完整的 SQL 表达式闭合后面才能跟上自己的注入语句。比如 Less-1 你输入 1实际执行的是 WHERE id1如果你要让它变成长度可控的表达式得先用一个单引号把前面的引号补上这就是闭合的来源。Less-2 是数字型没有引号包着判断起来反而更直接。3.2 第一关完整注入流程试探、数字段、联合查询现在拿 Less-1 走一遍标准流程。目标就一句话确认注入点存在然后绕过整个查询条件的限制。第一步是试探传入单引号让 SQL 语法出错出错说明特殊字符进入了 SQL 拼接curl -i http://127.0.0.1:8080/Less-1/?id1如果页面报出 MySQL 语法错误并且错误信息里带着你的输入那就说明代码把用户输入直接拼进了 SQL注入点实锤。这里要强调的是报错本身不是漏洞利用而是利用的地图。你得靠报错信息里暴露的语句结构去设计后面的 payload。第二步是判断字段数。使用 order by 逐一加列直到页面出错为止curl -i http://127.0.0.1:8080/Less-1/?id1 order by 3-- curl -i http://127.0.0.1:8080/Less-1/?id1 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/?id1) 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/?id1 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/?id1 and length(database())%d-- # 枚举长度哪个长度下返回页面正常数据库名就是几 for length in range(1, 20): r requests.get(url % length) if Your Login name in r.text: # 正常查询后的页面特征 print(fdatabase length: {length}) break注释和参数说明这个脚本的判定特征不能直接照搬你要打开 Less-5 的正常响应和错误响应各看一眼挑一个只出现在正确响应里的字符串来当特征。这里我用的是登录名区域不同环境要现场看。length(database()) 是 MySQL 内置函数返回数据库名的字符长度。这个骨架只是第一步第二步才是真正一个字符一个字符地猜把 length 换成 ascii(substr(...)) 和具体数值做比较请求量会成倍增加但逻辑完全一样。时间盲注则是在布尔盲注基础上多加一层判断如果条件成立就 sleep 3 秒利用响应时间差确认条件结果。典型 payloadcurl -i http://127.0.0.1:8080/Less-9/?id1 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 和反斜杠一起被解析成一个宽字符反斜杠就失去了转义作用后面的单引号顺理成章逃逸出来。典型 payloadcurl -i http://127.0.0.1:8080/Less-32/?id1%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 同样的 id1 页面上什么都没了。 原因Less-5 开始换了题型查询结果不再直接打印进入报错注入和盲注区域。很多人以为环境坏了实际上只是这道题的输出方式不一样。 解决先输入 id1 看页面还有没有登录名和密码再输入 id1 看是否出现 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/?id1, Less-5/?id1, Less-9/?id1, Less-32/?id1] for p in paths: url http://127.0.0.1:8080/ p start time.time() try: r requests.get(url, timeout5) print(f{p}: status{r.status_code} ftime{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 拼接语句抄出来圈出闭合符然后再决定注入手法。这套流程做完十有八九能一晚上推进到后半段。它比我当初直接拿工具扫的效率高太多原因很简单你已经知道每一关的代码长什么样剩下的只是在验证自己的判断。从那以后我每次部署新的靶场环境都强制自己先写一遍健康检查脚本再开打这套习惯帮我省掉过无数次环境问题的排查时间。希望帮到你动手时遇到什么值得记的坑也欢迎自己补进笔记里。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询