BUUCTF BabySQL:双写绕过黑名单与联合查询注入
2026/9/17 21:49:01 网站建设 项目流程

BUUCTF 上的 Web 题目里,入门 SQL 注入的经典就那么几道,[极客大挑战 2019]BabySQL 属于绕不过去的一关。它表面上看只是一个再普通不过的登录框,两个输入框、一个提交按钮,连验证码都没有,很多人第一次做的时候会条件反射地甩一句admin' or 1=1#进去,然后发现页面纹丝不动,或者干脆报一个莫名其妙的错。这时候新手最容易慌,觉得是不是自己环境搭错了、靶机挂了。其实不是,BabySQL 这个名字里的 Baby 不是说你菜,而是说它是一道“入门友好型”的注入题,它把考点从“你会不会注入”变成了“你会不会绕过过滤”。这个区别非常关键,因为现实世界里你能直接注进去的站点越来越少,真正的功夫恰恰在过滤和绕过之间那层博弈上。

这篇内容我想完整地把这道题的思路链条捋一遍,不是给你一个 payload 让你抄完就忘,而是讲清楚每一次输入背后到底发生了什么、服务端做了什么、为什么双写能成、为什么information会变成infmation。适合刚接触 SQL 注入、刷 BUUCTF 卡在这道题上的朋友,也适合已经会注但说不清过滤原理的人回头补课。我会带上实测的 payload、报错截图对照、还有我自己踩过的那几个坑,保证你照着走一遍能独立复现。

1. 挑战环境拆解与解题路线规划

1.1 一个登录框到底暴露了什么

拿到题目,第一件事别急着注,先看页面。BabySQL 的界面是一个典型的登录表单,HTML 里通常是index.php配合POST提交,字段叫usernamepassword。你打开开发者工具看一眼 Network,能看到提交方式是 POST,返回的是一个 PHP 页面。这一步信息量其实很大:POST 提交意味着参数在请求体里,注入时你要么用 Burp 抓包改,要么用浏览器插件,要么直接写脚本发请求,不能像 GET 那样在地址栏里拼。

我一般习惯先把表单随手提交一次错误的账号密码,比如admin / 123,观察返回。BabySQL 返回的提示通常很短,类似“用户名或密码错误”或者干脆只回显一部分。这个回显就是我们的“观察窗口”,后面判断注入、判断字段数全靠它。很多人做注入题上来就想着盲注,其实像这种有回显的题根本不需要浪费时间在时间盲注上,直接联合查询效率高得多。判断有没有回显,就看你能不能把select出来的数字显示到页面上,这是决定后续技术路线的分水岭。

再往下就是最有价值的一步:推测后端 SQL 结构。一个登录功能,最可能的长这样:

select * from users where username='$username' and password='$password'

为什么能这么笃定?因为这是 PHP 入门教程里写了十几年的标准写法,题目既然定位成 Baby 级别,就不会给你搞存储过程、预处理那些花活。确定了这条语句的结构,我们就知道注入点有两个,usernamepassword都可以利用。理论上两个都能注,但实践中我一般选username,因为它在前面,配合注释符#可以直接把后面的and password=...整段干掉,控制起来更干净。这就是解题第一步的“路线规划”:定位注入点、确定语句结构、选择利用入口。

1.2 黑名单过滤型注入该怎么打

这道题真正的门槛在于后端挂了一个过滤函数。按惯例,它会用一个黑名单数组,把一些 SQL 关键字从你的输入里替换掉。你输入union select,它可能给你变成空的;你输入and 1=1,它把and抹了。它的目的很朴素——让你拼不出完整的注入语句。

面对这种黑名单,处理思路其实就那么几类。第一类是找没被拉黑的替代写法,比如过滤了空格就用/**/%09代替,过滤了=就用likeregexp。第二类是大小写混写,UnionSeLect,但这招只对区分大小写的过滤有效,题目里通常用的是不区分大小写的替换,所以这招在 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,替换成空,字符串变成unionun+ion拼起来)。注意,这个union是替换动作产生的,不是原来输入里的,而str_replace扫过就扫过了,不会重新进来再处理一遍。于是最终返回给你的就是union

这里有一个很多人分不清的点:str_replace本身是“全部替换”,不是“只替一个”。那为什么还能绕过?因为黑名单是多个词轮流处理的,而且替换是从原始字符串一次性把该词清除。你藏进去的union只要是在清除完所有union之后才“浮现”出来的,就不会被再清除。所以双写绕过能否成功,取决于黑名单的处理顺序和词与词之间是否会相互干扰。这也是为什么有的人双写能过,有的人双写不行——写到一半被另一个关键字吃掉了。

我实测下来,BabySQL 的黑名单里至少包含了andorunionselectfromwhere这几个高频词。注意or这个词非常阴,它藏在很多单词里面。比如information_schema里的information就含有or(information),所以原样写information_schema会被替换成infmation_schema,直接报错。正确写法是infoorrmation_schema,双写or之后,替换完or恰好还原成information_schema

2.2 手工探测黑名单的完整过程

在动手写完整 payload 之前,我强烈建议先花几分钟把黑名单摸清楚。盲目猜黑名单,写出来的 payload 一直在报错,你会分不清是语法错了还是关键字被吃了。

探测方法很笨但很有效:往username里塞一小段带关键字的输入,看它返回什么。比如先测1',看是否报语法错误。报错说明引号没被过滤,可以正常闭合,注入点存在。然后再测1' and 1=1#,如果它照样说密码错误(说明and被吃掉,语句结构变了)或者直接报错,你就知道and可能被过滤了。接着测1' anandd 1=1#,如果这次返回正常,恭喜,双写成立。

