☰
任意文件下载与文件包含:从路径穿越到代码执行的攻防实录
2026/10/20 14:45:56 网站建设 项目流程

做安全技术这些年,我在评估和应急里见过太多“看起来是小问题”的漏洞。文件包含、任意文件下载,这两个词听起来不如远程命令执行那么刺激,可实战和攻防演练里,它们往往是最先被利用的那道门。很多人以为“任意文件下载”无非是读个/etc/passwd,文件包含也不过是多读几个文件;但真实场景中,从这类入口一路拿到数据库配置、服务器密钥、后台权限的情况,我见过不止一次。这篇文章适合正在做 Web 开发的人、负责安全评估的同事,还有想弄明白自己系统为什么总被反复扫描的运维同学。我会先把两类漏洞的成因拆开,再讲常见出现位置、检测排查的具体操作、修复加固方案,最后把实际测试中容易踩的坑也一并整理出来。文章里所有涉及验证和利用的内容,都默认停留在授权范围内,生产环境不要照搬到未授权的目标上。

1. 先分清“任意文件下载”和“文件包含”这两条线

1.1 一个能读,一个能执行,风险级别完全不同

经常有人把任意文件下载和文件包含混在一起说,排查的时候也习惯性套用同一套思路。实际上,两者的技术表现和最终危害有明显差异,先建立一个基本认知很重要。

维度任意文件下载 / 任意文件读取文件包含(LFI / RFI)
核心行为读取服务端文件内容并返回给客户端把文件作为代码或资源加载执行
典型入口导出报表、下载附件、图片预览页面加载、模板切换、语言包选择
直接危害信息泄露、源码泄露、密钥泄露信息泄露,并可升级为代码执行
常见函数readfile、file_get_contents、fopeninclude、require、模板引擎加载

我在实际项目里见过不少案例,下载接口一个参数没校验,结果把应用配置、数据库账号密码全部读了出来;而文件包含类的问题,经常出现在“页面动态加载”这种看起来很寻常的功能里,一个参数能被带入include,就直接从文件读取变成了远程代码执行。两者名字相近,一个偏“读”,一个偏“执行”,但底层成因往往一致:用户可控的路径进入了文件操作接口,而这个接口没有做足够的权限边界控制。

1.2 根因:用户可控路径直接进入文件接口

说得直白一点,这类问题就是“前端传了一个文件路径或文件名,后端不做判断,直接把它当参数用在了文件函数上”。

看一段最简单的示意代码:

$file = $_GET['file'] ?? ''; // 开发者以为这一步能防住目录穿越 $file = str_replace('../', '', $file); $content = file_get_contents('/var/www/app/data/' . $file); echo $content;

这段代码有两个非常典型的问题。第一,str_replace只替换了字符串中的../,攻击者用....//或者经过 URL 编码的变体,一次替换后依然可能形成有效的../。第二,文件读取基路径看似限制在/var/www/app/data/,但路径拼接和参数校验都是用户可控的,基路径约束形同虚设。

我自己的经验是:黑名单方式永远只能挡已知攻击形态,而攻击者最擅长的就是绕过已知形态。所以这类漏洞的真正根源不是“过滤没写全套”,而是“把文件路径选择权交给了调用者”。在架构设计上,调用者只需要提交一个业务标识,比如文件 ID、页面名称、语言代码,服务端再根据这个业务标识映射到具体文件路径,才是更稳妥的思路。

1.3 为什么“下载漏洞”摇身一变就成了“包含漏洞”

很多系统里,“读取文件内容并展示”和“把文件加载为代码模块”往往只有一个函数之差。用 PHP 举例,file_get_contents是纯读取,但include就不一样了——它先把文件内容当作 PHP 代码解析,然后再输出结果。

$page = $_GET['page'] ?? 'home'; include($page . '.php');

如果攻击者传入page=../../../../tmp/test,系统可能去包含/tmp/test.php;一旦/tmp/test.php内容可被他控制,里面的任意 PHP 代码都会被执行。这就是为什么同一个参数既可能被报成“任意文件下载”,也可能被报成“文件包含导致命令执行”——关键要看后端到底用的是读取函数还是包含函数。

