☰
路径遍历漏洞详解:从原理分析到过滤绕过实战
2026/10/15 4:46:10 网站建设 项目流程

最近把 PortSwigger 的路径遍历实验系列完整刷了一遍,这套题目确实让我对“文件路径到底是怎么被拼出来的”这件事有了很直观的认识。路径遍历(Path Traversal)在 Web 漏洞里属于看着简单、实际很容易踩坑的一类,它不需要什么高级利用链,往往一个../就能改变整个访问模型。如果你正在准备 Web 安全基础,或者想补齐漏洞原理层面的短板,这套实验的训练价值非常高。这篇文章我就按实际刷题顺序,把每道题的绕过思路、payload 构造逻辑、踩坑点都拆开讲,尽量让没刷过的人也能明白每一关背后到底在考什么。

1. 路径遍历到底是个什么漏洞?为什么值得专门刷一套实验

1.1 漏洞从哪来:文件拼接里埋下的雷

路径遍历的本质是应用把用户输入直接拼进了文件路径里。很多后端代码长这样:

<?php $file = $_GET['filename']; readfile("/var/www/images/" . $file); ?>

这段代码的本意是让用户通过filename参数读取产品图片。正常请求filename=product1.jpg时,实际读取/var/www/images/product1.jpg。但问题在于,用户输入没有经过任何校验就被拼到了路径末尾。如果输入filename=../../../../etc/passwd,拼接后的结果是/var/www/images/../../../../etc/passwd。

在 Linux 文件系统标准解析规则下,..表示上一级目录。从/var/www/images/出发往上跳四层,可能就到了/etc/passwd,于是本应只能读图片的接口直接暴露了系统账号文件。这种问题在 Java、Python、Node.js、Go 里都出现过,核心不是说哪个语言不安全,而是开发者把“前后端拼接路径”这件事想简单了。

1.2 为什么 PortSwigger 的这套实验适合作为训练主线

市面上的靶场很多,但 PortSwigger 这套路径遍历实验的编排方式比较合我口味。它在同一个主题下设计了多种不同过滤条件,难度是循序渐进的。我印象比较深的几个关卡:第一关是非常直接的../../../../etc/passwd穿越;后面几关分别考了绝对路径绕过、非递归剥除绕过、URL 编码绕过、起始路径校验绕过、扩展名校验绕过。每一关都只替换一个防御环节,你能清楚看到“同一段代码加了防过滤之后,攻击面发生了什么变化”。

用这套实验当训练主线还有一个好处:它的反馈机制很明确。请求发出后,页面要么直接回显文件内容,要么提示读取失败,你不需要猜系统内部状态。这种“一题一防御”的设计特别适合做对比总结,刷完一遍之后,各种绕过手法之间的关系就串起来了。

1.3 刷完这套实验能沉淀什么能力

以我个人的体会,掌握路径遍历不只是记几个 payload。它至少锻炼三类能力:一是识别参数入口的能力,看到 URL 里的文件名、路径名、下载参数,能条件反射想到“这里可能拼了路径”;二是绕过过滤的体系化思考能力,知道过滤手段有哪几个流派,每个流派对应的绕过形态是什么;三是防御视角的建立,刷完攻击侧再回看代码,你会更明白路径校验到底应该写在哪一层。这些能力在真实代码审计里非常实用。

2. 刷题前必须搞懂的若干个基础认知

2.1 实验环境的基本组成

这套实验需要在浏览器之外配合抓包工具使用。我的习惯是全程用 Burp Suite 的 Proxy 和 Repeater:Proxy 用来观察应用真实的请求格式,Repeater 用来反复修改 payload。

搭建环境其实不难,在实验平台注册账号后,每个实验会给你一个专属的临时访问地址。启动实验后,你会看到目标应用,通常是类似商品展示站的页面。路径遍历的注入点一般隐藏在图片加载、文件下载、富文本预览这类功能里。比如某商品页有张图片,URL 是/image?filename=product1.jpg,那个filename就是入口。

