☰
目录遍历与WAF绕过实战:解码差异与中间件解析之道
2026/9/29 15:25:52 网站建设 项目流程

接到这种标题,第一反应通常都是“又来一个卖课的”。但真在自己的项目里遇到过几次目录遍历绕过WAF之后,我得说:这个方向被讲烂了,真正讲透的人不多。很多人要么只会背payload,要么只会在靶场里打得不亦乐乎,换个真实应用的语义环境就抓瞎。这篇我不想整那些“从入门到放弃”的空话,直接把我自己在做路径穿越绕过、WAF规则对抗时总结的一套流程和库给你,重点说清楚为什么有些Payload在其他场景落地直接失效,以及怎么去体系化地组织和验证这些内容。

目录遍历本身不是新技术,但WAF绕过一直是攻防里比拼规则更新和解析差异的活。很多同学手里拿着网上扒下来的几百上千条Payload,打靶场没问题,上了真实授权项目就变成了“为什么我的POST发出去直接413,或者绕不过边缘节点的过滤规则”。问题往往不在Payload本身,而在于你根本不知道WAF和中间件之间到底谁在解码、解码了几次、解码顺序又是什么。

这篇内容适合刚接触Web安全的同学建立绕过思路,也适合已经在写脚本批量验证Payload的人参考一下怎么把800+条Payload按场景切成块,免得以后每次都被“Payload太多跑不完”“跑了半天全是误报”折腾到没脾气。我尽量少说废话,全部按我自己实测过、在授权环境下验证过的路径来讲。

1. 目录遍历与WAF绕过的底层逻辑:先搞清楚绕的是什么

目录遍历漏洞的根源,说白了就是程序拿着用户可控的路径参数去拼接文件路径时,没有做好规范化处理,导致../这类路径穿越符号能够跳出预期目录,读到/etc/passwd、Windows系统文件或其他敏感文件。这个漏洞从Web 1.0时代就有,到现在仍然频繁出现,原因就是很多开发对路径拼接这件事过度信任,总觉得框架会帮自己处理干净。

而WAF绕过,核心其实是“语义还原”问题。WAF要拦截的是“经过解码和规整后的最终请求路径里包含穿越特征”,但它看到的是原始HTTP报文。于是整个绕过过程就变成了:我能不能通过某种编码、某种协议特性、某种服务器解析差异,让WAF还原出来的语义和Web后端还原出来的语义不一致。

我举一个最典型的例子。你发送:

GET /download?file=..%252f..%252fetc/passwd HTTP/1.1

WAF可能做了两次URL解码,把%252f还原成%2f,然后发现没有../或..%2f特征,直接放行。而真实后端如果只做了一次URL解码,拿到的就是..%2f..%2fetc/passwd,中间件在访问文件系统前还会再做一次路径规范化,把%2f还原成/,于是穿越成功。关键就在于两边解码次数不一致。

这就解释了为什么网上很多Payload抄过来就是打不了:因为你这个站点的WAF解码次数、后端框架的路径处理方式,和人家写Payload时的环境根本不一样。所以真正能打的Payload,不是背出来的,是测出来的。

1.1 三种角色在路径处理上的视角差异

在目录遍历绕过里,有四个角色在各自的规则里理解这段路径:

角色理解方式典型动作
客户端/攻击者原始字符串往参数里塞各种编码字符
WAF/网关按规则做解码还原匹配../等特征后拦截
Web中间件按RFC做路径解析合并反斜杠、压缩多余斜杠、处理.符号
应用框架拿到最终路径参数不做校验,直接把值拼到文件路径上

WAF和中间件只要对同一个字符的标准化结果不一样,就有绕过的空子。这就是全部原理,其他花里胡哨的技巧都是在这个基础上变种。

1.2 WAF检测目录遍历的常规规则