这个过程听着繁琐,但摸清一次之后,后面所有 payload 都能套用同一套变形规则。为了让你少走弯路,我把这道题里需要变形的关键字整理成了对照表,直接照着替换就行:

目标关键字双写写法说明
unionununionion联合查询核心词
selectseselectlect查数据必用
fromfrfromom指定表
wherewhwhereere条件筛选
andanandd与条件
oroor逻辑或,注意藏在单词里
informationinfoorrmation含 or,必须双写
orderoorrdreer排序,盲注常用

注意:or的双写要特别小心。它不只出现在or关键字本身,还藏在informationorderfrom里没有(from 里没有 or),但在information里一定有。忘掉这一条,你会卡在枚举表名那一步,报错信息看着像语法错误,其实是被过滤吃字了。

摸清对照表之后,真正的注入就变成了填空题:把想执行的 SQL 语句里的敏感词,逐个替换成双写形式,拼进usernamepassword随便填,因为#会把它注释掉。这时候要记得 URL 编码或 Burp 里正确传参,#作为 fragment 标识符在 GET 里要编码成%23,POST 里一般没问题,但稳妥起见我习惯用--+--加空格)代替,两者效果一样。

3. 从注入点确认到 flag 落地全流程

3.1 闭合方式与注入点确认

正式开打。先确认闭合方式。往username输入admin'password随便,提交。如果页面返回 SQL 报错(类似You have an error in your SQL syntax),说明单引号闭合正确,注入点确认。这一步报错是好事,报错意味着我们的输入直接进了数据库解析器。

接着测试注释符。输入admin'#,如果返回不再是报错而是普通的结果(可能显示登录成功失败或空的),说明#生效,成功把后面的and password='...'注释掉了。到这一步,注入点的利用框架就搭好了:username里可以写任意 SQL,后面的密码段已经被平台注释掉了。

这里有个小细节我要提醒。很多人写完admin'#发现还报错,就以为是#没用,其实可能是 URL 传输时#被当成锚点了没发出去。POST 表单提交一般不会,但如果用浏览器地址栏手动拼,一定要写%23。我用 Burp 改包的时候,习惯直接用admin'--+,这个写法更稳,--后面必须有一个空格,+在 URL 里代表空格,所以--+就是“两个减号加一个空格”。

确认注入点之后,先别急着union。先用一个最简单的逻辑测试确认一下双写规则是否成立,比如输入admin' anandd 1=1#。如果返回变成“登录成功”或者回显正常,说明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.tables

admin' ununionion seselectlect 1,group_concat(table_name),3 frfromom infoorrmation_schema.tables whwhereere table_schema=database()#

这里出现了三个变形词:seselectlectfrfromominfoorrmation。特别注意infoorrmation_schema,如果你少写一个or,它会变成infmation_schema,直接报错。表名拉出来之后,你会看到类似usersflag这样的名字。到这一步,flag 大概率就在flag表里。

接着枚举flag表的列名:

admin' ununionion seselectlect 1,group_concat(column_name),3 frfromom infoorrmation_schema.columns whwhereere table_name='flag'#

注意最后的table_name='flag'里用到了单引号,如果单引号没被过滤就能直接用;如果被过滤了,就要换成十六进制写法0x666c6167。BabySQL 里单引号一般能用,但心里要有这根弦,遇到报错优先怀疑引号。

列名拿到之后,比如叫flag,最后一步就是直接查:

admin' ununionion seselectlect 1,flag,3 frfromom flag#

页面上回显出来的那串flag{...},就是我们要的答案。整个链条是“库名 → 表名 → 列名 → 数据”,一层层往下剥,逻辑非常线性,这也是联合查询注入最舒服的地方——有回显,所见即所得。

3.4 把整个过程串成可复现的操作清单

为了让你照着就能跑,我把关键 payload 按顺序列成一张流程表,每一步都标了目的和需要注意的变形点:

步骤payload(username 字段)目的变形重点
1admin'#确认闭合与注释#--+
2admin' anandd 1=1#验证双写可用andanandd
3admin' oorrdreer bbyy 3#探测字段数orderoorrdreer
4admin' 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_schema=database()#取表名from/information/where
7admin' ununionion seselectlect 1,group_concat(column_name),3 frfromom infoorrmation_schema.columns whwhereere table_name='flag'#取列名同上,注意引号
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,再自动发请求、正则提取回显。我自己的习惯是把变形规则做成一个字典,unionselectfromwhereandor这些一一对应,脚本里遍历替换一遍就行。不过要记得,or的替换顺序得放在最后,否则可能把infoorrmation里已经双写好的or又处理一遍,反而弄巧成拙。这种细节只有自己写过一遍才会懂。

最后再分享一个我个人的体会。这类“过滤 + 绕过”的题,考的不是你背了多少 payload,而是你对字符串处理逻辑的理解。看到关键字被替换,第一反应应该是“它替换几次、按什么顺序替换、替换完会不会产生新的关键字”,把这几个问题想通了,双写、内联注释、等价替换全都是一回事。BabySQL 只是让你在最低的难度上把这套思维建立起来,后面碰上更狠的过滤,比如把unionselect全换成星号、把空格彻底删掉的题,你照样能顺着这个思路一点点试出来。做完这道题,我建议你顺手回去把 [极客大挑战 2019]LoveSQL 也拿来对比着看,两道题的过滤强度不一样,放在一起做,对“过滤与绕过”的理解会一下子立体很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询