顺带说一句,正式开始前把 Burp 的拦截关闭,先正常逛一遍目标站点,把所有带参数的请求都记录一遍。很多人上来就改 payload,结果连注入点都找不全,后面绕再多也没用。

2.2 路径解析的三个基本事实

绕路径遍历过滤之前,脑子里要非常清楚三件事。

第一,操作系统解析路径的方式和应用层过滤路径的方式不同。应用层看到的../只是普通字符串,但到了文件系统层面,它会被标准化成“上跳一级”。所以很多过滤逻辑在字符串层面做替换和删除,但操作系统最终还是会按..语义去解析。这就是各种双写和编码绕过能成功的前提。

第二,URL 解码发生的时机不同。浏览器发请求时会把特殊字符编码,服务器接收到后可能需要解码成原始字符再交给应用。如果应用层做了一次过滤,中间又做了一次解码,过滤就可能被绕过。比如%2e%2e%2f在被解码前就是普通字符串,过滤规则匹配不到;解码后才变成../。

第三,相对路径和绝对路径是两套体系。相对路径需要从当前目录逐级往上跳,所以../数量必须足够;绝对路径则直接指向文件系统根目录,很多时候一个/etc/passwd就结束了。

2.3 用什么姿势判断目标存在路径遍历

判断漏洞存在不能只靠猜。我常用的几个观察维度:

  • 回显型:文件内容直接出现在响应体里,这是最理想的情况。
  • 报错型:路径无效时页面报错,错误信息里带出了工作目录或者文件路径片段。
  • 静态资源差异型:比如正常图片能加载,payload 成功后页面图片区域变空白或显示异常。
  • 附带信息型:响应头的Content-Type、Content-Length发生变化,也能辅助判断。

在 PortSwigger 实验里大多是回显型,所以判断相对容易。但这不代表可以瞎试,必须观察清楚正常请求的响应特征,再对比 payload 请求的响应差异,才能确定是否命中。

2.4 构造 payload 的通用模板

先梳理一个通用思路:知道参数入口是filename,知道目标文件是/etc/passwd,接下来要计算相对路径要从当前目录跳多少层才能到根目录。PortSwigger 实验里图片目录深度一般是 4 层,payload 模板为:

filename=../../../../etc/passwd

不要一上来就堆十个../。数量不够时文件找不到,数量太多时如果路径校验有些特殊逻辑也可能出问题。我刷题时习惯先用../../../../etc/passwd探测,不行再加层数,或者切换绝对路径方案。

3. 逐关拆解核心绕过手法

3.1 最直接的../穿越

第一关基本属于送分题。进入实验后,随便打开一个商品详情页,能看到商品图片的请求格式是/image?filename=xxx.jpg。把参数改成../../../../etc/passwd,响应体直接就是 passwd 文件内容:

root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/bin/sh bin:x:2:2:bin:/bin/sh

这里有一个很容易犯的错:有些人看到内容是横向文本,以为没成功,其实回显在响应里一部分。用 Burp 的搜索功能直接搜root:关键字,比肉眼扫页面快得多。

做题的意义在于理解为什么会成功。应用层拿到的是/var/www/images/../../../../etc/passwd,文件系统解析时先定位到/var/www/images/,然后逐级上跳:images→www→var→/。到了根目录后再进入etc读取 passwd。这也就是为什么需要四层../,而不是三层。

3.2 绝对路径绕过前缀限制

第二类关卡引入了一个过滤逻辑:代码里不再直接拼接相对路径,而是做了前缀拼接:

$file = "/var/www/images/" . $_GET['filename']; if (strpos($file, '/var/www/images/') !== 0) { die("Invalid path"); }

很多初学者看到这句校验会认为安全了,因为任何../开头的路径都会让前缀字符串不匹配。但这里的漏洞在于:校验的是拼接后的结果是否以/var/www/images/开头,并没有规定用户输入里不能嵌入绝对路径组件。

