
说实话我当初搭 sqlilabs 靶场的时候以为就是个解压、打开、点两下 setup 的事结果光环境就折腾了大半天后来才发现瓶颈根本不是搭建而是对 SQL 语句本身不熟。刷到后面几关的时候满屏幕报错和盲注逻辑看不懂 payload 里那些函数在干嘛那才叫真难受。所以这篇东西我打算把两件事揉在一起讲清楚第一sqlilabs 怎么从零搭起来不光是点鼠标配置文件里那些坑是怎么回事第二搭完之后每一关到底在考什么 SQL 语句为什么要这么写为什么注释符有时候是--有时候是#。这篇文章适合两类人看一类是刚接触 Web 安全、想在本地建个靶场练手的初学者另一类是写过不少 SQL 但没系统接触过注入原理的开发者。读完你至少能自己把 Less-1 到 Less-9 走通并且搞明白每个 payload 背后的语句逻辑。1. 为什么用 sqlilabs 练手以及本地环境搭建1.1 sqlilabs 到底解决什么问题sqlilabs 是个开源的 SQL 注入训练靶场把注入场景按关卡编号拆成了 70 多关从最基础的 GET 传参注入一路到 POST 注入、报错注入、布尔盲注、时间盲注、堆叠注入、二次注入、宽字节注入难度是阶梯式上升的。它的价值在于每一关都强制你手工判断注入点、闭合方式、回显位置然后自己拼出可用的 payload。这个自己拼的过程才是练 SQL 语句基本功的地方。为什么不建议直接去找真实站点练手道理很简单未经授权对目标系统做注入测试是明确的违规行为哪怕只是试一个单引号也可能对业务数据造成不可逆影响。本地靶场是干净的、可控的、随便炸无所谓的环境这是演练注入技术唯一安全的方式。我只在本地练这句话我希望每个读者都能放心说出口也真的做到。另外sqlilabs 这个靶场的技术栈非常传统PHP 加 MySQL逻辑简单没有任何框架干扰SQL 拼接的代码就那么几行你能直接看到后台是怎么处理你传的参数。这种一眼看穿后台逻辑的透明感对理解注入根因至关重要比拿着复杂框架的站点去猜要高效得多。1.2 搭建的整体思路一个经典的 PHP MySQL 环境sqlilabs 本质是一个 PHP 应用跑在 Apache 或 Nginx 上数据存在 MySQL 里。所以搭建靶场本质上就是搭一个能跑 PHP 的 Web 服务再建一个 MySQL 数据库。我用的是经典组合Apache PHP MySQL这套组合也是 sqlilabs 官方文档默认推荐的。操作系统选 Linux 或者 Windows 都行差别不大关键是 PHP 和 MySQL 的版本别太新。具体版本建议结合我踩过的坑来说PHP建议 5.6 到 7.4 之间。PHP 8 之后很多老语法和隐式转换行为变了sqlilabs 部分关卡会出现诡异报错或者页面直接白屏。MySQL5.7 或 8.0 都行。但要注意MySQL 8 取消了mysql_native_password的默认授权插件如果用旧版集成面板连可能认证报错需要额外设置。Web 服务器Apache 最优因为 sqlilabs 的.htaccess规则和 URL 重写依赖 Apache 特性。Nginx 也能跑但需要手动配置 pathinfo 支持麻烦一点。如果你用的是 Windows 本地开发我建议直接用集成面板装好 Apache MySQL PHP 的固定组合。这类面板的好处是把三个组件都配好了启动和停止环境也就点一下的事对新手最友好。Linux 下就老老实实用系统包管理器装apt install apache2 mysql-server php php-mysql装完把靶场丢到 web 根目录就行。1.3 解压、配置与初始化数据库靶场的安装目录名称是 sqlilabs 的解压目录你把它放到 Apache 的站点根目录下比如 Windows 面板里的www目录或者 Linux 下的/var/www/html/然后需要改一个配置文件sql-connections/setup.php里面主要配置数据库账号密码。// sql-connections/setup.php 关键部分 $dbuser root; $dbpass 你的数据库密码; $dbname security; $dbserver localhost;改完别急着访问首页先访问setup.php这个文件完成初始化比如http://127.0.0.1/sqlilabs/setup.php它会自动建库建表把靶场用的security数据库和 users 表之类的东西都创建好。看到提示创建成功之后再回到主页面就能看到绿色的关卡列表了。初始化失败时绝大多数原因就是setup.php里的数据库密码和你 MySQL 实际密码不一致要么是 MySQL 服务没启动。我之前一度以为代码有问题最后发现只是密码写错了一位。这种低级错误排查的时候先从数据库连通性开始别对着 PHP 代码发愣。注意如果你用的是 MySQL 8caching_sha2_password认证插件可能导致老版本 PHP 的 mysqli 扩展无法连接表现为页面提示数据库连接失败。解决办法是在 MySQL 里把这个靶场账号的认证插件改回mysql_native_password。1.4 环境配置里的几个关键开关搭建过程中有两个 php.ini 配置直接影响靶场体验强烈建议提前调好。第一个是display_errors也就是把 PHP 错误展示开关打开。sqlilabs 很多关卡就是靠页面报错来泄露 SQL 语句和表结构的报错被关掉后你只看到白屏什么都判断不了。具体操作是在 php.ini 中找到display_errors On error_reporting E_ALL第二个是magic_quotes_gpc这个只对 PHP 5.4 之前的版本生效如果版本较老建议设置为 Off。它会在所有外部输入上自动加反斜杠转义直接把很多注入手法废掉。其实 sqlilabs 自己会在关卡代码里处理这层逻辑但环境层级开着的化还是会影响部分关卡的触发结果提前关掉最省心。第三个略微冷门但实测重要如果用的是 Apache确认启用了mod_rewrite。某些关卡跳转依赖 URL 重写没启用的话会访问不到正确页面。Windows 集成面板一般默认开启Linux 下需要手动a2enmod rewrite。2. 玩转 sqlilabs 前必须吃透的 SQL 语句基础2.1 注入的本质闭合、注释与拼接很多新手一上来就急着背 payload看到 or 11 --就往 URL 里塞塞完不理解换个关卡就不会了。其实注入的本质非常简单后台程序把用户输入直接拼接进了 SQL 语句而你输入的内容被当作 SQL 代码执行了。我习惯用一个拼句子的类比来解释。后台原本要执行的是SELECT * FROM users WHERE username admin它里面admin是用户输入的。正常用户输入admin拼出来就是上面这句。但如果你输入的是admin or 11 --后台拼接后的完整语句就变成了SELECT * FROM users WHERE username admin or 11 -- 这里发生了三件事你用单引号闭合了字符串开头的引号用or 11改变了查询条件用--注释掉了原本结尾的那个单引号。三条加在一起原本只查一个用户现在成了查询所有用户。所以每次构造注入你就问自己三个问题它在单引号里还是数字里怎么让我的输入闭合掉前面的引号注释符能不能把后面没用的代码吃掉这是所有注入语句的基本功比任何函数都重要。2.2 union 联合查询从数据回显到脱库sqlilabs 前面最经典的几关核心就是 union 注入。union 本身是 SQL 里用来合并多个查询结果的关键字要求每个 select 的列数相同注入里正好利用它把原始查询结果和自定义查询结果合并在一起输出。要构造 union 注入第一步是确定列数。方法是用order by不停增大数字比如 order by 1 -- order by 2 -- order by 3 --当数字超过真实列数时 MySQL 会报错Unknown column 4 in order clause。能正常排序的最大数字就是当前查询的列数。这一步极其重要列数不对后边的 union 语句必然报错。知道列数后第二步是确定哪一列的数据会显示在页面上。构造 union select 1,2,3 --如果页面显示 2 和 3 的位置有数字说明第 2、3 列有回显。接下来就把对应位置替换成你想要的敏感信息比如 union select 1,database(),user() --database()返回当前数据库名user()返回当前数据库用户。这还只是第一步要查全库数据还得借助 MySQL 自带的信息库information_schema。查表名 union select 1,group_concat(table_name),3 from information_schema.tables where table_schemadatabase() --查字段名 union select 1,group_concat(column_name),3 from information_schema.columns where table_nameusers --查数据 union select 1,group_concat(username,0x3a,password),3 from users --注意0x3a是冒号的十六进制写法用于把用户名和密码隔开显示。这套流程从 order by 到 union 再到 information_schema是整靶场前半段的核心主线也是所有手工注入的基础操作。2.3 报错注入与盲注当页面没有回显时怎么办union 注入建立在页面有回显位置这个前提上。但 sqlilabs 从 Less-5 开始专门设置了不回显的关卡你传入的参数执行了但页面上看不到执行结果。这时候就得换思路也就是报错注入和盲注。报错注入的核心是让 SQL 语句主动报错并把想在报错信息里看到的内容塞进去。MySQL 里最常用的是updatexml和extractvalue它们都会因为 XPath 表达式格式错误而抛出异常而异常信息里恰好包含了传入的参数内容。典型写法 and updatexml(1,concat(0x7e,database(),0x7e),1) --concat(0x7e,database(),0x7e)会在数据库名前后加上波浪号这是为了让拼接结果不符合 XPath 格式从而强制报错并把内容显示出来。0x7e是波浪号~的十六进制表示。我之前一直不理解为什么报错注入里的 payload 都要加0x7e后来才明白不是装饰而是为了触发非法的 XPath 路径格式。再说盲注。盲注分两种布尔盲注和时间盲注。布尔盲注的页面不会直接告诉你数据内容只会告诉你条件真假比如查询成功不回显但页面显示内容有变化。通过不断构造某个字符的 ASCII 码是否等于某个值来猜数据。基本函数组合是 and ascii(substr(database(),1,1)) 100 --如果页面表现如常说明数据库名第一个字符的 ASCII 码大于 100。然后继续调整数字逐位逼近直到确定每一位字符。这个过程极其费手但能深刻理解 SQL 函数和条件判断。时间盲注更残忍页面什么反馈都没有只能通过数据库是否延迟睡觉来判断。核心是 and if(ascii(substr(database(),1,1)) 100, sleep(3), 1) --条件成立时数据库傻等 3 秒再响应不成立时秒回。所以一遍遍刷新页面看响应时间就猜出了数据内容。说实话刷时间盲注特别考验耐心我经常刷到一半怀疑人生但这套逻辑搞懂了后面对 SQL 语句的执行顺序理解会直接上一个台阶。2.4 堆叠注入与二次注入别只盯着 selectsqlilabs 中后段有一关会让不少人大呼还能这样那就是堆叠注入。很多注入教程默认只能执行一条 select因为后台代码大多数只把输入拼接进一条查询查询完就结束。但如果后台用的是支持多语句执行的接口那就可以用分号隔开一次执行多条 SQL。; insert into users(username,password) values(hack,hack); --这条 payload 的含义是先闭合前一条语句然后用分号引入一条插入语句再注释掉结尾多余内容。比起 union 注入只能查询堆叠注入可以做增删改危害等级完全不同。但能堆叠的接口很少多数 PHP 的数据库驱动默认就禁止一次执行多条语句。二次注入则是另一个脑洞第一次提交的数据被安全处理过存在数据库里的时候是干净的但下次调用时直接拼接进 SQL没有再做过滤导致原来被转义掉的引号重新激活。sqlilabs 的 Less-24 就是这个经典场景。理解二次注入重点在于认识到过滤只发生在入口危险在数据重新使用时再次引爆。3. sqlilabs 经典关卡手动注入全流程3.1 从 Less-1 到 Less-4识别字符型、数字型与括号闭合Less-1 到 Less-4 讲的都是查询闭合方式是最基础也是最核心的入门四关。它们后台代码的 SQL 拼接方式分别如下Less-1 是字符串直接拼接WHERE id$idLess-2 是数字型WHERE id$idLess-3 是字符串加括号WHERE id($id)Less-4 是双引号加括号WHERE id($id)手动测试第一步永远是加单引号看页面反应。以 Less-1 为例访问?id1页面报错并直接爆出了拼接后的 SQL 语句You have an error in your SQL syntax ... near 1 LIMIT 0,1从报错能清晰看到我们的输入被嵌在单引号里。所以构造 payload 的思路就是先闭合掉左边那个单引号再处理掉右边那个单引号。最简单的做法是输入1 --其中--后面必须有空格它把结束的单引号注释掉。SQL 标准里注释符后面要有空格MySQL 才认。Less-2 数字型就省事了直接id1 or 11或者id-1 union select 1,2,3因为根本没有引号参与数字直接参与运算。Less-3 和 Less-4 则是既要考虑引号又要考虑括号需要多试1)、1))这样的闭合组合。我自己的经验是在脑子里先想清楚后台最可能的拼接代码再构造对应的闭合字符串效率比盲试高得多。3.2 从 Less-5 到 Less-9没有回显时的手工判断Less-5 到 Less-9 是我刷靶场的一道分水岭因为从这一区间开始页面不再直接回显 SQL 查询结果。Less-5 和 Less-6 可以通过报错注入解决而 Less-7 是文件读写场景Less-8 是典型的布尔盲注Less-9 是时间盲注。以 Less-5 为例正常传?id1页面只显示一句话You are in...传?id1就会看到 SQL 报错信息说明数据库会暴露错误。这里就用上一节说的 updatexml 或 extractvalue 函数构造 payload?id1 and updatexml(1,concat(0x7e,database(),0x7e),1) --页面报错信息里就直接显示数据库名。想把数据一步步导出来就改成?id1 and updatexml(1,concat(0x7e,(select table_name from information_schema.tables where table_schemadatabase() limit 0,1),0x7e),1) --一次只能看一条数据所以要配合limit不断调整偏移量才能把所有表名看完。这就是报错注入的实际手感。Less-8 的布尔盲注则更辛苦输入?id1 and 11页面正常输入?id1 and 12页面会变成空白或者显示不同的文案。利用这个差异结合ascii()和substr()逐位猜解。比如猜数据库名第一个字母是不是大于 100?id1 and ascii(substr(database(),1,1)) 100 --页面正常说明成立然后继续二分逼近最终确定每个字符。这个过程繁琐但能让你彻底弄懂查询条件和布尔逻辑在 SQL 里的真实执行方式。Less-9 是我觉得最磨人的一关页面输出完全固定怎么输入都长一个样只能靠响应时间判断。payload 就是前面提过的if(condition, sleep(3), 1)结构。用时盲注时浏览器自带的网络面板一定要打开每次刷新看耗时才能判断条件真假。3.3 从手工到 sqlmap什么时候该用自动化我见过两种极端情况一种是从头到尾纯手工刷到后面几个盲注关卡直接崩溃手指都点酸了另一种是一上来就甩 sqlmap跑出结果也不知道 payload 为什么可以换个场景立刻傻眼。我的建议是前半段关卡必须纯手工后面盲注和报错关卡手工理解原理后可以用 sqlmap 验证结果。sqlilabs 本身就是本地靶场用它来练自动化工具的用法完全没问题。最基本的命令python sqlmap.py -u http://127.0.0.1/sqlilabs/Less-1/?id1 --batch --dbs加--dbs可以一次性把数据库列表跑出来。想跑某个库的表再指定库名python sqlmap.py -u http://127.0.0.1/sqlilabs/Less-1/?id1 -D security --tables拿表名后继续-T users --columns拿字段最后-C username,password --dump导出数据。sqlmap 的优势是快缺点是会把你的思考过程侵蚀掉。我刷完一遍靶场后刻意要求自己对每关先手工跑通再让 sqlmap 盲跑对比这样两边都练到了。重点以上所有操作只在本机靶场环境执行。sqlmap 输出结果和手工验证结果一致说明你的判断是对的如果不一致要回去检查闭合方式是不是判断错了而不是怀疑工具坏了。4. 搭建与注入实战中的常见问题排查实录4.1 环境层面的坑数据库连不上、页面白屏/404搭建环节最普遍的问题就是 setup.php 页面报数据库连接失败。我见过的新手十个有八个卡在这里。排查顺序固定为MySQL 服务起没起账号密码对不对密码里有没有特殊字符导致 PHP 解析错误。另外如果你用的是 MySQL 8python 或 PHP 老驱动连不上的话在 MySQL 里执行一句修改认证插件即可这个我在前面提到了。第二个典型问题是页面能开但所有关卡都 404。多半是访问路径不对或者 Apache 的虚拟主机配置里 DocumentRoot 没指向 sqlilabs 所在目录。检查路径时注意大小写Linux 下目录名是严格区分大小写的Sqlilabs和sqlilabs是两回事。第三个是页面白屏或者直接下载 PHP 源文件。白屏优先查 display_errors 是否开启以及 PHP 版本是否过高导致兼容性问题。看不了 PHP 文件内容就说明 PHP 解析没执行多半是 Apache 的 PHP 模块没加载要么重装环境要么确认 Apache 配置里LoadModule php那一行存在。这种问题在集成面板里一般不会出现自己手动装环境的时候最容易踩。第四个是 80 端口或 3306 端口被其他程序占用。Windows 上最常见的是本地某个服务把 80 抢了或者 MySQL 和某个程序抢 3306。各自改端口或者关掉占用进程就行。改 MySQL 端口后记得同步修改 setup.php 里的 dbserver。4.2 注入层面的坑注释符失效、编码混乱和过滤绕过刷关卡的时候有几个高频问题值得单独拿出来说。第一个坑是--后不加空格导致注释不生效。MySQL 的注释语法比较特殊--后面必须跟一个空格或者控制字符否则不被识别为行注释。很多新手在 URL 里写--因为浏览器和服务器解析时会把末尾空格吃掉注释就失效了。碰到这种情况换成#注释或者--来规避其中在 URL 里会被解析为空格所以在 URL 传参时尤其好用。第二个坑是双引号和单引号被转义。后台如果开启了某些转义函数单引号会被加上反斜杠导致闭合失败。这时候要么用十六进制编码绕过比如把字符串写成0x7573657273要么寻找业务本身就不需要引号的注入点。sqlilabs 里专门有一关是宽字节注入利用 GBK 编码把转义用的反斜杠吃掉这个现象非常绕但刷过去后再看字符集和转义之间的问题就通透多了。第三个坑是order by某些情况下不能直接用 union 接在后面。比如order by查询出来的结果排序可能有混淆导致回显位置判断不准。实际上 union 判断列数还有一种方法是用union select 1,2,3,...不断加数字直到不报错为止。两者可以互相验证。第四个坑是数据库版本差异导致 information_schema 结构不同。MySQL 5.6 以前information_schema.tables的table_schema列名和 8.0 一致但某些接口或视图有细微差别。如果 payload 报列不存在优先看 MySQL 版本号把有针对性的语法调整一下这个问题在真实开发中也会遇到。4.3 手工注入的通用自查思路与工具配合刷完前面章节你已经知道注入的核心是闭合、注释、回显和函数组合。但当 payload 不生效的时候怎么快速定位问题我给自己定了一个五步排查法实测非常管用确认注入点参数是 GET 还是 POST是否被编码过。用浏览器开发者工具看实际发出的请求别凭记忆手敲 URL。确认闭合方式。加单引号、双引号、括号观察报错变化。报错信息是排查免费提示器别浪费它。确认注释符是否真的生效。换#、--、--多试几次。如果还在 URL 里注意会被解码成空格。确认列数和回显位置。一条一条试order by不贪快。确认函数和子查询是否受限。有些关卡对逗号、空格、关键字做了过滤可能要用替代写法或者编码绕过。工具配合方面本地靶场建议浏览器开发者工具加一款抓包工具比如 Fiddler 或 Burp Suite就够用了。抓包工具的主要用途不是自动化而是看清第一次请求和第二次请求的实际报文差异以及响应里到底多了什么。sqlilabs 后半部分有 cookie 注入、POST 注入、UA 注入这类场景直接在地址栏改参数是不够的必须靠抓包工具修改请求再重放。我的工作流是浏览器访问关卡确认页面状态抓包工具打开手工改一处参数发送观察响应差再用 sqlmap 拉一把做辅助验证。手动和自动交替既能把 SQL 语句逻辑吃透又能熟悉工具链。4.4 靶场复现与扩展多刷几轮注重对比sqlilabs 刷一遍只是个开始。我强烈建议刷完一遍之后换一种环境再来一轮比如在 Windows 面板环境刷完一遍换到 Linux 命令行环境再来一遍。搭建过程中的差异会逼你把环境配置的每个细节搞清楚而这恰恰是安全从业者的基本功。另外sqlilabs 只覆盖了注入类问题。如果你对 SQL 语句其他方向感兴趣可以把靶场里遇到的数据查询逻辑搬到自己的本地测试库里自己写一个简单的登录页复现一遍二次注入或堆叠注入的场景。这样你就从做题人变成了出题人对 SQL 语句的理解会更落地。我记得自己第二次搭靶场的时候特意开了 PHP 错误日志把每个关卡触发时的 SQL 语句捞出来和 payload 做对照。那一刻我才真正理解为什么注入 payload 里的--要加空格为什么有些场景要用%0a换行来绕过为什么盲注函数要这么嵌套。这些细节光看教程永远学不会只有自己亲手搭一遍、调一遍、炸一遍才记得住。最后说点实在的。sqlilabs 这套靶场代码虽然老但它对 SQL 语句的考察面覆盖得非常系统从简单的拼接闭合到复杂的条件盲注每一关都是对 SQL 语言本身的拆解。如果你正处于会写增删改查但不太懂 SQL 执行机理的阶段认真刷完前 20 关对 SQL 的理解会有一次质的提升。搭建过程中遇到任何环境报错别慌按着数据库、Web 服务、配置项的顺序一层层排查绝大多数问题都跑不出这几个环节。