大多数WAF对目录遍历的检测分为三层:

  1. 静态特征匹配,比如正则(\.\./|\.\.\\)、(\.\.%2f|\.\.%5c);
  2. 解码后匹配,做一次URL解码、HTML实体解码,再匹配穿越特征;
  3. 语义分析,把路径还原成标准化路径后,再看是否越过了配置的基准目录。

第一层最好绕,多编码一次就没了;第二层要会判断解码次数;第三层就比较麻烦了,但真实WAF里第三层能做好的也不多,很多厂商设备仍然停留在前两层。不是他们不想做,而是做语义分析要牺牲性能、要维护大量应用规则,还要面对各种畸形HTTP报文的兼容性问题,做得越多误拦越多,用户投诉也就越多。

2. 编码绕过实战:从双层URL编码到Unicode陷阱

编码绕过是目录遍历WAF绕过里最基础也最实用的一类。我不是让你死记硬背编码表,而是要把“为什么编码能绕过”这个逻辑搞清楚,然后按场景取用。

WAF匹配的关键词一般是解码后的明文,比如../、..\、..%2f。如果你把一个字符用编码表示,让WAF的解码器没还原出来,而后端又能还原,就达成了绕过。核心在于“多一层编码”。

2.1 双层编码与多层编码的标准打法

当后端框架对参数做了两次URL解码时,你可以构造:

GET /file?name=..%252f..%252fetc/passwd HTTP/1.1

WAF如果只解一层,看到的还是%2f或者是没有意义的字符串,直接放行。后端第一次解出..%2f..%2fetc/passwd,第二次解出../../etc/passwd。这里的关键是你要先确认后端到底解码几次。怎么确认?自建一个本地环境,对同一个参数做不同的编码次数,看返回内容差异,或者观察响应时间、错误信息。

实战里我一般先打这三种序列:

  • 单层URL编码:..%2f..%2fetc/passwd
  • 双层URL编码:..%252f..%252fetc/passwd
  • 混合编码:..%252f..%2fetc/passwd

每种都去观察返回包。如果单层被WAF拦截,双层可能就过了。如果双层还是被拦,那说明WAF至少做了两层解码,你就需要用更复杂的畸形结构,或者转向中间件解析差异。

2.2 Unicode编码和全角字符的绕过场景

Unicode编码是另一个常用场景。/的Unicode全角形式是/(U+FF0F),有些后端在做编码转换的时候会把全角斜杠还原成普通斜杠,而WAF的规则库如果没覆盖这种映射,就能打过去。

构造方式:

GET /file?name=../../etc/passwd HTTP/1.1

注意,这个Payload的可靠性完全取决于目标站点的字符编码处理方式。Java的某些版本、老旧的Tomcat连接器配置、部分PHP的mb_convert_encoding场景都出现过类似解析问题。我遇到更多的情况其实是IIS和ASP.NET在处理%c0%af这类过时编码时的历史问题。现在Windows系统基本修复了大部分,但内网老系统里仍然可能碰到。

2.3 编码Payload怎么快速按场景切块

这就说到你关注的“800+ Payload直接用”了。我不建议你把800条Payload全塞进一个文件里盲目爆破,那样除了把自己网络打崩、日志刷爆、误报率飙升之外没有任何好处。我更建议你把Payload分成8个区,称为payload提取分区,每个区对应一种解码行为和一种后端特征:

分区编码方式适用后端特征
A区单层URL编码几乎所有后端
B区双层URL编码Java、Python某些框架
C区三层URL编码极少见,通常用于二次解码场景
D区Unicode/全角字符IIS、老Java、特殊字符集配置
E区混合编码前几项被WAF拦截后的后备方案
F区反斜杠与正斜杠混用Windows路径、Nginx反向代理场景
G区空字节截断老版本PHP、Java
H区语义规整类Node.js、Next.js等现代框架