假如传入filename=/var/www/images/../../../etc/passwd,拼接后完整路径是/var/www/images//var/www/images/../../../etc/passwd。字符串校验通过了,因为开头确实是/var/www/images/;但文件系统解析时,前面的前缀部分没有实际影响,后面的../../../etc/passwd会把路径从/var/www/images/一路弹到根目录再进etc。最终读到的还是目标文件。

这类题的核心启发是:校验字符串前缀不等于约束真实文件路径,绝对路径元素在拼接和解析过程中并不会被“中和”掉。碰到这类过滤时,先看校验逻辑的输入输出位置,再决定 payload 怎么构造。

3.3 非递归剥除过滤与双写绕过

再往上难度递增,遇到了一个关键过滤:代码尝试把输入里的../子串直接替换为空:

$file = str_replace('../', '', $_GET['filename']);

这种方案很多开发者觉得挺好,但它有一个致命问题——替换不是递归的。所谓非递归,就是只从头到尾扫一遍,把能匹配的../删掉;删完之后新生成的字符串里如果又出现了../,那就管不了了。

经典绕过构造是....//。为什么有效?把....//里插入../的位置观察一下,假如输入....//....//....//etc/passwd,替换规则一执行,中间那部分../会被删除,但原来多出来的两个点和两个斜杠重新组合成了新的../。一次替换完成后,字符串反而变成了../../../../etc/passwd。

另一种形态是..././。..././中,../匹配不上,因为顺序不对;但替换后剩下../加.,重新拼出一个../。这两种写法本质上都利用了“过滤一次之后字符串重新组合”这个特点。做这类题不要死记....//,要理解过滤器的工作机制,然后再构造“删除后重组”的序列。

3.4 URL 编码绕过与二次解码陷阱

编码绕过是路径遍历实验的重头戏。先说单层编码:../的 URL 编码形态是%2e%2e%2f。如果过滤代码只检查原始字符串里有没有../,会漏掉编码形态。服务器在把参数交给文件系统前通常会做一次 URL 解码,%2e%2e%2f解码后就是../,于是漏洞被触发。

这里最折磨人的是二次解码场景。我一度以为实验没反应,后来才发现是编码层数不对。有的应用会经历“Web 服务器解码 → 应用防火墙解码 → 应用再解码”的过程,中间只要多一层解码,单层编码就不够用。于是需要构造双重编码:

%252e%252e%252f

这个 payload 先被解码一层,变成%2e%2e%2f;再被解码一层,才变成../。实验里可能要求传递filename=....//....//....//etc/passwd之后再进行 URL 编码,也可以直接用 Burp 的编码功能处理一整段字符串。

我踩过的坑:在 Repeater 里手动粘贴 payload 时,Burp 会自动对某些字符做编码,导致我看到的../实际传的是%2f%2e%2e顺序错乱。遇到响应一直不对,先在 Inspector 面板看清楚最终发送的字节内容,再用 Repeater 的“URL encode”开关控制编码层数。这个细节能省下大量排查时间。

表:常见 URL 编码形态速查

原始形态单层编码双重编码适用场景
../%2e%2e%2f%252e%252e%252f过滤只检查明文../
..\%2e%2e%5c%252e%252e%255c后端运行在 Windows 路径解析
.%2e%252e组合变体绕过字符黑名单

3.5 起始路径校验与扩展名校验的绕过思路

起始路径校验关卡有一个代码片段,约束路径必须从/var/www/images/开始:

$file = $_GET['filename']; if (strpos($file, '/var/www/images/') !== 0) { die("Invalid path"); } readfile($file);

注意,这里没有做任何目录拼接,而是直接信任用户输入并以它作为完整路径。所以构造方式就变成了:URL 里显式带上合法前缀,再通过../弹出去。

filename=/var/www/images/../../../etc/passwd

这一步的本质是:前缀校验通过的路径不一定是最终被解析的路径。只要开头字符串符合要求,后面跟着多少../都能回到更高级目录。

扩展名校验关卡更隐蔽。后端要求文件必须以.png结尾,否则拒绝读取:

$file = $_GET['filename']; if (!preg_match('/\.png$/', $file)) { die("Invalid file extension"); } readfile($file);

