BUUCTF BabySQL:双写绕过黑名单与联合查询注入

发布时间:2026/9/17 21:49:18
BUUCTF BabySQL:双写绕过黑名单与联合查询注入 BUUCTF 上的 Web 题目里入门 SQL 注入的经典就那么几道[极客大挑战 2019]BabySQL 属于绕不过去的一关。它表面上看只是一个再普通不过的登录框两个输入框、一个提交按钮连验证码都没有很多人第一次做的时候会条件反射地甩一句admin or 11#进去然后发现页面纹丝不动或者干脆报一个莫名其妙的错。这时候新手最容易慌觉得是不是自己环境搭错了、靶机挂了。其实不是BabySQL 这个名字里的 Baby 不是说你菜而是说它是一道“入门友好型”的注入题它把考点从“你会不会注入”变成了“你会不会绕过过滤”。这个区别非常关键因为现实世界里你能直接注进去的站点越来越少真正的功夫恰恰在过滤和绕过之间那层博弈上。这篇内容我想完整地把这道题的思路链条捋一遍不是给你一个 payload 让你抄完就忘而是讲清楚每一次输入背后到底发生了什么、服务端做了什么、为什么双写能成、为什么information会变成infmation。适合刚接触 SQL 注入、刷 BUUCTF 卡在这道题上的朋友也适合已经会注但说不清过滤原理的人回头补课。我会带上实测的 payload、报错截图对照、还有我自己踩过的那几个坑保证你照着走一遍能独立复现。1. 挑战环境拆解与解题路线规划1.1 一个登录框到底暴露了什么拿到题目第一件事别急着注先看页面。BabySQL 的界面是一个典型的登录表单HTML 里通常是index.php配合POST提交字段叫username和password。你打开开发者工具看一眼 Network能看到提交方式是 POST返回的是一个 PHP 页面。这一步信息量其实很大POST 提交意味着参数在请求体里注入时你要么用 Burp 抓包改要么用浏览器插件要么直接写脚本发请求不能像 GET 那样在地址栏里拼。我一般习惯先把表单随手提交一次错误的账号密码比如admin / 123观察返回。BabySQL 返回的提示通常很短类似“用户名或密码错误”或者干脆只回显一部分。这个回显就是我们的“观察窗口”后面判断注入、判断字段数全靠它。很多人做注入题上来就想着盲注其实像这种有回显的题根本不需要浪费时间在时间盲注上直接联合查询效率高得多。判断有没有回显就看你能不能把select出来的数字显示到页面上这是决定后续技术路线的分水岭。再往下就是最有价值的一步推测后端 SQL 结构。一个登录功能最可能的长这样select * from users where username$username and password$password为什么能这么笃定因为这是 PHP 入门教程里写了十几年的标准写法题目既然定位成 Baby 级别就不会给你搞存储过程、预处理那些花活。确定了这条语句的结构我们就知道注入点有两个username和password都可以利用。理论上两个都能注但实践中我一般选username因为它在前面配合注释符#可以直接把后面的and password...整段干掉控制起来更干净。这就是解题第一步的“路线规划”定位注入点、确定语句结构、选择利用入口。1.2 黑名单过滤型注入该怎么打这道题真正的门槛在于后端挂了一个过滤函数。按惯例它会用一个黑名单数组把一些 SQL 关键字从你的输入里替换掉。你输入union select它可能给你变成空的你输入and 11它把and抹了。它的目的很朴素——让你拼不出完整的注入语句。面对这种黑名单处理思路其实就那么几类。第一类是找没被拉黑的替代写法比如过滤了空格就用/**/或%09代替过滤了就用like或regexp。第二类是大小写混写Union、SeLect但这招只对区分大小写的过滤有效题目里通常用的是不区分大小写的替换所以这招在 BabySQL 上不太好使。第三类也就是这道题的核心考点——双写绕过利用服务端过滤函数“只替换一次”的缺陷把关键字拆成两半拼进去替换完反而正好还原。你写ununionion它把中间的union删掉剩下的union恰好就是完整的union。为什么会这样因为它用的是类似str_replace或者preg_replace的函数而且很可能只执行了一轮。一轮替换是自左向右扫描的当它把ununionion里的union干掉之后剩下的字符union是替换完之后才形成的扫描已经过去了不会再回头处理。这就是典型的“replace once”漏洞。理解了这一点整道题的钥匙就到手了。剩下要做的就是把所有需要的关键字都做一次双写变形然后一道道把信息从数据库里掏出来。2. 过滤机制识别为什么双写能绕过2.1 单次替换的实现缺陷到底在哪要讲清楚双写为什么成立得先看服务端大概长什么样。一个典型的过滤实现是这样function check($str){ $blacklist array(and,or,union,select,from,where,sleep,order); foreach($blacklist as $word){ $str str_replace($word, , $str); } return $str; }关键在这个str_replace。它是把字符串里所有出现的$word都替换掉但这并不意味着你的双写会全军覆没恰恰相反——因为黑名单是一个词一个词轮流替换的每次替换之后字符串会“缩水”新的相邻字符可能拼出新的关键字但那一轮已经过去了。举个具体的例子你输入ununionion第一次扫到union时找到ununionion里第 3 位开始的union替换成空字符串变成unionunion拼起来。注意这个union是替换动作产生的不是原来输入里的而str_replace扫过就扫过了不会重新进来再处理一遍。于是最终返回给你的就是union。这里有一个很多人分不清的点str_replace本身是“全部替换”不是“只替一个”。那为什么还能绕过因为黑名单是多个词轮流处理的而且替换是从原始字符串一次性把该词清除。你藏进去的union只要是在清除完所有union之后才“浮现”出来的就不会被再清除。所以双写绕过能否成功取决于黑名单的处理顺序和词与词之间是否会相互干扰。这也是为什么有的人双写能过有的人双写不行——写到一半被另一个关键字吃掉了。我实测下来BabySQL 的黑名单里至少包含了and、or、union、select、from、where这几个高频词。注意or这个词非常阴它藏在很多单词里面。比如information_schema里的information就含有orinformation所以原样写information_schema会被替换成infmation_schema直接报错。正确写法是infoorrmation_schema双写or之后替换完or恰好还原成information_schema。2.2 手工探测黑名单的完整过程在动手写完整 payload 之前我强烈建议先花几分钟把黑名单摸清楚。盲目猜黑名单写出来的 payload 一直在报错你会分不清是语法错了还是关键字被吃了。探测方法很笨但很有效往username里塞一小段带关键字的输入看它返回什么。比如先测1看是否报语法错误。报错说明引号没被过滤可以正常闭合注入点存在。然后再测1 and 11#如果它照样说密码错误说明and被吃掉语句结构变了或者直接报错你就知道and可能被过滤了。接着测1 anandd 11#如果这次返回正常恭喜双写成立。这个过程听着繁琐但摸清一次之后后面所有 payload 都能套用同一套变形规则。为了让你少走弯路我把这道题里需要变形的关键字整理成了对照表直接照着替换就行目标关键字双写写法说明unionununionion联合查询核心词selectseselectlect查数据必用fromfrfromom指定表wherewhwhereere条件筛选andanandd与条件oroor逻辑或注意藏在单词里informationinfoorrmation含 or必须双写orderoorrdreer排序盲注常用注意or的双写要特别小心。它不只出现在or关键字本身还藏在information、order、from里没有from 里没有 or但在information里一定有。忘掉这一条你会卡在枚举表名那一步报错信息看着像语法错误其实是被过滤吃字了。摸清对照表之后真正的注入就变成了填空题把想执行的 SQL 语句里的敏感词逐个替换成双写形式拼进usernamepassword随便填因为#会把它注释掉。这时候要记得 URL 编码或 Burp 里正确传参#作为 fragment 标识符在 GET 里要编码成%23POST 里一般没问题但稳妥起见我习惯用----加空格代替两者效果一样。3. 从注入点确认到 flag 落地全流程3.1 闭合方式与注入点确认正式开打。先确认闭合方式。往username输入adminpassword随便提交。如果页面返回 SQL 报错类似You have an error in your SQL syntax说明单引号闭合正确注入点确认。这一步报错是好事报错意味着我们的输入直接进了数据库解析器。接着测试注释符。输入admin#如果返回不再是报错而是普通的结果可能显示登录成功失败或空的说明#生效成功把后面的and password...注释掉了。到这一步注入点的利用框架就搭好了username里可以写任意 SQL后面的密码段已经被平台注释掉了。这里有个小细节我要提醒。很多人写完admin#发现还报错就以为是#没用其实可能是 URL 传输时#被当成锚点了没发出去。POST 表单提交一般不会但如果用浏览器地址栏手动拼一定要写%23。我用 Burp 改包的时候习惯直接用admin--这个写法更稳--后面必须有一个空格在 URL 里代表空格所以--就是“两个减号加一个空格”。确认注入点之后先别急着union。先用一个最简单的逻辑测试确认一下双写规则是否成立比如输入admin anandd 11#。如果返回变成“登录成功”或者回显正常说明anandd被还原成了and双写路线打通可以放心往下走了。3.2 联合查询的字段数探测与回显位确认union select要求前后两个查询的列数一致所以我们得先知道原查询select * from users到底查了几列。最常用的方法是用order by递增配合双写admin oorrdreer bbyy 3#如果order by 3不报错说明至少 3 列再试order by 4报错说明只有 3 列。这个逻辑很朴素order by N是按第 N 列排序如果第 N 列不存在数据库必然报错。BabySQL 这张用户表通常就是三列id、username、password所以字段数是 3。确定 3 列之后直接上联合查询把数字填进去看哪几个位置会回显到页面上admin ununionion seselectlect 1,2,3#提交后会看到页面上原本显示用户名或欢迎信息的地方变成了2或者3这样的数字。哪个位置回显我们后面就把要查询的内容塞到哪个位置。假设回显位是第 2 位那么后面所有的查询都要写成select 1, 查询内容, 3。这一步非常关键选错回显位你查出来的数据无处显示会误以为查询失败。实操心得有时候页面只回显第 2 位第 1 位和第 3 位被 HTML 模板吃掉了。别急着怀疑查询错了先把三个数字都试一遍把回显位置记下来。我在答题时习惯先本地记一行注释比如“回显位2”后面拼 payload 就不容易出错。3.3 用 information_schema 逐级枚举库表列拿到回显位就可以开始掏数据了。第一步先拿当前数据库名用database()函数admin ununionion seselectlect 1,database(),3#回显出来的就是当前库名。假设是geek题目环境一般就是一个简洁的库名。有了库名下一步是把库里所有的表名拉出来数据来自information_schema.tablesadmin ununionion seselectlect 1,group_concat(table_name),3 frfromom infoorrmation_schema.tables whwhereere table_schemadatabase()#这里出现了三个变形词seselectlect、frfromom、infoorrmation。特别注意infoorrmation_schema如果你少写一个or它会变成infmation_schema直接报错。表名拉出来之后你会看到类似users、flag这样的名字。到这一步flag 大概率就在flag表里。接着枚举flag表的列名admin ununionion seselectlect 1,group_concat(column_name),3 frfromom infoorrmation_schema.columns whwhereere table_nameflag#注意最后的table_nameflag里用到了单引号如果单引号没被过滤就能直接用如果被过滤了就要换成十六进制写法0x666c6167。BabySQL 里单引号一般能用但心里要有这根弦遇到报错优先怀疑引号。列名拿到之后比如叫flag最后一步就是直接查admin ununionion seselectlect 1,flag,3 frfromom flag#页面上回显出来的那串flag{...}就是我们要的答案。整个链条是“库名 → 表名 → 列名 → 数据”一层层往下剥逻辑非常线性这也是联合查询注入最舒服的地方——有回显所见即所得。3.4 把整个过程串成可复现的操作清单为了让你照着就能跑我把关键 payload 按顺序列成一张流程表每一步都标了目的和需要注意的变形点步骤payloadusername 字段目的变形重点1admin#确认闭合与注释#或--2admin anandd 11#验证双写可用and→anandd3admin oorrdreer bbyy 3#探测字段数order→oorrdreer4admin ununionion seselectlect 1,2,3#找回显位union/select 双写5admin ununionion seselectlect 1,database(),3#取库名无6admin ununionion seselectlect 1,group_concat(table_name),3 frfromom infoorrmation_schema.tables whwhereere table_schemadatabase()#取表名from/information/where7admin ununionion seselectlect 1,group_concat(column_name),3 frfromom infoorrmation_schema.columns whwhereere table_nameflag#取列名同上注意引号8admin ununionion seselectlect 1,flag,3 frfromom flag#取 flagfrom 双写这张表我建议你保存下来以后遇到类似的“关键字被替换一次”的注入题把这张表里的变形换一换基本都是通用的。你会发现不同题目黑名单不完全一样但套路高度重合只要它用单轮替换双写就永远有戏。4. 踩坑记录与常见问题速查4.1 高频报错与排查思路做这道题的过程中我遇到的坑主要集中在几类写出来给你省点时间。第一类是“明明双写了还报错”。最常见的原因是漏了information里的or。很多人把infoorrmation写成information因为它看起来不含敏感词其实含or。遇到枚举表名就报错先检查这个单词。第二类是“注释符没生效”。表现是 payload 拼进去之后语句后半段还在参与解析导致语法错误。根因是#在传输过程中被当成了 URL 片段标识。解决办法是改用--或者把#编码成%23。我在 Burp 里改包时一律用--省心。第三类是“回显位选错”。页面上啥都没变你以为查询失败了其实是把数据塞到了不显示的那一列。遇到这种情况回到第 4 步老老实实把1,2,3三个位置都试一遍确认哪个数字真的显示出来。第四类是“字段数对不上”。union select的列数和原查询不一致时会直接报The used SELECT statements have a different number of columns。这说明你order by探出来的列数是错的回去重新探从 1 开始往上涨涨到报错为止上一个不报错的数字就是正确列数。还有一种比较隐蔽的是关键字之间互相干扰。比如你写的某个双写词在替换过程中恰好被另一个黑名单词拆开导致最终拼出来的不是你要的词。遇到这种玄学报错把 payload 拆短一个词一个词地测通常能定位到是哪个词捣的鬼。4.2 速查表与往自动化方向延伸把上面的经验压成一张问题速查表方便你边做边对现象可能原因解决方式报 SQL 语法错误关键字被过滤吃字检查双写是否完整注释后仍报错#未生效改用--或%23页面无回显变化回显位选错逐个测试 1/2/3 的位置列数不一致报错字段数探错重新order by递增探测枚举表名报错information含 or 被吃写infoorrmation查询无结果表名/列名拼错先查 information_schema 确认手工跑通一遍之后如果你想把这道题沉淀成自己的工具完全可以写个简单的 Python 脚本把双写变形规则封装成一个函数输入普通 SQL输出变形后的 payload再自动发请求、正则提取回显。我自己的习惯是把变形规则做成一个字典union、select、from、where、and、or这些一一对应脚本里遍历替换一遍就行。不过要记得or的替换顺序得放在最后否则可能把infoorrmation里已经双写好的or又处理一遍反而弄巧成拙。这种细节只有自己写过一遍才会懂。最后再分享一个我个人的体会。这类“过滤 绕过”的题考的不是你背了多少 payload而是你对字符串处理逻辑的理解。看到关键字被替换第一反应应该是“它替换几次、按什么顺序替换、替换完会不会产生新的关键字”把这几个问题想通了双写、内联注释、等价替换全都是一回事。BabySQL 只是让你在最低的难度上把这套思维建立起来后面碰上更狠的过滤比如把union、select全换成星号、把空格彻底删掉的题你照样能顺着这个思路一点点试出来。做完这道题我建议你顺手回去把 [极客大挑战 2019]LoveSQL 也拿来对比着看两道题的过滤强度不一样放在一起做对“过滤与绕过”的理解会一下子立体很多。