没记错的话这道题在攻防世界的Web区里算是一道很经典的看着简单、入口明确、但过滤能把人卡死的题目。页面干净得离谱一个输入框参数名叫inject摆明了就是让你注。但真上手以后你会发现前面几步走得很顺等你习惯性地把union select打上去一整条路直接给你焊死。这篇writeup不打算只给一串payload我会把每一步的判断依据、中途踩过的坑、以及为什么最终能绕过去的原理都摊开讲清楚。适合正在刷Web题、或者想把SQL注入绕过思路彻底理一遍的选手参考。1. 开场三连测确认注入类型和注入点1.1 先看页面和参数再做黑盒试探打开靶机以后页面就是一个输入框提交之后URL里会出现?injectxxx。没有源码提示也没有任何前端注释偷藏线索所以这就是一个标准的黑盒注入题。我的习惯是拿到这种单参数页面以后先做三个最基础的请求把注入类型摸清楚?inject1 ?inject1 ?inject1 --三次请求的结果非常典型1页面正常返回一行数据看起来像某个ID对应的记录1页面直接报SQL语法错误说明单引号被拼进SQL并且打破了原本的语句结构1 --恢复正常说明用了单引号闭合注释符把后面多余的内容吃掉了。到这里基本可以断定后端SQL是字符串拼接我的输入被直接放进了一个带引号的查询里。大概率是类似这种结构select id,data from words where id$inject这个猜测后面非常重要。因为整个题的第二条路——换表名让后台自己查——就是建立在后台查询逻辑写死了words表这个前提上的先记着。1.2 测列数order by 是唯一没有第一时间被拦的东西确定是字符型注入之后下一步自然是猜列数。用order by逐项试?inject1 order by 1 -- ?inject1 order by 2 -- ?inject1 order by 3 --结果order by 1和order by 2都正常order by 3直接报错。说明查询结果只有两列。这个信息也很有用因为两列结构通常对应id, data或者id, flag之类的简单表。到这里为止一切看起来都是标准流程。我也很自然地准备上联合查询了然后就被教育了。2. 联合查询当场报废一条 preg_match 挡住半张地图2.1 union select 第一次被拦时的完整报错当我往注入点里提交?inject1 union select 1,2 --页面返回的不是SQL错误而是出现了类似return preg_match(/select|union|from|where|order|by|and|or/i, $inject);这条提示等于是把过滤规则直接甩你脸上了后端用preg_match做了关键词黑名单select、union、from、where这些查询类关键词全被拉黑而且带/i修饰符大小写绕过直接作废。第一次看到这种提示反而安心因为至少知道过滤在哪、过滤了什么。接下来要做的就是拿不同姿势去试边界。2.2 常规绕过轮番试一遍全灭我先把平时常用的几种绕过方式都丢进去试了一圈结果如下尝试方式示例结果大小写混写UnIoN SeLeCt被拦因为/i忽略大小写内联注释/*!50000union*/ select被拦注释没法绕过正则关键词匹配空格替换为tabunion%09select被拦正则不看空白字符注释替代空格union/**/select被拦union和select本身还在编码绕过%55nion无效服务端不一定解码两次结论很明确这题的过滤不是针对某个特定payload而是把union和select这两个查询链条上的核心词全毙了。联合查询这条路在这道题里不是难走是直接锁死。2.3 顺手把没被过滤的词也摸一遍既然有黑名单那就顺便把所有可能用到的SQL关键词都过了一遍。排查结果大概是select、union、from、where、order、by、and、or全部被过滤show、set、prepare、execute、rename、alter、handler、update没被过滤这个差异是决定性的。它意味着联合查询和直接select被堵死但管理类、预处理类、存储引擎类的SQL语句仍然可以自由使用。这为后面的堆叠注入和预处理语句绕过提供了完整的前提。3. 堆叠注入分号打开的另一个世界3.1 为什么联合查询死了堆叠注入还能活很多Web安全入门教程会把注入局限在在一条查询里改变它的逻辑——比如用union select追加查询用or 11改变判断条件。这种方式无论怎么变形最终都受限于当前这一条SQL语句的语法结构。堆叠注入是另一条路它利用的是数据库协议允许一次提交多条语句的特性。你输入1;show tables;--后端执行的时候就变成了select id,data from words where id1;show tables;--分号把前一条查询结束掉show tables成了一整条全新的独立语句。只要后端没有限制连接会话的多语句执行权限堆叠注入就能绕开所有基于单查询结构的过滤。打个不严谨的比方联合查询相当于你在被审查的申请单里夹带私货关键词被拦就夹不了堆叠注入则是把申请单撕了直接另写一张纸递给窗口。preg_match过滤的是纸上的关键词它管不到MySQL解析器后面执行什么。3.2 用 show 拿库名和表名先确认堆叠注入可用?inject1;show databases;--正常返回能看到当前库supersqli以及其他系统库。这就说明MySQL执行了分号后面的语句。继续?inject1;show tables;--结果里出现两个表words 191981093111451514看到这张表名的时候我的想法是名字都不装了。一个是标准的words一个是纯数字乱命名flag大概率在191981093111451514这张表里。3.3 新问题堆叠只能执行不能直接读知道表名之后第一反应自然是?inject1;select * from 191981093111451514;--结果仍然被preg_match拦下。注意这里是个容易迷惑人的点堆叠注入能执行新语句但新语句的内容照样要被过滤器检查。你只是绕过了单查询结构的限制并没有绕过关键词黑名单的限制。所以这题的下一个核心问题变成了在不能直接写出 select 关键词的情况下如何让MySQL执行一条查询。这时浮现出两条路一条是预处理语句一条是让后台替我们查。4. 预处理语句绕过把select藏进字符串再让它复活4.1 PREPARE 怎么就成了绕过利器MySQL的预处理语句语法是这样PREPARE stmt_name FROM 任意SQL字符串; EXECUTE stmt_name;PREPARE接收的是一个字符串这个字符串可以由变量拼接而来也可以直接写字面量。关键在于PREPARE本身不是查询它只是把字符串编译成SQL真正的执行发生在EXECUTE。过滤器的正则扫描的是你发送的整个HTTP请求文本它会看到PREPARE、EXECUTE、CONCAT这些词但看不到一个完整的select查询语句。比如这样的payload?inject1;PREPARE s from concat(select * from 191981093111451514);EXECUTE s;--在正则扫描时select并没有作为一个SQL关键词出现在注入点里它只是concat()函数的一个字符串参数。MySQL拿到之后由预处理机制在服务器内部解析并执行过滤器管不到这一步。4.2 最终的干净payload与执行结果我最后打通的payload长这样?inject1;PREPARE s from concat(select * from 191981093111451514);EXECUTE s;--提交之后页面把191981093111451514表的内容完整显示出来flag就在其中。几个细节值得记住纯数字表名必须用反引号包起来。如果不加反引号MySQL会把191981093111451514当成一个数字字面量而不是表名直接报语法错误。concat()在这里不是必须的直接用PREPARE s from select * from ...也能过因为字符串字面量里的内容不触发正则。用concat()的主要好处是后续可以接变量灵活度更高。注释符必须生效。--在URL里实际是把加号解析成空格注释符能吞掉后面遗留的单引号和SQL片段保证EXECUTE之后没有多余的尾巴。4.3 另一种预处理写法十六进制藏select如果担心字符串里出现select字面量也会被某个更严格的正则抓走可以用十六进制编码绕?inject1;set a0x73656c656374202a2066726f6d206031393139383130393331313134353135313460;PREPARE s from a;EXECUTE s;--0x73656c656374...是select * from ...的十六进制表示。这样整个请求文本里连select这四个连续字母都不存在了属于更彻底的绕过方式。当时我在本地尝试过这种写法直接可用后来在一些其他注入题里也经常靠这一招救命。4.4 这个解法成立的前提堆叠注入 预处理能用依赖两个条件后端连接数据库的账号对当前会话开放了多语句执行能力MySQL版本支持PREPARE/EXECUTE这个语法从MySQL 5.0往后都支持老环境也能用。如果第一个条件不满足比如PHP的mysqli-query()默认就不允许多语句执行需要multi_query才行那堆叠注入直接失效预处理也就无从而起。这题的环境显然是允许的。5. 不用select也能读表换表名让后台替我干活5.1 思路后台那句写死的查询才是最好的工具在做题过程中我想到了第二条路而且这条路的思路很值得玩味。还记得开场判断的后台SQL吗select id,data from words where id$inject后台是直接把表名写在代码里的。那么问题来了我们能不能不查191981093111451514而是把191981093111451514变成words这样后台那句写死的select ... from words就在替我们干活根本不需要注入select关键词了。这个思路的本质是注入点不只是用来拼接SQL语句的它还能用来操作数据库对象本身。当查询关键词被过滤干净的时候对象级别的操作往往能绕出一个新天地。5.2 三步交换表名实际操作需要三步?inject1;rename table words to word;rename table 191981093111451514 to words;--先执行?inject1;alter table words change id id varchar(100) character set utf8;--再提交一个恒真条件?inject1or 11最终效果select id,data from words where id1 or 11where条件恒真后台会把words表的第一行记录返回而这时的words实际上就是原来的191981093111451514表flag直接输出。5.3 为什么要执行alter table很多人做到rename那两步就去提交了结果报字段不认识。原因在于新表191981093111451514的字段名和原words表不一样。原后台查询写的是select id,data from words如果新words表里没有叫id和data的字段查询执行就会出错。所以alter table ... change id id varchar(100)的核心目的是保证换表之后查询仍能成立把字段名和类型改成后台需要的形状。这一步不是可有可无的是整套逻辑的闭合点。5.4 两种解法的定位差异到这里这题其实有两条可达路径了我整理了一下它们的定位对比维度预处理语句解法换表名解法核心操作PREPARE EXECUTERENAME ALTER依赖条件堆叠注入可用堆叠注入可用 后台查询表名可预测过滤绕过点关键词藏在字符串里根本不出现查询关键词通用性高换一道题也能套低依赖具体后台SQL结构踩坑点纯数字表名要反引号换表后字段名必须对齐个人做这类题的经验是两条路都走一遍很有价值。预处理解法更通用换表名解法更考验对整个查询逻辑的掌控力。实际出题人想考的往往是后者因为它的绕法更贴近真实数据库的攻击面。6. 从这道题里提炼的注入绕过思路6.1 过滤探测别只盯关键词要盯语句类型这道题最值得复盘的一点是preg_match过滤了select/union/from/where看起来封锁面很全但show、prepare、rename这些非查询类关键词全部漏掉了。这提醒我做过滤绕过时不要只在关键词列表里打转要往语法树上去想。哪些语句类型能读取数据select、show、handler、prepareexecute、load_file……当一类被锁死的时候剩下好几类可能还活着。MySQL的HANDLER语句也是一条没被过滤的备选HANDLER 191981093111451514 OPEN; HANDLER 191981093111451514 READ FIRST;它以存储引擎接口的方式直接读取表数据和select完全无关。我在本地环境试过这个思路部分MySQL版本可用。如果在赛场上PREPARE/EXECUTE的路也被断了HANDLER值得一试。6.2 注意反引号这个逃生通道这道题里纯数字表名差点坑到我。表名是191981093111451514如果直接写进SQLMySQL会把它当成数字字面量什么也查不出来。必须加反引号才能被识别为标识符。这个细节在平时写项目SQL时几乎不会遇到但在CTF的拼凑式payload里极其常见。凡是目标表名带特殊字符、纯数字、或者和函数名重名都要第一时间想到用反引号包裹。6.3 preg_match的看得见与看不见这题的过滤器是直接返回了报错信息所以探测成本很低。遇到不返回过滤信息的题判断过滤规则就需要靠盲试和响应差异对比了难度上一个台阶。但不管过滤器藏不藏有一个规律是不变的它一定基于文本扫描。那么绕过思路就可以统一归结为——让目标关键词不以明文关键词的形式出现在请求文本里。常见手段有十六进制编码、字符串拼接、变量存储、注释符切割。掌握这一个原则能覆盖大多数关键词过滤场景。7. 复现建议和几个容易踩的坑7.1 环境准备这题可以直接在攻防世界的在线环境里复现不需要本地搭建。如果要在本地自建一个模拟环境建议用PHP 5.x MySQL 5.x 的组合因为新版PHP和MySQL在多语句执行和报错细节上会有差异。我在本地复现时用的就是经典的 PHP MySQL 组合payload基本可以直接迁移。7.2 提交payload时的细节用浏览器直接在URL里提交时注意几个细节--里的在URL中是空格的意思。如果直接用Burp或Python请求可以写成--也就是两个减号加一个空格效果相同。如果提交后页面空白或报错优先检查表名的反引号有没有遗漏。堆叠注入中间的分号必须和后面的语句连在一段URL里不要被URL编码弄丢。7.3 实际做题时的时间分配建议我在复现时先走的PREPARE/EXECUTE一条payload三分钟解决战斗。后来为了验证思路才去试了rename表名路线。建议做题时先上堆叠注入探测确认可用后直接预处理语句拿flag时间充裕再试其他解法。因为换表名那套流程稍长步骤一多出错率也高赛场上时间是硬成本。另外多说一句这类Web题的Writeup重点看的不是那串payload本身而是它背后的决策树。为什么联合查询失败后马上转堆叠为什么堆叠可行但select还被拦为什么最终落在PREPARE每一步都是对SQL执行机制理解的产物。把这条决策链吃透比背一百个payload都管用。