每个分区里我一般保留100条左右变体,总共800条。但真正跑的时候不是全量跑,而是要结合目标指纹先缩小范围。比如通过响应头判断是Nginx+PHP-FPM,那就主跑A、E区,顺带跑H区的一些绝对路径Payload,其他区先放一放。

3. 路径规约与歧义结构:不靠编码也能绕

很多人以为绕过WAF只能靠编码,其实路径语义规整这个方向才是更高级的玩法。因为即使WAF把请求完整解码,也不一定知道后端到底会把这段字符串“理解”成什么路径。

3.1 绝对路径与点目录:绕过“必须包含..”规则

有些WAF规则认的死理是:只要没有..就不算目录遍历。这时候绝对路径反而好用。

GET /file?name=/etc/passwd HTTP/1.1 GET /file?name=C:/Windows/win.ini HTTP/1.1

如果后端直接把参数拼进路径前缀,比如/var/www/files/+$name,你传/etc/passwd可能拼出来是/var/www/files//etc/passwd,Linux文件系统会把连续斜杠压缩成一个,等同于/var/www/files/etc/passwd,读不到passwd。但如果后端对拼接结果做了“去前缀”处理,或者拼接时用的字符串处理函数把前面的路径覆盖掉了,那就可能直接读取成功。

更常见的是配合./来吃掉前缀:

GET /file?name=./../../../../etc/passwd HTTP/1.1

有些人疑惑为什么前面要加./,这是因为某些路径拼接库在处理相对路径时,会先把.解析成当前目录,随后..才能正常往上一层走。不带这个点,有些解析器会把首段的..当作非法路径直接拒绝。

3.2 嵌套穿越与路径压缩差异

另一种思路是利用后端路径规范化时对.和..的处理顺序。比如:

GET /file?name=....//....//....//etc/passwd HTTP/1.1

某些服务器在规范化路径时会把....//解析成../(把后面的两个点加斜杠视为完整穿越标记,然后把前面多余的点归并掉),而WAF的正则可能只匹配了..//或../的精确形式,漏掉了这种变体。

这类Payload测试时不要贪多,同一个目录深度多套几个不同写法就行,因为真实后端往往是固定层数,你套十几层和套四五层,最终能读到的文件可能一样。但WAF的拦截日志会记录不同深度,跑得太多容易被封IP。

3.3 URL解析器对分号、逗号和参数混淆的处理

RFC 3986允许URL路径中包含分号,有些Web服务器会把;后面的内容当作路径参数,不影响文件路径。利用这一点可以在穿越序列中间插入干扰字符:

GET /file?name=..;/..;/..;/etc/passwd HTTP/1.1 GET /file?name=..%3b/..%3b/..%3b/etc/passwd HTTP/1.1

WAF如果把分号后面的内容当作独立参数或者忽略掉了,就看不到完整的../序列。而Java的Servlet容器对路径参数的处理历史上有几次著名的绕过事件,直接导致CVE。虽然现在的新版本修了,但内网里存量系统用老版本的仍然不少。

4. 解码器与网络节点的“理解差”:413和超限请求带来的额外变量

前面讲的所有Payload,其实都建立在一个前提上:你的HTTP请求能被完整送到后端。但是真实攻防里,WAF前面可能还有CDN、反向代理、网关防护,每一个节点都可能因为请求本身异常而直接拒绝,根本不给你测试的机会。

4.1 什么是unexpected status 413 payload too large

这个词条看起来像报错,实际是你在用Burp或者自动化脚本跑Payload时特别常见的一个坑:Payload太长,或者请求头/请求体体积超过了中间件或上游Provider的限制,返回413 Request Entity Too Large。很多同学第一次看到413 payload too large: upstream provider rejected the request就直接懵了,以为是Payload打出了什么神奇效果。

其实这就是Nginx配了client_max_body_size,或者是API网关默认只接受小体积请求。你拿着动辄几十KB的超长Payload列表往一个只允许8KB请求体的接口上灌,自然被拒。

