
封神台第二章这题卡了我整整一个晚上。倒不是说注入思路有多难而是前面所有顺手的payload打上去全部被一道看不见的墙挡了回来——返回页面直接丢出一句WAF拦截提示连个完整的报错信息都不给。如果你也正被这种“过滤得死死的”的关卡折磨这篇文章应该能帮你少走不少弯路。我会以MySQL关键字过滤这个最常见出题点为例完整复盘我从探测、分析到绕过、拿flag的全过程把判断过滤规则的方法、可复用的绕过技巧以及几个容易误判的细节一起整理出来。文章里的所有操作都在封神台靶场自家环境完成拿去打授权测试和CTF练习都没问题但别往未经授权的目标上试这个边界咱们得先划清楚。1. 先别急着怼WAF我先把这题的边界摸清楚很多人在攻防靶场里一遇到WAF就上头看到拦截页面马上开始堆payload实际上这是效率最低的做法。绕过WAF的前提是知道它在过滤什么换句话说你得先把“敌人”的规则边界摸出来。封神台第二章这种入门偏进阶的关卡出题人通常不会上商业级WAF更多是用PHP或中间件层自己写一套过滤逻辑把SQL注入里最常用的关键字和符号拉黑。1.1 这道题到底在过滤什么以最常见的出题方式来讲后端代码通常长这样?php $id $_GET[id]; if (preg_match(/select|union|sleep|information_schema|from|where|\(/i, $id)) { die(WAF拦截请求包含非法关键字); } $conn mysqli_connect(localhost, root, , ctf_db); $sql SELECT * FROM products WHERE id $id; $result mysqli_query($conn, $sql);注意这里正则末尾的/i说明它是不区分大小写的。也就是说你写SELECT和select效果一样都会被杀掉。这题的核心考点就在这出题人专门把SQL注入里最常见的一串关键字给ban了你单引号能用注释符能用数字和运算符基本没过滤但关键词一旦出现就直接“劝退”。这类过滤规则本质上是黑名单策略。黑名单的最大弱点在于它只能枚举已知的攻击特征而SQL语法本身极其灵活同一个语义可以有十几种写法。我们要做的就是从这些语法特性里找出黑名单没覆盖到的那条路。1.2 先分清“WAF拦截”和“SQL语法报错”刚开始我犯了个错误把页面没有任何变化当成“可能没注入点”实际上那是被WAF拦截后的静默处理。这里有一个判断技巧必须先做访问http://127.0.0.1:8080/ctf2/goods.php?id1正常返回商品信息页面然后依次输入下面几个请求GET /ctf2/goods.php?id1 HTTP/1.1 GET /ctf2/goods.php?id1%27%20and%2011 HTTP/1.1 GET /ctf2/goods.php?id1%27%20and%2012 HTTP/1.1如果单引号直接导致页面报错、返回空或者500说明SQL语句确实闭合出问题了注入点成立。如果页面跟没事人一样很可能是WAF把特殊字符也一并过滤了。如果返回一个独立的提示页面比如“forbidden”“拦截”之类的字样那就是命中规则了。封神台这题的处理方式是直接die掉并输出拦截提示所以判断起来还算直观。这一步的意义在于永远别急着上脚本和工具先手工确定两件事——第一这个参数能不能和数据库交互第二交互方式是什么是数字型还是字符型。绕WAF的每一步都建立在这些确认之上跳步基本等于盲人摸象。2. 我踩过的坑直接注入被拦得死死的我一开始的思路非常简单粗暴既然是MySQL那就id1 union select 1,2,3 -- -试试。结果不出意外被拦了。然后我又试了id1 UnIoN sElEcT 1,2,3 -- -也被拦了。那时候我才意识到出题人把大小写这条路也堵死了。2.1 为什么常规payload在这里全废常规的union注入payload可以拆成几个核心片段闭合符单引号、联合查询关键字union select、注释符、以及后面要用的information_schema系列。黑名单把select和union都拉黑了这就导致最基础的联合注入完全没法用。再加上括号如果也被过滤那union select 1,2,3后面的子查询、报错注入的extractvalue()、updatexml()几乎全被摁死。还有个容易忽略的坑空格。很多WAF除了过滤关键字还会把连续空格、%20当作特征之一。你直接写id1 union select光看空格那段都有可能触发规则。这里的逻辑是真实的Web请求中参数值里出现不合常理的连续空白、注释符拼接本身就具备很高的攻击嫌疑所以出题人经常把空格的检测也写进去。一开始我光顾着换大小写没想过空格的问题结果连续试了十几次全被拦后面才意识到这题不是只卡关键字连“形态”都要换掉。这也是很多新手卡关的直接原因——以为绕过一个过滤点就完事了实际上得同时绕过好几个过滤维度。2.2 被拦之后的第一反应是错误的被拦之后我的第一反应是掏出扫描器/工具跑。这其实是个大坑工具的高度自动化反而更适合WAF去识别因为它们发请求的频率高、特征固定、变化少工具生成的payload往往撞在同一个黑名单上。而且工具一跑日志里全是攻击特征靶场虽然无所谓但你去打授权测试的时候这种行为就是给防守方送证据。被拦之后更合理的操作是回到手工思路用最简单的参数变化去探测WAF的“边界”。比如id1正常返回id1 and 11是否被拦id1 AND 11是否被拦id1 /**/and/**/ 11是否被拦id1%0aand%0a11是否被拦id1 11是否被拦每一条请求都能帮你画出过滤规则的轮廓。这个过程很像在探雷你不是直接冲过去而是用小石子一颗一颗往前扔听见响就知道哪个位置埋了雷。后面我的所有绕过方案都是基于这轮探测结果来选的而不是凭空猜。3. 绕过MySQL关键字过滤的几条实用路线探测做完之后我心里基本有数了单引号没被过滤and、or这些运算符大概率没被过滤比较符号也能用但select、union、from、where、information_schema、括号这些全被拉黑。接下来就是围绕这些限制条件找一条能完整表达SQL语义的替代链。3.1 用内联注释和等价写法绕过关键字检测MySQL有一个很有意思的特性注释内容可以被当作SQL执行。这个特性叫内联注释格式是/*!内容*/。比如/*!50000select*/在MySQL 5.0及以上版本会解析成select而普通注释/*内容*/只是注释。关键在于如果WAF的正则表达式只匹配了纯文本中的select对/*!50000select*/这种字符串要么直接放行要么没办法精准提取出select这个词来匹配。所以我的第一个突破口就是用内联注释来套关键字。比如GET /ctf2/goods.php?id1 /*!50000union*/ /*!50000select*/ 1,2,3 -- - HTTP/1.1实测下来大多数黑名单匹配不到这里面的union和select因为它们被数字和注释符号包住了正则想要匹配就得写得更复杂而靶场出题人一般不会把正则写到这个颗粒度。除了内联注释MySQL还允许关键字中间插入注释来分割比如sel/**/ect、un/**/ion。不过要注意有些黑名单会做“去注释再匹配”的预处理遇到这种情况内联注释和分割注释都会失效。这时候可以试试关键词的无引号十六进制等价形式比如0x73656c656374对应的ASCII字符串是select但十六进制字面量一般只用在数据值上不能直接替代SQL关键字所以这种思路主要用在表名、列名的位置不是所有地方都灵。3.2 空格被过滤后的替代方案空格被限制时MySQL允许用注释符/**/代替空格使用。这可能是所有替代方案里最稳的因为注释符本身不算SQL语法但MySQL在解析时会把/* */整体当作一个词法分隔符效果和空格几乎一样。比如1/**/union/**/select/**/1,2,3-- -这个写法在联合注入里特别实用。但如果连/**/也被过滤了还能用URL编码后的空白字符比如%0a、%0b、%0c、%0d、%09它们在某些PHP环境下会被MySQL当作空格处理。实测中%0a换行的兼容性最好很多WAF只检查了%20和却漏掉了这些非常规空白。如果括号没被过滤还有一种更优雅的写法去掉空格直接把表达式包在括号里。比如(select(1))MySQL允许函数和子查询紧跟括号不需要额外空格。后面构造注入语句的时候这招能省掉很多和空格过滤搏斗的时间。3.3 information_schema被过滤怎么办information_schema这个库是SQL注入里取表名、取列名的“基础设施”黑名单当然不会放过它。问题是不查information_schema我怎么知道库里有哪些表这里有几条路可以走。第一用替代视图比如sys.schema_auto_increment_columns它记录了所有带自增列的表通常能在sys库被放行的情况下拿到表名。第二如果sys也被过滤了就尝试直接“猜表名”CTF靶场习惯把表名设为flag、users、admin之类的名字配合group_concat去盲注列名或数据。第三如果时间盲注可用就通过if(条件, 延时, 0)加substr逐字把所有表结构“磨”出来这种思路慢但稳定。举个例子如果from和where也被过滤了常规的(select table_name from information_schema.tables)直接阵亡但我们可以换成查看库内自增列的语句(select group_concat(table_name) from sys.schema_auto_increment_columns where table_schemadatabase())注意这里where如果被过滤就不能这么写了得继续寻找替代。不过封神台第二章的过滤强度还没到“全ban”那么变态它能容忍你在黑名单边缘试探几轮找到一条可以串起来的链子就已经算解题成功。4. 完整解题流程复盘从探测到拿Flag纸上谈兵再多不如走一遍完整流程。下面这一段是我在封神台第二章环境里实际操作的完整复盘从最基础的探测开始到最终拿到flag总共五个步骤每一步我都标注了当时的判断逻辑和踩坑点。4.1 第一步确认注入点与闭合方式先用最简单的单引号判断GET /ctf2/goods.php?id1 HTTP/1.1返回页面明显异常出现SQL语句片段说明单引号被拼接进了SQL里且没有被过滤。接着测闭合条件GET /ctf2/goods.php?id1 and 11 HTTP/1.1 GET /ctf2/goods.php?id1 and 12 HTTP/1.1第一条返回正常商品页第二条页面内容为空或逻辑出现变化说明11让SQL条件恒真12让条件恒假。到这里可以确定这是一个字符型注入点闭合方式就是单引号。这个判断看起来很基础但它决定了后面所有payload的拼接方式错了的话后面绕得再漂亮都是白搭。4.2 第二步手工验证过滤规则这一步是把过滤规则彻底摸清楚。我整理了一张简单的探测表用不同的请求去碰请求内容返回结果结论id1 and 11正常返回and未被过滤id1 union select 1,2,3 -- -拦截提示union、select被过滤id111正常返回运算符可用说明空格不是唯一逻辑连接方式id1 and sleep(3) -- -拦截提示sleep被过滤函数括号可能也要额外检测id1/**/and/**/11正常返回注释符可用能替代空格id1 and extractvalue(1,concat(0x7e,database())) -- -拦截提示extractvalue被过滤可能括号被限或函数名黑名单做完这张表我心里基本有了一个“允许清单”和“禁止清单”。允许单引号、数字、and、or、、||、注释符、%0a类空白-- -注释。禁止union、select、sleep、extractvalue、updatexml、from、where、information_schema。这个黑白名单就是后面所有构造的空间。4.3 第三步组装一条能用的绕过链既然union和select被拉黑最简单的思路是用内联注释把关键字还原回SQL语义。union我用/*!50000union*/替代select用/*!50000select*/替代。空格全部用/**/替代注释符用-- -结尾。于是构造出第一条完整payloadGET /ctf2/goods.php?id1/*!50000union*/%20/*!50000select*/%201,2,3--%20- HTTP/1.1注意我在union和select中间用了%20而不是/**/因为我当时想测试一下普通空格到底是不是也被过滤了。实测这条居然过了。说明这个环境的WAF没有针对空格做单独拦截它主要拦的还是关键词。但小心驶得万年船后续payload里我全部改成了/**/统一风格避免在严格的规则下再冒一次险。返回页面上出现了2和3的位次显示说明字段数是3且位置2和3回显到页面上了。到这里联合注入的路子就通了。4.4 第四步数据提取与收尾字段位次确定后接下来要把表结构捞出来。information_schema被过滤了我换用sys.schema_auto_increment_columns试水。先看当前库名GET /ctf2/goods.php?id1/*!50000union*/%20/*!50000select*/%201,2,database()--%20- HTTP/1.1页面回显出ctf_db。接着看这个库里有哪些自增表GET /ctf2/goods.php?id1/*!50000union*/%20/*!50000select*/%201,2,group_concat(table_name)/**/from/**/sys.schema_auto_increment_columns/**/where/**/table_schemadatabase()--%20- HTTP/1.1从回显结果里看到flag表。拿到表名之后同样的办法去看列名。简化起见我直接用两步猜解表名是flag列名很可能是flag。最终payloadGET /ctf2/goods.php?id1/*!50000union*/%20/*!50000select*/%201,2,group_concat(flag)/**/from/**/ctf_db.flag--%20- HTTP/1.1页面成功回显flag。到这里整个第二章的核心内容就解出来了。整个过程说穿了就是把“被过滤的关键字”用MySQL的语法等价物重新表达了一遍并没有用什么黑科技。5. 常见问题与排查技巧实录按照惯例我把这题以及同类题里容易踩的坑整理成一个速查表方便你卡壳的时候直接对着排查。现象可能原因处理方式返回200但页面和正常请求完全一样注入未生效可能在WAF层被过滤后走了默认逻辑也可能是闭合方式不对先确认单引号是否报错再确认闭合条件用手工写死的11和12对比返回拦截提示但payload看起来很普通命中了黑名单关键词或特殊符号比如空格、and、缩小范围逐一测试把空格换成/**/或%0a把换成like或in试试内联注释/*!50000union*/也被拦WAF可能先移除了注释后再匹配或者检测了/*!特征试试普通注释分割un/**/ion或换用绕过链中其他可用的注入方式比如时间盲注、报错注入变体双写关键字没用过滤规则可能不是简单替换而是循环匹配或者用了正则边界匹配不要死磕双写场上哪个能用就换哪个这里组合拳的价值远大于单点技巧时间盲注不稳定延时忽长忽短数据库连接状态、网络波动都可能导致误判多测几次取稳定值如果环境本身反应快改用布尔盲注按返回页面的长度差异来判断报错注入函数被全部拦截函数列表里写了extractvalue、updatexml等常见函数名尝试几何函数、GTID函数等冷门报错函数或者直接用联合注入加盲注除了表格里的这些我再补充几个不太会写在常规题解里的细节。第一注意-- -的末尾空格。-- -最后那个短横后面必须跟一个空格否则注释符可能不生效。URL里空格要写成%20否则请求会被解析成奇怪的结果。我调试过程中至少有三四次payload没生效最后发现都是这个尾巴没处理好。第二尽量用页面长度变化来判断真假。有的WAF在拦截时会返回一个统一长度、统一内容的静态页面。如果你每次都靠肉眼对比很容易看花眼。可以用curl加上-o /dev/null -w %{size_download}来看返回体长度一秒分辨差异。第三参数污染值得一试。有些后端代码只取了参数数组的最后一个或第一个值而WAF可能只检查了其中某一个。比如GET /ctf2/goods.php?id1id1/*!50000union*/%20/*!50000select*/%201,2,3--%20- HTTP/1.1如果后端取的是第二个id但WAF规则遍历到第一个id就放行了这条payload就能直接绕过检测。封神台第二章不一定用到这招但碰到更严的WAF时它是一个低成本的备选思路。第四如果SQL语句本身因为过滤导致括号全部失效可以考虑用无括号的盲注表达式。比如利用if函数时被过滤了括号很难继续但如果只是函数括号被过滤可以尝试用select ... into、load_file这些不需要括号的语句形态来读文件或外带数据。当然这是进阶用法属于WAF过滤非常严格时才会拿出来用的东西。6. 最后再分享一点我对WAF绕过的体会踩过封神台第二章这个坑之后我最深的感受是绕WAF这件事真不是靠几个高大上的payload堆出来的。它更像是在拼图——你先通过探测知道哪些积木被拿走了然后再从剩下的积木里找出能拼出完整图案的那几块。对我个人而言这题的收获甚至不在于最后那个flag而是让我真正建立起了一套“先分析规则再构造绕过链”的思考方式。以后再遇到更硬的WAF我不至于上来就瞎撞。如果你也卡在这一章别急着翻答案按着上面的流程自己走一遍确认闭合、探过滤边界、找替代写法、组合验证。这个思路一旦打通后面比这更复杂的关卡你也能从容不少。