初学者面对这个限制往往会想“那还怎么读 passwd?”,因为 passwd 不是.png。但实验环境里存在一个特性:文件路径解析时遇到%00(空字节)会截断字符串。于是 payload 可以构造为:

filename=../../../../etc/passwd%00.png

服务器先做扩展名检查,看到字符串结尾是.png,校验通过;但真正交给文件系统解析时,%00后面内容被丢弃,最终打开的文件变成了/etc/passwd。这种空字节截断在传统 PHP 5 环境里很常见,现代应用基本修复了,但用来理解“校验层与解析层之间的信息差”非常经典。

3.6 盲测场景:没有回显时怎么定位路径

不是所有路径遍历都有内容回显,PortSwigger 实验里也存在盲测型场景。响应里看不到文件内容,但你可以通过响应时间、报错信息、日志行为确认漏洞是否被触发。

盲测的关键是把读取操作变成“可观察的信号”。一种思路是利用带外交互,比如让服务端去请求你控制的域名,观察是否收到请求。另一种思路是制造延迟,比如尝试读取/dev/random、预期大量输出,或者读取某些大文件让处理时间发生变化。

我做这类实验时,习惯先用/etc/passwd和/windows/win.ini做常规探测,观察响应长度、响应头的Content-Type差异。如果完全没差异,再考虑带外验证。带外验证的 payload 模板通常是:

filename=/etc/passwd%00.png

或者结合路径穿越让应用加载远程资源,比如利用//双斜杠和 URL 解析差异。不过这些组合手法需要结合具体后端特性,不要硬套模板,否则很容易被平台的环境差异干扰判断。

4. 实操中的问题排查与踩坑记录

4.1 payload 构造对但读不到文件的原因

我刷题过程中最常遇到的就是“payload 看起来没问题,但提示文件不存在或读取为空”。这类问题一般跑不出几个原因。

第一是目录深度不对。很多商品图片目录结构是/var/www/images/,但有些站的目录层级更深,比如/var/www/html/static/images/。此时四层../不够,要先用不同层数批量探测。第二是路径中有重复的 URL 编码干扰。Burp 默认会对请求做标准化处理,如果你在 Repeater 里手动粘贴%2f之类的内容,可能传的时候被再次编码。第三是目标文件权限问题,尤其在容器化环境里,/etc/passwd不一定可读。

排查方法:先拿一个你确定的正常请求做基准,比如filename=product1.jpg,确认响应正常;然后逐步替换参数,每次只改变一个变量;最后用 Burp 的 Comparer 对比正常响应和 payload 响应的差异。不要同时改编码、层数、路径前缀,否则错了都不知道错在哪个环节。

4.2 Burp Suite 联动过程中的细节

PortSwigger 实验全程用 Burp 效率最高。几个细节值得提一下:

在 Proxy 的 HTTP History 里,先选中正常图片请求,右键发送到 Repeater。Repeater 里修改filename参数时,注意 URL 路径里如果有中文或特殊字符,Burp 的展示层可能做了自动解码。为了确保 payload 按你设想的方式发送,我习惯手动复制编码后的完整字符串,并且每次发送后看一眼 Raw 面板里的实际字节。

另外,如果实验里要求的是双重 URL 编码,不要直接在整个 URL 上做一次“URL encode all”就完事。要分清哪些字符编码一次,哪些字符编码两次。一个比较实用的技巧是先把 payload 写成明文,比如../../../../etc/passwd%00.png,然后用 Burp 的 Decoder 对指定部分手动编码,再粘贴到 Repeater 里。

4.3 常见现象与处理思路速查表

现象可能原因处理方式
No such file or directory目录层级不够或路径前缀错误逐层增加../,测试/etc/passwd绝对路径
响应 200 但页面无图片内容读取到了文件但内容无法作为图片渲染查看响应原文,搜索目标文件关键字
响应的文件内容带杂乱字符浏览器对待特殊字符编码不一致用 Repeater 直接查看 Raw 响应,不要只看 Preview
绕过过滤后仍拦截过滤规则执行了多次或有多层解码改为双重编码或双写组合
提示无效文件扩展名扩展名校验生效尝试%00.png或.绕过
正常请求也 404实验环境初始化未完成刷新实验页面,确认地址是否过期