我还碰到过一些更隐晦的场景:一类功能提供了“预览文件内容”能力,底层实现直接用了模板引擎加载,结果用户给个.php文件路径,模板引擎把它当模板渲染时解析了内嵌代码。这类问题排查起来更费时,因为从表面看,功能本身设计目的就是输出文件内容,和下载漏洞的表现非常相似,但实际执行路径完全不同。区分这两条线,对后续判断危害、制定修复方案都至关重要。

2. 这类问题到底容易出现什么地方

2.1 业务功能名单:先把最常踩的点画出来

先讲一个实战观察:文件读取和文件包含漏洞极少出现在核心业务逻辑里,反而集中在一些“看起来没技术含量”的外围功能上。这些功能容易被开发低估,安全测试也常被忽略,等到被利用时才发现权限边界根本没收住。

从过往排查经验看,高频风险位置大致是这些:

  1. 下载中心:比如download.php?file=xxx.pdf、/api/file/get?filename=xxx。
  2. 附件预览:在线预览合同、图片缩略图、文档转图片,参数常带路径。
  3. 数据导出:报表导出、日志导出、用户数据打包,后端拼路径后压缩输出。
  4. 页面动态加载:index.php?page=news、?module=admin这类参数对路径影响最直接。
  5. 模板或语言切换:?theme=default、?lang=en,容易进入主题目录、语言包目录读取文件。

这些功能在开发时很容易被写成“把参数直接拼到路径里”,因为开发者自己心里清楚预期路径长什么样,于是想当然地省略了校验。可攻击者不需要知道完整业务路径,他们只需要把../往上跳,总能跳到一个值得读的位置。

2.2 一个拼接引发的连锁反应

低危漏洞变高危,往往不是单点造成的,而是利用链形成了。用一个典型流程来说明为什么这类问题值得认真对待:

第一步,攻击者通过某个下载接口把配置文件的读取变成了现实,比如拿到了数据库连接串、Redis 密码、对象存储密钥。第二步,他再通过源码审计或者代码泄露发现应用里还有另一个文件包含点,正好能包含日志文件。第三步,利用日志注入写入一段可执行代码,再通过包含点触发,直接在服务器上拿到执行权限。整个过程里,你很难说哪一个节点单独看是致命的——下载漏洞看起来只是信息泄露,日志包含看起来只是技术彩蛋——但串起来就是一条完整的打穿链路。

我参与过的一次模拟项目里,攻击入口就是一套答辩系统附件的下载接口。对方没有暴力破解任何账号,仅仅修改了file参数的值,就把系统源码、数据库配置、运维脚本全部读走了,随后利用源码里的硬编码密钥连上内网机器,完成了横向移动。事后复盘时发现,这个下载接口上线前只做过“能否下载附件”的功能测试,没有任何权限边界测试。

如果你是开发或运维,看到这类功能时最该补的功课就是:如果参数被反复修改到其他路径,系统会如何表现?底层函数是只读取,还是会执行?用户是否有必要知道真实文件路径?这几个问题想过一遍,就比单纯加几道过滤靠谱得多。

3. 授权测试里的检测与排查路线图

3.1 动手之前先把场景摸清楚

不管你是做安全评估,还是自查自纠,拿到一个目标后,我都不建议直接拿目录穿越 payload 一通乱打。先做信息收集,把场景摸清楚,效率和安全都会高很多。

首先要确认目标使用了什么语言和框架。PHP 项目通常有index.php、include等特征;Java 项目常见.do、.action后缀,文件读取更多在控制器层;Node.js 项目重点看静态服务目录和路由参数。语言不同,伪协议和绕过方式都不太一样,但底层路径校验原理是通用的。

其次,把参数结构梳理清楚。正常请求里?file=report.pdf和?path=uploads/2025/report.pdf代表的可控程度完全不同。前者只需要考虑文件名部分,后者整段路径都能控制,危险性更高。最好先发一次正常请求,记录响应内容长度、状态码、响应头特征,后面用它和异常请求做对比,更容易判断是否命中敏感文件。

另外要确认一个原则:所有检测都要在授权范围内进行。对生产系统的未授权测试既有合规风险,也可能影响线上服务。我自己习惯先在测试环境或本地复现,再到授权目标上做最小化验证。