我实测下来,处理这种问题有几个办法:

  • 把Payload进行分区,每次只测一个区,而不是全量拼接;
  • 压缩请求体积,去掉所有和路径穿越无关的Cookie、冗余Header;
  • 把路径穿越参数放到URL参数中,而不是POST body里,很多WAF和代理对URL参数的长度限制比body严格,但也有反过来的情况;
  • 如果必须用POST,改用multipart/form-data包装,有些网关对表单字段的解析和普通body不一样。

4.2 413本身会不会成为WAF绕过的机会?

这个问题有意思。当你发一个超大数据包时,有些WAF为了保证可用性,会启动“放行模式”或“快速失败模式”,不再做深度检测。但是这里我要泼冷水:靠把请求撑爆来绕过WAF的成功率非常低,而且极容易触发防护联动封禁。我更多是把413当成一个信息点,它帮我确认了中间件的版本和限制参数,接下来可以针对性调整Payload长度。

真正跟413有关的一个可操作方向是:当Payload过大被上游拒绝时,你在Burp的历史记录里能看到网关回包里的Server头、Via头、X-Cache头,这些指纹信息能帮你确认中间件类型,然后再决定下一轮分包里的Payload方向。比如确认了是Nginx,就重点测反斜杠混合和/规整问题;确认了是Tomcat,就重点测分号路径参数。

4.3 把Payload拆成小块跑的效率提升法

既然413是体积问题,那就从体积入手。建议你把整个Payload库按前面说的8个分区,每个分区再按“单包长度不超过4KB”的标准切成小组。每个小组内尽量保持同一类绕过思路,不要一会儿编码一会儿分号,否则命中了你都不知道是哪条规则起的作用。

我用自动化验证时,会预先给每个Payload打标签,类似于[enc-double][linux][url-param],跑完直接从报告里按标签汇总命中项,而不是打开几百条请求日志一条条看。这个习惯节省的时间,比你花十分钟写个复杂脚本多得多。

5. 中间件与框架的解析差异:针对特定目标精准绕过

到了这个环节,就不是通用Payload能解决的了。你要根据目标的中间件和框架特征选择对应的绕过方式。下面是我在授权测试中积累的几条比较典型的中间件判断方向,不代表某个产品现在还存在的漏洞,只是帮你理解解析差异。

5.1 Nginx与Apache的路径归一化差异

Nginx在处理URI时,默认会把%2f还原成/,但它对路径中连续斜杠、.和..的处理比较严格。Apache则有自己的MergeSlashes配置,默认会把多个斜杠合并。区别在于:同一个Payload,Nginx可能直接404,Apache可能就穿过去了。

经典的测试结构:

GET /file?name=..%2f..%2f..%2fetc/passwd HTTP/1.1

如果Nginx场景下被返回400,但直接换成..%5c..%5c..%5cwindows/win.ini在某个Windows反代后面却正常返回200,说明后端是Windows+IIS类路径处理。这就是用差异本身去探测后端,而不是盲目堆Payload。

5.2 Tomcat与Java系的分号路径参数问题

Java Servlet规范里,分号在路径中用于分隔路径参数,比如/download/;/etc/passwd。有些老版本Tomcat在解析时会忽略分号后的内容,如果你把穿越序列放在分号后面,等于给WAF看了个寂寞:

GET /download?path=..;/etc/passwd HTTP/1.1

注意这里的实际利用效果取决于具体接口怎么拼接路径,我不建议把它当通用武器,更像是一个在遇到Java应用时值得优先验证的探针。

5.3 Next.js这类现代框架的路径约束与绕过

这就要回应搜索热词里的“next.js payload教程”。其实Next.js本身对路径参数有比较严格的约束,尤其是App Router模式下,动态路由段不允许包含/和\。所以在Next.js应用里打目录遍历,更多是找历史遗留的pages/api接口、rewrites重写规则错误、或者服务端拿query参数直接拼fs.readFile的写法。