4.4 批量测试的高效技巧

路径遍历最怕一个个试,效率太低。我习惯用 Burp 的 Intruder 批量处理。

把请求发送到 Intruder,在filename参数上设置 payload 位置。Payload 类型选择“Simple list”,把所有候选 payload 写进列表:不同层数的../、单层编码、双重编码、双写变体。发送后按响应长度排序,有回显的响应长度通常和正常响应明显不同,一眼就能看出来。

还有一个技巧是设置一个“关键字 marker”。在 Intruder 的 Settings 里添加 Grep-Match,匹配root:或flag等关键字,就能直接在结果列表里标出命中的请求。这个方法在盲测场景或响应没有渲染到页面时特别好用。

5. 刷完后的技法沉淀与防御视角

5.1 把绕过思路整理成可复用的体系

全部刷完之后,我最大的收获不是记住了多少 payload,而是建立了一个判断框架。拿到一个文件读取类接口,先问三个问题:这个路径是直接拼出来的还是做了前缀拼接?过滤规则匹配的输入是原始值还是解码后的值?文件系统解析发生在哪一层?

按这个框架,处理逻辑可以简化为:

  1. 如果没有任何校验,直接../../../../etc/passwd。
  2. 如果有前缀拼接或前缀校验,尝试绝对路径前缀 +../组合。
  3. 如果过滤了../,尝试双写、嵌套写法,观察过滤是否递归。
  4. 如果过滤基于明文,尝试 URL 单层编码、双重编码。
  5. 如果校验后缀,尝试空字节或其他截断字符。

这个判断顺序是递进的,但实际应用中可能要组合使用。比如一个接口既做了前缀校验,又过滤了../,还要求后缀为.png,那就得同时构造前缀 + 双写编码 + 空字节后缀。这种组合题不是背一个 payload 就能解决的,必须理解每个防御点各自拦截的位置。

5.2 服务端到底怎么防才能防住

站在防御侧看这套实验,也能得到很大启发。最核心的一条是:永远不要相信用户输入中的任何路径片段。

我在实验里观察到很多“看似安全”的姿势,比如过滤黑名单、前缀校验、扩展名校验,其实都有被绕过的空间。真正的防御措施应该是把用户输入和文件路径彻底分离。比较实用的做法包括:

  • 使用文件 ID 映射到存储路径,用户只能选择 ID,不能输入文件名。
  • 对提交的文件名做严格白名单校验,比如只允许字母、数字、点、下划线。
  • 在读取前做路径规范化检查,用realpath或等价 API 解析出绝对路径,再校验解析后的路径是否落在允许目录内。
  • 使用沙箱目录或独立的文件存储服务,从架构层面限制文件访问范围。

很多人以为加一个str_replace('../', '', $input)就够了,但实验已经证明这种过滤可以通过双写绕过。防御端必须假设攻击者会尝试所有绕过手法,而不是靠运气挡住一个 payload。

5.3 最后说点实际操作中的体会

这套实验刷完,我个人的建议是每道题都坚持手打 payload,不要直接看别人给的答案。路径遍历的难点不在数量而在变化。同一个../在不同过滤规则下会衍生出十几种变体,你亲手构造过一遍,才能理解每个字符在解析流程中到底起什么作用。

另外,刷题时可以顺手记一份自己的 payload 字典。我至今保留着一份按过滤器类型分类的笔记:明文字符型、URL 编码型、前缀拼接型、后缀校验型、盲测型,每一类下面都写了对应实验中的成功请求。之后再碰到文件读取功能,直接翻笔记对照,省了很多重复试错的时间。这套实验本质上是攻击和防御的对抗练习,刷一遍只能算入门,隔段时间再看一遍,结合新的漏洞案例复习,理解会更深。

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

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

立即咨询