3.2 手动测试从浅到深

手动测试的核心思路,是从“正常输入”逐步过渡到“异常输入”,观察系统在每一步的反应。以下这套流程,我在评估中反复使用,推荐按顺序来。

第一步,参数探测。把原始参数改成不存在的文件名、特殊字符、长字符串,观察报错信息。很多应用会把错误堆栈直接返回,里面往往带着真实绝对路径,这对后续构造 payload 帮助很大。比如返回/var/www/html/download/index.php,基本就确认了目录层级。

第二步,目录穿越测试。最常见的是逐步回退目录:

/download?file=../../../../etc/passwd /download?file=/etc/passwd /download?file=....//....//....//etc/passwd

如果响应里出现root:x:0:0:root:...,说明参数可直达系统文件,漏洞成立。如果是 Windows 服务器,可以尝试..\..\..\windows\win.ini,不过要注意编码和转义。

第三步,编码绕过验证。很多系统会做简单的关键字过滤,比如过滤../、过滤..,这时常见手法是双重 URL 编码或大小写变形:

/download?file=..%252f..%252f..%252fetc%252fpasswd /download?file=..%2f..%2f..%2fetc%2fpasswd /download?file=..%5c..%5c..%5cetc%5cpasswd /download?file=....//....//etc/passwd

这里要注意,不要机械地认为“编码后请求能拿到文件就是漏洞成立”。有些服务端会先做一次解码,再交给文件函数处理;有些会做多次解码。验证成功与否,最终以前端响应内容是否真实包含敏感文件为准。

第四步,针对包含型接口测试伪协议。如果目标语言是 PHP,且参数确实会被包含函数加载,可以尝试 php 伪协议读取源码:

index.php?page=php://filter/read=convert.base64-encode/resource=config.php

这类请求返回的是一段 base64 编码的字符串,解码后就是源码。之所以用 base64 包裹,是避免 PHP 文件在加载时直接被当作代码执行,导致我们只看到执行结果而看不到源码内容。对于确定文件包含漏洞,这个手法非常高效,但它在某些配置下可能不可用,需要结合报错或日志进一步判断。

我把常见探测结果整理成一张速查表,便于现场对照:

发送内容可能观测判断
file=test.txt正常提示文件不存在参数生效,继续测试
file=../../../../etc/passwd显示root:x:0:0任意文件读取成立
file=....//....//etc/passwd显示root:x:0:0过滤可绕过
page=php://filter/...resource=config.php返回 base64 字符串文件包含可读源码
所有 payload 均返回统一错误页可能是路径校验严格或虚拟主机配置考虑转向源码审计

3.3 从防御视角看日志与告警侧重点

检测不只是攻防测试时的事。日常安全运营里,日志和告警也能捕捉这类漏洞的痕迹。我见过不少系统被扫描器刷过几百次,但因为告警规则太粗,管理员完全没注意到。

重点盯两类请求特征:一类是包含大量../、%2f、%252e等路径穿越特征的请求;另一类是文件读取接口突然出现大量不同参数值、不同路径后缀的访问。后者在业务上解释不通,大概率是自动化工具在遍历敏感文件。

防御侧可以预设一些简单规则,比如对已知敏感文件名建立黑名单告警:passwd、shadow、config.php、web.xml、.git/config、application.yml。请求中出现这些名字,直接提升关注等级。同时,关注请求响应的大小变化。正常附件下载接口返回文件大小是稳定的,一旦同一个接口反复返回不同大小或异常状态码,多半有人在试探。

日志侧还有一个容易忽略的点:如果文件包含类漏洞被利用,业务日志和 Web 访问日志里可能会出现“文件内容被当作脚本执行”的迹象,比如访问日志里出现脚本报错、进程启动异常、异常文件创建等。把这些事件串起来看,比单个特征判断更可靠。

4. 修复加固不能只防一个方向

4.1 代码层面:白名单与真实路径校验一起上

修复这类漏洞,最核心、最直接的点在代码层。我强烈不建议继续依赖黑名单过滤,哪怕你加了好几层str_replace,也只是在和攻击者比谁更会绕。正确做法是两个方向同时做:业务参数白名单化,文件路径真实化校验。

