做CTF有一段时间的人对BUUCTF平台应该都不陌生。上面这道《强网杯 2019》的“随便注”基本是Web方向必刷的经典题名字起得很随意做起来可一点都不随意。我记得第一次碰到它是在比赛结束后的复现阶段当时还在“注入”这个知识点里打转一上来就被过滤规则按在地上摩擦后来把绕过方式捋清楚之后才发现这题把字符型注入、堆叠注入、MySQL预处理、黑名单绕过这些点全串起来了非常适合用来检验自己的注入基本功。这道题的目标很直白通过输入框构造注入语句把数据库里的flag拿出来。它适合两种人学习一种是刚学完SQL注入基础、想找个综合题目练手的Web新手另一种是看了一堆WriteUp但没搞清楚“为什么能绕过”的选手。这篇文章我从信息收集、注入点判断、堆叠注入到三种不同绕过姿势完整复盘一遍并把做题过程中容易踩的坑都标出来希望能帮你少走点弯路。1. 初识题目别被“随便”骗了1.1 这道题到底考什么先说结论“随便注”表面看起来就是一个带输入框的SQL注入靶场背后却藏了好几个考点而且每个考点都是环环相扣的。如果你只是会简单的 or 11 --到这里大概率连flag的影子都摸不到。这道题的完整考点链条可以梳理成一张表考点具体体现难度评估字符型注入判断输入1触发SQL语法报错入门基础字段数量判断利用order by确定查询列数入门基础经典union注入被黑名单拦截必须换思路中等堆叠注入用分号分隔执行多条SQL语句中等偏上黑名单绕过select被过滤需借助MySQL特性核心难点数据读取预处理语句、Handler、改表名核心难点这个考点的设计其实非常“有心机”。它没有把过滤做得很复杂但却精准卡死了最常见的方法逼着你去了解MySQL本身的一些高级特性。我第一次做的时候卡在了select被过滤的地方脑子里全是SQL注入常规流程完全没想到还能有别的路后来查资料才反应过来这题要的不是“你会注入”而是“你懂MySQL”。1.2 页面形态与后端逻辑还原打开靶机页面非常简洁一行欢迎文字加一个输入框、一个提交按钮。输入1提交页面会正常输出一条查询结果输入一个英文单引号页面立刻弹出类似这样的SQL报错SQLi query error: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 1 limit 0,1 at line 1报错信息这个东西在CTF里就是最好的信息泄露途径。从near 1 limit 0,1这里我反推出后端实际执行的SQL大概率长这样select * from words where id $input limit 0,1;关键信息有三个第一这是字符串型拼接单引号是闭合点第二查询末尾带limit 0,1只取一行但这对后续利用影响不大第三表名是words页面展示的数据来自这个表。这三点先记在心里后面每一步判断都靠它们。很多新手看到报错就慌其实报错信息是给你递话呢。顺着报错反推SQL语句结构是Web题基本功这个习惯从入门就要养成。1.3 摸清过滤规则做注入题拿到输入框之后除了测闭合方式最重要的就是测过滤规则。所谓“知己知彼百战不殆”你连对面拦什么都不知道绕个锤子。我尝试提交1 union select 1,2 --页面返回了一行PHP代码提示return preg_match(/select|update|delete|drop|insert|where|\./i,$inject);这道题的黑名单直接写在报错里了简直是送分信息。从正则看它过滤了这些SQL关键词和一个点号selectupdatedeletedropinsertwhere.点号preg_match后面的i表示大小写不敏感所以SeLeCt、SELECT这种写法也过不去。不过大家注意这个名单里并没有show、set、prepare、execute、handler、alter这些后面全成了突破口。这里我多说一句CTF里的WAF过滤规则从来都不是为了做到天衣无缝而是给你留一条“设计好的弯道”。你要做的不是硬刚正则而是顺着它没拦住的点绕过去这才是这类题的精髓。2. 注入探测从入门测试到字段判断2.1 单引号探针与注入类型判定先来最基础的判断。我按顺序提交了这几组数据1 1 1 --执行结果分别是正常显示、SQL报错、恢复正常。这个组合非常典型几乎可以确定后端存在字符型注入因为单引号破坏了原来的字符串闭合后面的内容顶到了语法层。1 --能恢复正常说明注释符把末尾的limit 0,1和多余单引号注释掉之后SQL语句语法正确了。为了进一步确认再用逻辑恒真恒假测试一下1 and 11 -- 1 and 12 --11为真时页面有数据12为假时页面无数据。到这里已经实锤了存在注入而且是where id后面这个位置。顺便说明一下这里的注释符我习惯用--是MySQL里--加一个空格的意思。在URL传输时号会被当成空格所以很多WriteUp里都写--。如果是在Burp Suite里发数据包直接把空格写成%20也行或者用#并URL编码成%23效果一样。2.2 order by确定字段数量确认注入点后第二件事就是测字段数。因为不知道查询返回几列后续不管是union还是数据输出都会很被动所以先用order by粗暴排个序1 order by 1 -- 1 order by 2 -- 1 order by 3 --前两个都能正常返回第三个直接报错说明原查询只有2列。这一步得到的“2列”非常关键因为后面一旦要构造union或者想通过其他方式把数据映射到页面展示位都得知道这个数。有些文章把order by判断字段数当成默认技能但我还是建议新手把这个过程当作“仪式感”来做一遍因为有些题在order by上也会做文章比如过滤数字或过滤order实际场景中你还是要随机应变。2.3 union注入撞墙知道哪里死了字段数知道了正常人的第一反应就是尝试union联合注入1 union select 1,2 --结果被弹了过滤提示问题出在select上。有人可能会想那我用大小写绕过改成Union SeLeCt试试但前面说了正则有i修饰符大小写不敏感这条路直接堵死。那还能不能继续用union类的方式其实也不是完全没有变通余地比如可以尝试用union不带select看反应或者用注释拆分关键字。但在没有其他过滤干扰的情况下这道题直接放弃了union路线转而考虑另一类更有价值的注入方式堆叠注入。为什么字段数还是重要的因为就算走堆叠注入后面看几个表、几条语句的结果也得心里有数。而且万一别的思路需要拼接一个查询结果集字段数就是你构造语句的地基。3. 堆叠注入show语句打开局面3.1 堆叠注入原理与适用条件堆叠注入英文叫Stacked injection核心就是利用SQL语句用分号;分隔的特性在原本的注入点后面再追加一条或多条完整的SQL语句让数据库一次性执行。传统union注入是把额外结果拼到原查询里本质上还是“一条查询”堆叠注入则是“执行多条语句”灵活性完全不是一个级别。比如你可以在后面直接执行show tables、set 变量、prepare、alter table、handler等等。但堆叠注入有一个前提后端驱动必须支持多语句执行。PHP的mysqli_multi_query可以PDO默认不一定开mysql_query干脆不行。这道题能走通堆叠注入说明后端支持这个特性这也正是题目设计给出来的“正路”。3.2 用show语句拿到表名与字段名既然select被过滤那先不读数据先用show看结构。提交1;show tables; --页面成功返回了两个表名1919810931114514 wordswords很好理解是页面原本查询的表1919810931114514这个名字一看就不是正经业务表flag大概率就藏在里面。继续看字段。先看words表1;show columns from words; --返回两个字段id、data。再看目标表1;show columns from 1919810931114514; --这里有个细节表名以数字开头MySQL里直接写表名会当作数字常量导致语法错误所以必须用反引号把整个表名包起来。返回结果是一个字段flag。到这一步信息收集完成。目标明确从1919810931114514表把flag字段取出来。问题也很明确所有读数据的常规语句基本都带select而select被过滤了。怎么办接下来就是这道题真正的核心章节。4. select被过滤后的三条出路4.1 预处理语句拼接绕过我用的第一个成熟解法是MySQL预处理语句。MySQL里可以通过PREPARE、EXECUTE把一段字符串当作SQL语句来执行类似于动态SQL。这个特性最妙的地方在于黑名单只盯着你提交的原始字符串你完全可以不写select这个词而是运行时再拼出来。核心payload如下1;set sqlconcat(sel,ect * from 1919810931114514);prepare stmt from sql;execute stmt; --逐段拆开解释set sql...定义一个用户变量sql。concat(sel,ect * from ...)把字符串拆成两段拼接拼出来的完整内容是select * from1919810931114514因为提交的payload里没有出现完整的select单词所以不会触发正则过滤。prepare stmt from sql;把变量里的字符串交给MySQL预编译成一条可执行语句。execute stmt;执行这条预编译的语句结果就是读取目标表数据。这里还有一个升级版思路用十六进制编码绕过。我把完整的select * from \1919810931114514转成十六进制然后直接用十六进制串赋值1;set sql0x73656c656374202a2066726f6d20603139313938313039333131313435313460;prepare stmt from sql;execute stmt; --十六进制串里全是数字和字母黑名单正则一个也匹配不上天然免疫关键词过滤。如果你在环境里遇到过滤了很多符号和关键字的场景这种hex编码方式往往比拆字符串更省事。我在实机复现时用第一种payload就能稳定出flag不过建议两种都试一遍毕竟换个环境过滤规则可能不一样多一种手段多一条活路。4.2 handler直读表数据第二种解法更讲究对MySQL语法的熟悉度。MySQL里有一个日常少见的读表语句HANDLER它不是SQL标准语法但MySQL支持而且它本身不在黑名单里。HANDLER和SELECT最大的区别是它是一个独立的“打开表、逐行读取”接口不需要经过select关键字。payload如下1;handler 1919810931114514 open;handler 1919810931114514 read first; --第一步handler ... open打开表第二步handler ... read first读取第一行数据。目标表只有一行一条flag数据所以读一次就够。试的时候注意一点handler read之后的数据展示可能跟普通查询有一点点区别有的环境需要配合handler close把表关掉或者重新open才能再读。不过在这道题里一条payload下去flag就直接出来了。我后来也把handler这一招用到过一些实战测试环境里。很多WAF对select查得非常勤快但对handler这种冷门关键字常常漏掉凡是遇到MySQL数据库的注入场景这招值得优先想一下。4.3 改表名置换的思路第三种思路最“反直觉”不直接读数据而是把表结构改成“页面原本就查的样子”让后端自己把flag输出出来。相当于我把保险柜里的东西直接搬到别人指定打开的那个抽屉里页面再查的时候查到的就是flag了。完整payload分三步走1;alter table words rename to words2;alter table 1919810931114514 rename to words;alter table words change flag id varchar(100); --第一步把原来的words表改名成words2让它暂时让位。 第二步把存放flag的1919810931114514表改名成words顶替原来的位置。 第三步把新words表里的flag字段改成id字段并调整类型为varchar(100)好让原查询where id...能正常匹配。执行完这三条alter之后再正常提交1 or 11 --这时的执行逻辑就变成了后端原来的SQL查询select * from words where id 1 or 11 limit 0,1查的是被改装过的新words表11恒真直接取出第一行flag就到手了。这个思路最大的价值不是“它也能出flag”而是它给你展示了一个完全不同的视角既然绕不过查询语句那就把数据搬到你已经能查询的地方。在CTF里这叫“换一种思路解决问题”在实战里这叫“绕过拦截的最小改变原则”本质上都是空间换时间。5. 实战避坑与经验贴士5.1 三条路线完整payload一览为了方便复现和对照我把三条路线的完整payload整理成一张表你做题的时候可以直接抄路线完整Payload预处理拼接1;set sqlconcat(sel,ect * from \1919810931114514);prepare stmt from sql;execute stmt; --预处理hex1;set sql0x73656c656374202a2066726f6d20603139313938313039333131313435313460;prepare stmt from sql;execute stmt; --handler直读1;handler \1919810931114514 open;handler 1919810931114514 read first; --改表名置换1;alter table words rename to words2;alter table \1919810931114514 rename to words;alter table words change flag id varchar(100); --后接1 or 11 --这里特别提醒一件事payload里的反引号不能省。无论是1919810931114514这种数字开头的表名还是某些含特殊字符的字段MySQL都会需要反引号来处理。如果你在show columns from 1919810931114514时报错十有八九就是反引号的问题。5.2 做题中最容易踩的四个坑我把这类题常见的坑从头梳理了一遍基本都是我实操时亲身遇到过的。第一注释符选错。--后面必须跟一个空格--里的加号在URL中会被当作空格这个写法在Burp里也能用但如果用--换行或者用#没有URL编码成%23都会导致注释无效后面的引号和limit把整条语句搞炸。建议固定用--简单稳定。第二空格被吃掉。在URL里直接提交1;show tables; --时中间的很多空格如果被URL解析吃掉会直接报语法错误。我习惯先在Burp Suite的Repeater里操作把空格统一编码成%20或者用代替避免踩这个坑。第三堆叠注入语句顺序错了。MySQL预处理里prepare和execute必须在同一个会话里而且变量要先赋值再预编译。有次我把execute stmt写在了prepare stmt from sql前面结果直接报“prepare statement not found”这属于语法顺序的低级错误但新手很容易犯。第四改表名思路一旦失败表结构就乱了。alter操作是不可逆的如果中途某一步写错了原来的words表可能被你改名成words2页面接下来所有查询都会异常。我在本地测试时经常把自己绕晕最后只好重置靶场环境重来。所以用改表名方案时三步alter的每一步都要检查清楚再执行。5.3 从这道题延伸出的经验做完“随便注”这道题我觉得收获最大的不是会了某一条payload而是学会了一种处理注入的思路顺序。以后再遇到被过滤的SQL注入我会先干这几件事第一收集过滤名单精确到哪些关键字和符号被拦第二测一下是否支持堆叠注入这决定了后续能不能用show、set、handler这些非查询类语句第三如果有堆叠就把黑名单和MySQL特性对照一遍看哪些关键字没被拦第四凑齐了关键字之后再考虑数据怎么取、怎么展示。这套流程其实可以迁移到很多Web题上包括BUUCTF里其他类似题和真实渗透测试中的WAF绕过。相比背一个固定payload这种带思路的做题方式会让你走得更远。我个人习惯是在本地用Docker把这道题的环境跑起来改了源码里的过滤规则反复练比如把prepare也加入过滤名单或者把handler给拦掉看看自己还能不能绕过。这种“加难度练习法”对建立MySQL语感特别有帮助你试过就知道真的比对着WriteUp抄一遍有用得多。