WAF绕过实战:协议解析差异与逻辑错配的深度利用
1. 项目概述:当WAF遇上“变态”绕过思路
在Web安全攻防的战场上,WAF(Web应用防火墙)就像一道横亘在攻击者与目标应用之间的坚固城墙。常规的SQL注入、XSS、文件上传等攻击,往往在WAF的规则匹配下被轻松拦截。然而,总有一些场景,一些思路,能够像“奇兵”一样,从意想不到的角度绕过这道防线。今天要聊的,就是两个堪称“变态”的WAF绕过案例,它们并非依赖最新的0day漏洞,而是对协议特性、应用逻辑和WAF规则盲区的极致利用。这类学习对于安全研究者、渗透测试工程师和CTF选手来说,价值巨大,它能极大地拓宽我们对“漏洞利用”和“防御绕过”的认知边界。
提到WAF绕过,很多人会立刻想到各种编码、混淆、等价替换。比如用/**/代替空格,用&&代替AND,或者利用MySQL的/*!50000union*/这种内联注释。这些是基础,是“常规武器”。而“变态”绕过,往往意味着它跳出了对攻击载荷本身的修修补补,转而从请求的上下文环境、服务器与WAF的交互逻辑、甚至是协议解析的差异中寻找突破口。理解这些,不仅能让你在CTF比赛中面对“ctfshow waf绕过”这类题目时游刃有余,更能让你在真实的红队评估中,具备穿透深度防御体系的关键能力。
2. 核心思路拆解:协议层与逻辑层的降维打击
要理解高难度的WAF绕过,首先要明白现代WAF(特别是云WAF或反向代理型WAF)的基本工作原理。通常,流量路径是:用户请求 -> WAF节点 -> 后端真实服务器。WAF节点会解析HTTP请求,应用规则引擎进行匹配,放行无害请求,拦截或清洗恶意请求。这里的“解析”二字,就是第一个可能产生差异的地方。
2.1 思路一:利用协议解析不一致性
WAF和后端服务器(如Nginx、Apache、Tomcat、PHP-FPM)对HTTP协议规范的实现可能存在细微差别。这种差别在正常情况下无关紧要,但在安全检测的“放大镜”下,就可能成为致命的绕过通道。
一个经典的例子是“分块传输编码(Chunked Transfer Encoding)混淆”。HTTP/1.1 允许使用分块编码传输消息体。规范格式是每个数据块前有一个十六进制的大小值,最后以一个大小为0的块结束。有些WAF为了性能,可能不会完全按照规范实现一个“宽容”的解析器,而后端服务器则可能更贴近规范或实现不同。
假设我们有一个注入点:id=1。常规注入id=1 union select 1,2,3会被WAF拦截。那么,我们可以尝试将整个请求体进行分块编码,并在分块数据中插入一些“干扰符”。例如,规范要求块大小后必须跟一个CRLF(\r\n),但有些WAF解析器可能遇到\n就认为块大小声明结束,而后端服务器则严格等待\r\n。攻击者可以故意制造这种差异:
POST /vuln.php HTTP/1.1 ... Transfer-Encoding: chunked 5; \n(这里故意只用换行符) union 5\n(这里用\n分隔) select 3\n 1,2 0\n\n在上面的畸形分块数据中,我在第一个块大小5后面用了; \n(分号和空格加换行),而非规范的\r\n。WAF的解析器可能将5;整体解析为块大小(解析为0),或者因为遇到\n就停止读取块大小,导致其后续对数据块union的解析与预期不符,可能就跳过了检测。而后端服务器如果严格遵循\r\n作为结束,它会继续读取,直到找到\r\n,最终将5; \nunion中的union作为第一个数据块的一部分。这样,恶意载荷就“溜”过去了。
注意:这只是原理性演示。实际利用需要根据目标WAF和后端服务器的具体实现进行Fuzz测试。常用的工具如
Chunked-Encoding-Converter或Burp Suite的插件HTTP Request Smuggler可以帮助我们构造和测试这类载荷。
另一个协议层攻击是“请求走私(HTTP Request Smuggling)”。它利用WAF(作为前端代理)和后端服务器对“Content-Length”和“Transfer-Encoding”这两个头部处理优先级或逻辑的不同,造成请求解析错位,从而使WAF看到一个请求,而后端服务器看到两个(或多个)请求。攻击者可以将恶意载荷隐藏在“第二个”请求中,因为WAF只检测了“第一个”请求。这类攻击对集群环境威胁极大,且绕过效果非常彻底。
2.2 思路二:利用应用逻辑与WAF检测逻辑的错配
这种思路不直接攻击协议,而是“欺骗”WAF的检测逻辑。WAF的规则往往是基于模式匹配的,它试图从请求中找出“看起来像攻击”的字符串或结构。但如果请求的“样子”因为应用的特殊处理而发生了变化,WAF就可能失明。
案例:参数污染与多重解析。假设一个PHP应用同时从$_GET、$_POST和$_REQUEST中获取参数,并且处理逻辑有缺陷。PHP中,$_REQUEST默认包含$_GET和$_POST,且如果有同名参数,其值取决于php.ini中的request_order设置,通常是后到者覆盖先到者。
攻击场景:一个搜索接口,GET /search.php?keyword=test。WAF规则会检测keyword参数中的SQL注入关键词。如果我们这样发送请求:
POST /search.php?keyword=1&keyword=union select 1,2,3--+ Content-Type: application/x-www-form-urlencoded keyword=harmless_value这里发生了什么?我们通过GET传了一个包含注入语句的keyword,又通过POST传了一个无害的keyword。WAF可能会检测到GET参数中的恶意内容并拦截。但是,如果WAF配置只检测$_REQUEST['keyword']的最终值,或者后端应用代码错误地使用了$_POST['keyword'](认为只有POST),而实际业务逻辑却用了$_REQUEST['keyword'](被GET污染了),那么WAF看到的是POST传来的harmless_value(或经过某些处理后的$_REQUEST最终值),从而放行。而后端有缺陷的代码在处理时,可能因为逻辑分支使用了$_GET或错误的参数合并方式,最终执行了GET请求中的恶意载荷。
案例:编码与解码的“套娃”。有些应用框架或自定义逻辑会对用户输入进行多次解码。例如,参数可能先被URL解码,然后被Base64解码,最后才交给SQL查询。WAF通常只对原始请求进行一层或固定层数的解码检测。攻击者可以构造一个经过多重编码的载荷:
原始注入:union select 1,2,3一次URL编码:%75%6e%69%6f%6e%20%73%65%6c%65%63%74%20%31%2c%32%2c%33再将上述结果进行Base64编码:JTI1NzUlMjU2ZSUyNTY5JTI1NmYlMjU2ZSUyNTIwJTI1NzMlMjU2NSUyNTZjJTI1NjMlMjU3NCUyNTIwJTI1MzElMjUyYyUyNTMyJTI1MmMlMjUzMw==
WAF可能只做一次URL解码,看到的是%75%6e...这一串百分号编码,识别不出union。或者做了URL解码但没做Base64解码。而后端应用逻辑却忠实地执行了URL Decode -> Base64 Decode -> 拼接SQL的流程,导致注入成功。
实操心得:挖掘这类绕过的关键在于“差异”。一是通过信息收集(报错信息、响应头、已知框架特征)判断后端技术栈;二是通过Fuzz测试探测WAF的检测边界和容忍度。例如,你可以系统性地测试各种特殊字符(空格、换行、制表符、空字节
%00)、各种编码(URL、HTML、Unicode)、参数位置(URL、Body、Cookie、Header)的检测情况,绘制出WAF的“检测图谱”,从而找到盲区。
3. 实战案例深度剖析:两个“变态”绕过详解
下面,我们结合更具体的场景,拆解两个需要一定脑洞的绕过实例。请注意,这些方法可能针对特定版本的WAF或服务器环境,核心是学习思路。
3.1 案例一:利用HTTP参数污染(HPP)与WAF规则覆盖不全
场景:一个用户登录日志查询功能,后端SQL语句大致为:SELECT * FROM logs WHERE username = ‘$_REQUEST[‘user’]’ AND date > ‘...‘。WAF对user参数进行了严格的SQL注入检测。
绕过过程:
- 观察与推测:通过发送
user=test‘(带单引号)并观察报错,确认存在SQL注入点且为字符型。直接使用union select被WAF拦截。 - 测试参数污染:发送请求
GET /query.php?user=admin‘&user=union select 1,2,3-- -。发现被WAF拦截。说明WAF可能检测了所有同名参数。 - 引入参数分隔符:尝试利用
&和;作为参数分隔符的差异。构造请求:GET /query.php?user=admin‘;user=union select 1,2,3-- -。这里看起来是一个参数user,其值为admin‘;user=union select 1,2,3-- -。但某些Web服务器(如早期Tomcat)允许将;视为参数分隔符。因此,服务器端可能将其解析为两个参数:user=admin‘和user=union select 1,2,3-- -。 - 利用WAF解析差异:关键点在于,WAF的解析器可能不将
;识别为分隔符,而将其视为参数值的一部分。因此,WAF看到的user参数值是一个整体:admin‘;user=union select 1,2,3-- -,这个字符串里没有明显的union select(因为被;user=隔开了,WAF的规则可能不会跨“伪参数名”进行匹配)。然而,后端服务器将其解析为两个参数后,第二条user参数的值union select 1,2,3-- -就被直接拼接进了SQL语句。 - 最终Payload:需要根据数据库类型调整。假设是MySQL,最终请求可能如下:
这个Payload到达后端后,可能被解析为:GET /query.php?user=admin‘;user=union select 1,concat(username,0x7c,password),3 from users-- -- 参数1:
user=admin‘-> 导致SQL语句前半部分为... WHERE username = ‘admin‘‘(多出一个单引号)。 - 参数2:
user=union select 1,concat(username,0x7c,password),3 from users-- --> 这会被当作第二个赋值,但SQL语法已错乱。 实际上,更可能的情况是后端代码逻辑有缺陷,比如用了$_GET[‘user’](只取第一个)或错误地拼接了所有同名参数值。因此,实际测试时需要结合具体代码逻辑调整。例如,如果后端用implode(‘,’, $_GET[‘user’])来处理多个值,那么Payload又需要另一种构造。
- 参数1:
排查技巧:如果参数污染不成功,可以尝试:
- 更换参数分隔符:
&、;、|、\n(换行符,需URL编码为%0a)。- 改变参数位置:将恶意载荷放在第一个参数值,将触发WAF的“诱饵”放在第二个。
- 结合HTTP方法:GET传一个,POST传另一个,利用
$_REQUEST的合并特性。
3.2 案例二:利用容器解析与WAF解析的差异(畸形Boundary)
场景:一个文件上传接口,后端使用Java Spring框架,通过MultipartFile接收文件。WAF会检测文件内容中的恶意字符串(如<?php eval()和文件名中的路径穿越(../)。
绕过过程:
- 常规绕过失败:修改文件后缀、制作图片马、使用
<?=短标签等常规方法均被WAF拦截。 - 分析请求结构:一个标准的多部分表单请求头如下:
正文中,每个部分以Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryABC123--boundary开始,最后以--boundary--结束。 - 构造畸形Boundary:WAF在解析多部分数据时,需要正确地识别每个部分的边界。如果边界字符串在请求体中出现了歧义,WAF和后端框架的解析就可能不一致。我们可以尝试“破坏”这种解析。方法A:Boundary注入换行符。构造一个边界,其中包含换行符:
然后,在请求体中,我们仍然使用Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryABC123\r\nInjected-Header: test----WebKitFormBoundaryABC123作为边界。一些WAF在从Content-Type头中提取boundary时,可能会在遇到\r\n时就停止,认为boundary是----WebKitFormBoundaryABC123。而Spring框架的解析器可能更“贪婪”,会读取到行尾,但\r\n在HTTP头中表示头部结束,因此Injected-Header: test实际上成了一个新的、非法的HTTP头,这可能导致WAF解析请求头时出错,从而跳过对Body的深度检测。但更可能的是整个请求被WAF拒绝。方法B(更实用):在请求体内部制造混乱。这是更常见的技巧。正常文件部分:
我们可以在边界字符串上做手脚,比如添加额外的横线,或者利用boundary结束符的解析差异:------WebKitFormBoundaryABC123 Content-Disposition: form-data; name="file"; filename="shell.jpg" Content-Type: image/jpeg <?php @eval($_POST[‘cmd‘]);?> ------WebKitFormBoundaryABC123--
这里,第一个部分结束后,我并没有立即结束整个请求,而是插入了一个额外的、无用的第二部分(dummy)。有些WAF在检测到第一个------WebKitFormBoundaryABC123 Content-Disposition: form-data; name="file"; filename="shell.jpg" Content-Type: image/jpeg <?php @eval($_POST[‘cmd‘]);?> ------WebKitFormBoundaryABC123 Content-Disposition: form-data; name="dummy" value ------WebKitFormBoundaryABC123--boundary行时,就认为一个文件部分开始了,它会持续读取直到下一个boundary行,然后对这部分内容进行检测。它可能将<?php ... ?>和紧随其后的Content-Disposition: ...都当作文件内容来检测。由于Content-Disposition: ...这一行看起来不像PHP代码,可能会干扰WAF基于统计模型或语义分析的检测引擎,降低其对该部分恶意代码的置信度,从而放行。而Spring框架会正确解析出两个独立的部分,第一个部分的内容就是纯的Webshell代码。 - 结合其他技巧:将Webshell代码进行轻微变形,比如用
.连接字符串、用动态函数调用等,进一步增加WAF识别的难度。$a = ‘ev‘.‘al‘; $b = ‘_PO‘.‘ST‘; $a($$b[‘cmd‘]);
注意事项:这种绕过高度依赖于WAF的具体实现和版本。在实战中,需要利用Burp Suite的Intruder或Repeater模块,对boundary格式、内容分布进行大量Fuzz测试。同时,要准备好服务器可能因为解析错误而返回400 Bad Request,这是测试过程中的正常现象。
4. 系统化WAF绕过方法论与Fuzz技巧
掌握了具体案例后,我们需要将其上升为方法论。面对一个未知的WAF,如何进行系统化的绕过测试?
4.1 信息收集阶段
- 识别WAF:通过发送恶意请求观察拦截页面、响应头(如
Server、X-Powered-By、X-WAF等)、Cookie特征来识别WAF厂商和可能版本。 - 探测解析器:发送格式正确但包含轻微畸形的请求(如多余的空白符、错误的换行符、大小写混淆的头部),观察响应是
200 OK(可能绕过)、400 Bad Request(严格拒绝)还是被WAF拦截。这有助于判断WAF的协议解析严格程度。 - 定位检测点:确定WAF检测哪些部分(URL参数、Body参数、Cookie、Header值、文件内容、文件名)以及检测的深度(是否解析JSON、XML、Multipart)。
4.2 Fuzz测试阶段
建立一个系统的Fuzz向量库至关重要。以下是一些分类:
| 测试类别 | 具体向量示例 | 测试目的 |
|---|---|---|
| 特殊字符 | \0(空字节),\n,\r,\t,\v,\f,空格,/**/,/*!*/ | 测试WAF对分隔符、空白符的过滤和标准化处理。 |
| 编码混淆 | 多重URL编码、HTML实体编码、Unicode编码、Hex编码、Base64编码。 | 测试WAF的解码层数和顺序。 |
| 语法变形 | SQL注入:UNION SELECT->UnIoN SeLeCt,UNION/**/SELECT,UNION%a0SELECT(换行符)。XSS: <script>-><scr<script>ipt>,<img src=x onerror=alert(1)>。 | 测试WAF的正则表达式是否大小写敏感、是否可被注释符打断、是否识别变形。 |
| 参数位置 | 将Payload放在:不同的HTTP方法(GET/POST)、Cookie头、自定义Header、JSON/XML的深层嵌套中。 | 测试WAF的检测覆盖范围。 |
| 协议层攻击 | HTTP请求走私(CL.TE, TE.CL, TE.TE)、分块编码畸形、管道化请求。 | 利用WAF与后端服务器解析不一致。 |
| 逻辑混淆 | 参数污染(同名参数多个值)、参数包裹(param[name]=value)、数组参数(param[]=value)。 | 利用应用处理逻辑与WAF检测逻辑的差异。 |
Fuzz工具链推荐:
- Burp Suite Intruder: 绝对主力,配合强大的Payload列表和Grep匹配规则,可以自动化测试大量向量。
- ffuf: 高效的Web Fuzzer,适合对目录、参数进行快速爆破。
- Wfuzz: 另一个功能丰富的Web Fuzzer。
- 自定义脚本: 使用Python的
requests库或httpx库,可以灵活构造各种畸形请求,特别是协议层攻击的Payload。
4.3 绕过验证与利用阶段
当Fuzz测试发现一个可能绕过的Payload后:
- 稳定性测试:重复发送该Payload多次,确认绕过不是偶然(如WAF的缓存或状态问题)。
- 上下文适配:将Fuzz出的绕过技巧(如特定的编码、特殊的字符位置)应用到实际的漏洞利用Payload中。例如,你发现用
%0a(换行符)可以打断WAF对union select的检测,那么你的最终注入Payload就应该是union%0aselect。 - 制作最终利用工具:将成功的绕过方法集成到你的漏洞利用工具中,如Sqlmap的
tamper脚本。例如,可以写一个tamper脚本,自动将所有的空格替换为%0a/**/。
5. 防御视角与总结反思
作为攻击技术的研究者,我们必须从防御视角思考,才能更深刻地理解这些绕过,并更好地防护自身系统。
对于防御方(蓝队/开发/运维):
- 纵深防御:不要依赖单一WAF。在应用层做好输入验证、参数化查询(SQL)、安全的输出编码(XSS)、严格的文件类型检查等。
- 标准化输入:在请求到达业务逻辑之前,对输入进行严格的标准化和规范化。例如,统一URL解码、去除非法字符、合并同名参数(按明确规则,如只取第一个或最后一个)。
- 更新与调优:及时更新WAF规则库。同时,根据自身业务特点调优WAF,避免过于宽松或过于严格。可以开启WAF的日志审计功能,分析被拦截和放行的请求,寻找误报和漏报。
- 协议一致性:确保网关(WAF/负载均衡)和后端应用服务器使用相同版本和配置的HTTP解析库,尽可能消除解析差异。
- 安全开发流程:在代码审查中,特别注意参数获取方式(避免使用
$_REQUEST,明确使用$_GET或$_POST)、文件上传处理逻辑、以及任何自定义的解码/解压逻辑。
个人实操体会:WAF绕过是一场永无止境的“猫鼠游戏”。我经历过无数次测试,一个精心构造的Payload可能只在某个特定WAF的某个特定版本上有效。真正的价值不在于收集多少个“0day”绕过技巧,而在于培养那种“寻找差异”和“系统化测试”的思维。当你面对一堵墙,不再只想着用更大的锤子,而是开始观察墙的材质、结构、地基,甚至思考砌墙人的习惯时,你总能找到那条缝隙。最后,务必在合法授权的环境下进行所有测试,技术的刀刃应当指向加固与防御,而非破坏。