先说包含型功能。如果页面加载、模板切换只需要几个固定页面,那就直接做映射白名单,不要把用户输入直接拼进路径:

<?php $allowMap = [ 'home' => '/var/www/app/views/home.php', 'about' => '/var/www/app/views/about.php', 'news' => '/var/www/app/views/news.php', ]; $key = $_GET['page'] ?? 'home'; if (!isset($allowMap[$key])) { http_response_code(400); exit('bad request'); } $file = $allowMap[$key]; include $file; ?>

这段代码的关键点在于:用户输入home、about这些业务标识,而不是文件路径。白名单之外的任何输入直接拒绝,基本断掉了路径穿越的可能。对这类设计,“最安全的路径就是没有路径”。

再说下载型功能。如果确实需要支持用户传入文件名或相对路径,那么必须用真实路径校验兜底:

<?php $baseDir = '/var/www/app/uploads/'; $requested = $_GET['file'] ?? ''; // realpath 会把 ..、符号链接、路径拼接后的真实结果算出来 $realPath = realpath($baseDir . '/' . $requested); // 文件必须真实存在 if ($realPath === false) { http_response_code(400); exit('bad request'); } // 文件必须落在 baseDir 内,前缀匹配必须基于绝对路径 if (strpos($realPath, realpath($baseDir)) !== 0) { http_response_code(400); exit('bad request'); } if (!is_file($realPath)) { http_response_code(404); exit('not found'); } readfile($realPath); ?>

这里最容易被看漏的是strpos前缀匹配。如果直接用字符串前缀判断,攻击者构造一个像/var/www/app/uploads_evil的路径也可能被放过;所以必须两边都先realpath,再用真实路径比较。同时,is_file检查能防止把目录、设备文件等非普通文件当作下载内容返回。

补充一个细节:有人会只用basename()过滤文件名,认为这样能去掉路径部分。但basename()只取最后一段路径,如果功能本身需要保留目录结构,它就会误伤正常功能;更关键的是它无法拦截一些编码、畸形路径的处理差异。所以basename可以作为第一道防线,但不能是唯一防线。

4.2 服务配置与权限同步收紧

代码修完,并不意味着万事大吉。运行环境和系统权限同样需要加固,否则即便代码层有小遗漏,纵深防御也能兜住一部分风险。

如果用的是 PHP 环境,建议在配置文件里做几项限制,前提是确认业务不依赖这些能力:

open_basedir = /var/www/app:/tmp allow_url_include = Off allow_url_fopen = Off

open_basedir会把 PHP 的文件操作限制在指定目录内,即便代码里出现路径穿越,函数层也会直接拒绝访问目录外的文件。allow_url_include关闭后,包含远程文件(RFI)的基本通道就没了。不过要提醒一点,open_basedir对真实路径解析后的文件仍然可能放行,所以它属于运行时兜底,不能替代业务层的白名单校验。

除了语言配置,Web 服务器的目录权限也要收紧。常见的做法是禁止站点目录下的敏感后缀文件直接被 URL 访问:

location ~* \.(ini|log|bak|conf|sql|sh)$ { deny all; return 403; }

这个配置更多是防“用户猜路径直接抓文件”,比如通过/backup.sql、/.git/config这类的直接请求。它不修复应用漏洞,但能把你最不想暴露的文件先挡住一层。

另外,应用运行账号的权限必须最小化。很多服务用 root 或高权限账号跑 Web,一旦出现文件读取漏洞,攻击者可以轻松读取/etc/shadow、密钥文件、其他用户目录。梳理一遍应用目录的属主和权限,把 Web 进程账号限制在业务目录内,是性价比很高的加固手段。

4.3 架构层面治本:不暴露真实路径

在我处理过的重灾区项目里,单纯修代码往往不够,因为开发者下次加需求时又可能写出类似代码。要从架构上治本,我建议把“真实文件路径”从用户侧彻底抹掉。

现在比较稳妥的思路是文件 ID 化:数据库里存文件的唯一 ID 或哈希值,下载接口只接收这个 ID,再由服务端根据 ID 反查真实存储路径。用户永远不知道文件落在磁盘的哪个位置,也就无法构造路径穿越。这个思路和对象存储的预签名 URL 类似:对外只暴露一个带时效的访问凭证,服务端解析后重定向到实际存储位置。