我之前在一个授权项目里遇到一个Next.js站点,它的/api/export接收filename参数,并且文档里只允许传入预设的模板名。但服务端代码直接做了字符串拼接:

const filePath = path.join(__dirname, '../templates/', fileName);

看起来path.join已经做了路径规范化,似乎没问题。但如果文件名传一个../../secret.txt,path.join会返回/app/secret.txt。WAF如果只拦截/etc/passwd这类关键词,不拦截../穿越,那这个接口就是目录遍历。所以现代框架的绕过重点通常不在框架本身,而在于业务代码的拼接方式。

5.4 CDN节点和源站解析不一致的链路绕过

这个思路在真实场景里特别好用。假设你的目标站点使用了CDN,CDN节点做一层WAF检测,源站是另一个配置。CDN的URL规范化标准和源站不一定一致,比如CDN会把//规整成/,源站不一定;CDN把%2e%2e解码成..,源站也解码,但顺序不同。

如果手上正好有源站的IP或者某个不经过CDN的域名,那就直接在源站环境测绕过,绕过了再想办法让流量走源站。如果没有源站入口,也可以尝试用Host头混淆、用不同端口、或者直接对CDN节点本身的解析逻辑做黑盒探测。

这里要特别提示一下:凡是涉及多个网络的测试,务必先确认目标属于你本人或者已获得书面授权的系统。我文章里提到的所有方法,都不应该用在未授权的系统上,这是底线。

6. 构建可复用的Payload库:从CTF靶场到真实场景的落地

很多人在CTFHub这类靶场里做目录遍历做得很顺手,各种Payload都验证通过了,结果一到真实项目就拉胯。原因很简单:靶场的后端逻辑是固定的,WAF要么没有,要么是模拟的,而真实项目的解析链路五花八门。所以我才反复强调Payload库要有分区、有标签、有场景映射。

6.1 CTFHub里的目录遍历训练能给你什么

CTFHub的目录遍历关卡的目的是让你理解不同参数位置的注入形式,比如直接在文件路径参数上做穿越、在包含文件路径的Cookie字段上做穿越、在跳转参数上做穿越。它的价值不在Payload本身,而在于让你形成“先看参数语义,再决定往哪儿插入穿越序列”的思维习惯。

我建议你在靶场里做三件事:

  1. 把每个关卡的标准Payload记录下来,标注关卡所用的后端类型;
  2. 用同样的Payload去绕靶场自带的模拟WAF,观察哪些编码被拦截、哪些能过;
  3. 把测试结果按“后端类型 / 参数位置 / 编码方式 / 是否绕过”四个维度整理成表格。

这份表格往后就是你真实项目里最基础的查询字典。CTFHub覆盖不了所有场景,但至少能帮你把思路理顺。

6.2 如何让800+ Payload真正“适配所有场景”

我想先纠正一个观念:没有任何一份固定列表能适配所有场景。所谓“适配所有场景”,正确的做法是库里有足够的模块,而你能根据目标指纹快速组合出对应模块。我的库里大概有8个分区、20多个变体组,每组算上不同深度组合,超过800条很轻松。

举个例子,一组典型的结构化变体:

Payload关键字变体数常见后端
../常规穿越50所有
..%2f单层编码80所有
..%252f二次编码60Java
..%c0%af畸形UTF-830IIS
..\反斜杠60Windows/IIS/部分Java
....//点号混淆90Nginx
..%3b分号混淆50Tomcat/Java
/absolute绝对路径40特定拼接场景
混合变体340按指纹组合
合计800全场景

你不需要一次性跑完,而是先做指纹定位。指纹定位就三件事:

  • 看响应头里Server、X-Powered-By、Set-Cookie的特征;
  • 看报错页面的框架标记;
  • 看URL上下文的参数命名和接口风格。