对于必须保留真实路径的高级管理功能,也可以加一层“代理读取”:前端请求/file/preview?id=xxx,后端根据 ID 查路径、校验权限、设置严格响应头,再通过内部函数输出文件流。这样文件不直接暴露在 Web 可访问目录下,减少了通过 URL 猜路径的概率。

架构调整会比改一行代码复杂,但它能显著降低整类问题的复发率。如果团队资源有限,至少第一步做到代码白名单校验,也能挡掉大多数常见攻击。

5. 现场经验:最容易踩进去的坑

5.1 坑一:看到../就当漏洞

不少人在测试报告里看到../../../etc/passwd能返回 root 开头的内容,就认定漏洞成立。其实有些系统虽然接收了路径穿越参数,却在后续做了真实路径拼接校验,最终读取的并不是系统文件,而是业务目录里的某个同名占位文件,或者干脆返回的是统一错误页。判断漏洞是否成立,最终要看两件事:参数是否实际影响了读取路径,响应内容是否真的来自预期范围之外。如果把所有带../的请求都当漏洞,报告不仅失真,还会让开发团队失去信任。

5.2 坑二:过滤一次就以为安全了

这是代码修复里最常见的问题。比如用str_replace('../', '', $file)清理路径,看起来把经典穿越姿势挡住了,但攻击者用....//或..%2f变体,一次替换后依然可能变成可用的../。还有的系统做了正则过滤,却忘了过滤..%5c、..%252f等编码形态。

我个人的习惯是:收到修复方案后,会专门列一组绕过测试集去验证。测试集不追求多,但必须覆盖双重编码、超长路径、混合分隔符、大小写变形、Unicode 编码这几类。任何号称“防穿越”的过滤器,如果过不了这套测试集,都说明它还不值得上线。

5.3 坑三:低估了“包含执行”这一步

有时候一个接口功能是“预览文件”,后端却用了模板引擎或动态加载机制,用户传入的路径不仅会被读取,还会被当作代码执行。如果只从输出结果看,它确实只是“任意文件读取”,但底层执行链完全不同。我在做测试时有个习惯:遇到文件读取接口,会顺手试几组 PHP 伪协议或模板表达式,确认是否存在“非预期加载”的可能性。这一步会改变漏洞定级和修复优先级,值得多花几秒。

5.4 坑四:目录校验写得“看起来没问题”

还有一种修复写法,看起来做了路径校验,实际还是破了个口子:

$real = realpath('/var/www/html/' . $file); if (strpos($real, '/var/www/html/') === 0) { readfile($real); }

如果攻击者上传了一个 PHP 文件到/var/www/html/uploads/shell.php,然后构造参数uploads/shell.php,这个校验会通过——因为文件确实在网站目录内。当接口只是读取时,问题可能还没那么严重;但一旦同一个文件能被包含功能加载,就成了代码执行。所以,校验目录范围必须基于业务边界,而不是简单用整个 Web 根目录兜底。上传目录和模板目录都不应该拥有执行权限,如果业务允许,最好放到 Web 根目录之外。

5.5 我自己的排查习惯

最后分享几个日常工作中的小习惯。第一,我手上常备一份“敏感文件对照表”,里面记录了常见系统文件、框架配置、容器元数据地址,测试时按顺序尝试,能快速确认漏洞影响面。第二,任何一次漏洞验证,我都会保留请求和响应的完整截图,不只是记录 payload,避免后续复盘时说不清现场情况。第三,提交修复建议时,我会同时说明“这里为什么不能用黑名单”和“建议改成白名单或真实路径校验”,这样开发同事更容易理解问题本质,而不是机械地加一个过滤函数。

我在实际项目里感受最深的一点是:漏洞本身往往不复杂,复杂的是对漏洞影响链的认知。文件包含和任意文件下载,最终能造成多大破坏,取决于开发者给了攻击者多少可操作空间。如果你能把“路径参数可控”这六个字理解透,再把这类问题从头到尾过一遍清单,大部分风险都能提前拦住。这套方法在不同语言、不同框架下反复验证过,希望对你有用。

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

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

立即咨询