然后从对应分区里挑出50到100条先跑一轮,命中一条,马上顺着这条的结构做相近变体的深度扩展。这才是效率最高的方式。

6.3 自动化验证中的注意事项

用Burp Intruder、ffuf或者自写脚本批量验证时,我有几个亲测有用的建议:

  • 把线程调低,尤其是目标站有明显WAF时,高并发只会换来封禁;
  • 对每个请求设置独立的随机参数名,避免WAF对同一个参数名的连续异常请求做聚合封禁;
  • 观察返回包时不要只看状态码,要同时看响应体长度和关键词,如果目标接口是API返回JSON,路径穿越成功往往表现为code变化、message里泄露文件内容前缀;
  • 遇到所有请求都是413或其他统一状态码时,先停下来修正请求体积,而不是继续跑。

我自己的习惯是先用三个最基础的Payload确认接口确实存在目录遍历,再切到自动化批量跑。如果三个基础Payload全部被拦截,就先回去做WAF指纹识别和编码探测,而不是直接上800条全量。这一步能帮你省下大量时间,也减少对目标系统的无谓打扰。

7. 针对WAF规则演进的反制思路:如何维持绕过能力

写到这里,我再补一个很容易被人忽略但非常关键的维度:WAF规则是动态更新的。你今天验证过能打的Payload,可能过一个月就被规则覆盖了。所以长期做这块的人,不应该只靠存量Payload库,而是要建立自己的“绕过能力生成机制”。

7.1 从绕过记录里提炼新的变体规则

每一次测试结束,我会把命中的Payload和没命中的Payload都归档,并对命中Payload做一个逆向分析:它到底是破坏了WAF的哪个检测环节?是解码顺序、长度限制、正则盲区、还是协议解析差异?把这条原因抽象成一个规则模板,比如单参数二次URL解码且WAF仅做一次解码,下次遇到类似环境,就能快速套用。

我强烈建议你用表格记录,而不是散落在笔记里:

日期目标类型WAF类型命中Payload绕过原因适用组件
日期Nginx+PHP某云WAF..%252fWAF仅解一层,后端解两层所有
日期Tomcat某硬件WAF..;/etc/passwdJava分号路径参数未检测Java系
日期IIS某CDN%c0%af畸形UTF-8解码差异IIS

这个表的价值会随着时间积累越来越大,三个月后你就拥有一份自己实测过的、有针对性的绕过手册,而不是网上随便扒来的大杂烩。

7.2 源站与WAF规则的赛跑:持续追踪中间件补丁

WAF绕过另一个必须关注的层面是上游中间件的更新动态。每当Tomcat、Spring、Nginx、IIS发布安全公告,我都会第一时间去读CVE描述里的路径处理细节,因为这些漏洞描述里往往藏着解析器行为变化的蛛丝马迹。你不需要成为中间件源码专家,只需要能看懂“某个版本对路径中的分号不再忽略”这类信息,就能反过来更新自己的Payload适用表。

7.3 兜底手段:绕过不了就换攻击面

最后说句实在话:不是所有目录遍历都能绕过WAF。有些WAF把语义分析做得很到位,解码次数多、路径规整能力强,你再怎么折腾编码和混淆都没用。这时候与其死磕,不如换个攻击面。旁站、接口文档泄露、源码仓库泄露、备份文件扫描,任何一个方向都可能比你在目录遍历上死磕有价值。攻防不是单点突破的游戏,是路径规划的游戏。

我在实际项目里最大的体会就是:Payload只是弹药,对目标解析链路的理解才是真正决定成败的东西。希望这篇内容能帮你把目录遍历和WAF绕过这件事从“背Payload”提升到“理解绕过机制”的层面。下次再有人给你丢一个“800+ Payload通杀”的压缩包,先别急着跑,按我上面说的分区和指纹探测方法走一遍,你会发现能用的东西其实比想象中多得多。

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

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

立即咨询