
做Web攻防和代码审计这些年我有个很深的体会真正把站点打穿的漏洞往往不是那种花里胡哨的框架0day而是文件下载、文件删除、目录遍历、目录穿越这类被很多人当成“小问题”的文件操作漏洞。它们藏在附件预览、文件下载、缓存清理、头像上传这些平平无奇的业务功能里攻击者只需要改一下文件名参数就能把配置文件、源码备份、甚至整个应用的数据带走。这篇实操指南把我踩过的坑、复现过的链路、以及最终落地的修复方案一起梳理出来适合正在做渗透测试、代码审计或者负责业务系统安全加固的读者参考。1. 文件与目录安全漏洞全景认知1.1 四个漏洞之间的关系别再把名字混在一起先给没接触过这块的读者把概念理清楚。四个关键词经常被混着说实际是四条不同但会互相联动的漏洞链文件下载漏洞下载、导出、预览功能接收了用户传的文件名直接拼到服务器路径上导致任意文件可读。文件删除漏洞删除功能同样没做路径校验攻击者可以删掉业务关键文件。目录遍历漏洞服务器把某个目录的文件清单直接列出来不用猜文件名敏感文件一览无余。目录穿越漏洞用../这类相对路径跳出本来限定的目录到达文件系统的其他位置。下面用一张表快速对照漏洞类型典型触发位置核心危害一句话判别文件下载漏洞附件下载、导出、预览任意文件读取、源码泄露下载接口拼了文件路径文件删除漏洞清理缓存、删除附件、删除临时文件文件被删、业务不可用删除接口没校验路径和权限目录遍历漏洞静态目录、备份目录、无索引页面目录清单泄露、敏感文件暴露直接列出目录内容目录穿越漏洞下载、上传、解压、文件包含路径越界读写、联动利用../越过限制目录它们之间的联动也很常见先用目录遍历发现可疑备份文件再用下载漏洞把它读出来或者用穿越漏洞绕过限制配合删除漏洞把安装锁删掉。很多人以为这些漏洞属于“低危”实际上组合起来就是一次完整的高危攻防链路。1.2 为什么这类漏洞在攻防里优先级很高我做过不少安全应急和红蓝对抗这类文件漏洞的出镜率极高主要原因有三个。第一暴露面广几乎任何一套管理系统都有附件上传、文件下载、数据导出、缓存清理功能每个点都是潜在入口。第二利用门槛低不需要复杂工具浏览器加Burp Suite改参数就能验证甚至手工构造URL就能做到。第三联动性强下载漏洞能带出源码和配置源码到手后进一步审计往往能挖到数据库口令、云密钥、内网地址从单一漏洞变成横向突破的跳板。相比之下修复成本其实不高很多时候只是少拼一个参数、多一次路径校验的问题。之所以总是漏主要是开发的时候只考虑了功能流程没把“用户输入不能直接拼路径”当回事。2. 文件下载漏洞看似普通的功能怎么变成信息泄露通道2.1 一个下载接口为什么会变成数据泄露口子先看最常见的代码写法。PHP后端处理附件下载时经常写成这样$file $_GET[file]; $path /var/www/uploads/ . $file; if (file_exists($path)) { header(Content-Type: application/octet-stream); readfile($path); }这段代码正常用没问题比如访问download.php?filereport.pdf会读取/var/www/uploads/report.pdf并返回。问题在于$file完全由用户控制攻击者把参数改成../../../../etc/passwd拼接出来的路径就变成了/var/www/uploads/../../../../etc/passwd实际上就是/etc/passwd于是服务器把系统账户文件返回给了攻击者。有些系统会加一层后缀比如$path /uploads/ . $file . .pdf;看起来安全一些但同样可以用空字节截断老版本或者尝试file../../etc/passwd%00更常见的绕过是用file../../somefile/..之类的方式把后缀吃掉。不要觉得加了后缀就高枕无忧核心问题还是用户输入进了路径拼接。2.2 实战中的危害判定先读什么文件最划算任意文件下载能造成什么后果取决于目标服务器里有什么敏感文件。我一般会按优先级依次尝试读取系统文件/etc/passwd、/etc/hostname、/etc/hosts用来确认操作系统类型、主机名和内网IP。Web配置config.php、database.php、.env、application.yml通常会直接挂着数据库账号密码、Redis密码、云厂商密钥。源码文件下载对应语言的入口文件、路由文件通过源码审计进一步找漏洞。备份与历史文件backup.zip、*.sql、*.tar.gz很多时候备份里的数据比线上还全。私钥与凭证/root/.ssh/id_rsa、SVN或者Git保存的凭据文件。Windows环境则优先尝试C:\Windows\win.ini、C:\Windows\system32\drivers\etc\hosts以及IIS的web.config。这一条漏洞链走下去轻则网站源码泄露重则数据库被直连、服务器被横向控制。我见过一个案例攻击者只是通过一个图片预览接口的file参数读到了.env里面是云数据库的地址和密码结果整个生产库被拖走。所以说文件下载漏洞从来不是“信息泄露小问题”它是很多大事故的起点。2.3 修复实操白名单与路径归一化修复任意文件下载核心就三句话不信任用户输入、不直接拼路径、路径解析后必须落在允许的目录里。具体落地有两种做法。方法一业务ID映射推荐优先采用。前端只传业务ID比如附件ID、报告编号、文件key后端根据ID去数据库或对象存储里查出真实存储路径。这样用户完全接触不到磁盘路径谈不上穿越。方法二路径归一化校验。如果必须传文件名至少要在后端做一次严格的路径校验$baseDir realpath(/var/www/uploads); $file $_GET[file]; if ($file || $file null) { http_response_code(400); exit; } // 禁止绝对路径 if ($file[0] / || substr($file, 0, 2) \\\\) { http_response_code(403); exit; } $realPath realpath($baseDir . DIRECTORY_SEPARATOR . $file); if ($realPath false) { http_response_code(404); exit; } if (strpos($realPath, $baseDir . DIRECTORY_SEPARATOR) ! 0) { http_response_code(403); exit; } if (!is_file($realPath) || !is_readable($realPath)) { http_response_code(404); exit; } header(Content-Type: application/octet-stream); readfile($realPath);这段代码的关键在于realpath()会把所有../、符号链接都解析成最终绝对路径再和允许目录的绝对路径做前缀比较。只要解析结果不在$baseDir之内直接拒绝。之所以用$baseDir . DIRECTORY_SEPARATOR而不是$baseDir本身是为了避免攻击者把路径指向/var/www/uploads_evil这种前缀相似的目录。2.4 容易漏掉的下载场景很多团队修完常规下载接口就以为结束了实际上下面几个场景同样会中招在线预览与转码。Word转PDF、图片加缩略图或水印经常需要传入原始文件路径这里没有过滤就会变成任意文件读取。导出功能。导出Excel、CSV、报表时filename参数如果是从URL带的拼到临时文件读取的路径里依然可以穿越。静态文件服务。Nginx或Apache配置alias时没有正确闭合路径location /download/的请求可以穿越到其他目录。对象存储签名URL。如果生成URL时直接拼接用户传入的对象key并且存储桶ACL过宽等于把一个超高危下载接口直接暴露在外。这些场景的修复思路与上面一致路径必须来自可信来源访问前做目录校验。3. 文件删除漏洞一条 delete 请求就能让业务瘫痪3.1 删除功能为什么会变成任意文件删除文件删除漏洞的人气不如下载漏洞但破坏力一点不低。很多系统的删除功能设计得很随意比如删除头像、删除附件、清理临时文件时直接接收一个文件路径参数$file $_GET[file]; unlink(/var/www/uploads/ . $file);攻击者看到这个接口不需要爆破后台密码只要把参数改成../../config/install.lock、../../application/config.php、../../logs/access.log就能去删除任意在Web进程权限范围内的文件。更麻烦的是删除和下载不一样。下载只是在读删除是写操作URL一请求文件就没了没有中间确认的机会。等到运维发现文件可能已经被删了一大批。3.2 一次删除漏洞造成的破坏链我参与过的一次应急就是这种情况。攻击者发现后台有一个清理缓存接口传参是文件名没有做鉴权也没做路径过滤。他先删了网站的install.lock文件系统把安装向导重新开放了他又直接访问安装页面重新初始化了应用配置把数据库表给清了。整个过程只用了大概十几分钟中间没有任何人警觉。这种利用思路比单纯删几个文件更值得警惕删除install.lock、installed.lock等安装锁文件重新安装系统并覆盖配置。删除任务队列的锁文件或PID文件让定时任务重复执行造成逻辑绕过。删除备份文件掩盖已发生的入侵痕迹给溯源和恢复增加难度。删除index.php、模板文件、静态资源直接造成业务不可用。删除.htaccess或路由配置文件让伪静态失效、目录列举重新打开。3.3 修复与加固要点删除接口不要直接碰路径修复删除漏洞最根本的做法是让用户永远不传路径只传业务ID。// 前端传附件ID $id (int) $_POST[id]; $file $db-query(SELECT storage_path FROM attachments WHERE id ?, [$id])-fetch(); if (!$file || $file[user_id] ! $currentUser-id) { http_response_code(404); exit; } $realPath realpath($file[storage_path]); if ($realPath false || strpos($realPath, realpath($dataDir)) ! 0) { http_response_code(403); exit; } if (is_file($realPath)) { unlink($realPath); } // 删除数据库记录这里还做了属主校验只允许删除当前用户自己的附件避免水平越权。如果业务必须支持管理员删除某个用户上传的文件同样要通过服务端逻辑查询记录不能接收客户端传的路径。接口层面也不能裸奔。删除操作必须要求登录态、做CSRF防护管理类删除还要有二次确认和操作日志。不要因为“这只是个内部接口”就省略鉴权内网穿透和CSRF攻击都可能导致外部请求直接打到这个接口上。部署层面PHP-FPM、Apache、Nginx的运行用户要遵循最小权限原则。上传目录可以写但配置目录、安装锁文件所在目录、日志目录应该只读删除功能尽量走一个独立服务或者在应用层限制为只能操作upload目录下的真实文件。3.4 我遇到过的一些特殊删除利用还有两种比较刁钻的利用方式一种是配合目录穿越删除缓存目录下某个锁文件让缓存重建流程触发耗光服务器资源另一种是删除对象存储上的对象如果删除接口把本地路径和对象key做了映射攻击者改一个ID就可能导致云上的文件批量丢失。处理这类问题没有捷径只能坚持“路径源头可追溯、删除权限可控制”这两个原则。4. 目录遍历漏洞整个目录的文件清单直接送给攻击者4.1 目录遍历与目录穿越的核心区别很多读者分不清目录遍历和目录穿越这里直接用表格说明对比项目录遍历目录穿越本质服务器把目录内的文件列表返回给用户用户通过../跳出限定目录核心危害信息泄露、目录结构暴露任意文件读、写、删除常见位置静态文件服务器、无默认页的目录下载、上传、解压、文件包含一句话概括看到了不该看的东西摸到了不该摸的地方简单说目录遍历是服务器主动把目录里的文件列表列给用户看属于配置或部署问题目录穿越是用户用../跳出限定目录属于代码问题。两者名字像修复思路也不同下文分别展开。4.2 目录遍历从哪里冒出来最常见的来源有三个Web服务器开启了目录列举。Nginx的autoindex on;、Apache的Options Indexes只要目录下没有默认首页访问目录URL时就会列出一堆文件名。开发、测试、备份目录直接放在Web根目录下。常见的有/backup/、/test/、/uploads/、/files/、/temp/以及版本管理目录.git/、.svn/。对象存储桶或云盘配置成了公共列举。权限设置不当谁都能用列举桶内对象的接口枚举全部文件。如果只是文件列表泄露有人觉得不算漏洞。但配合文件名猜测和信息关联攻击者往往能直接定位到想要的敏感文件。比如看到.git/目录接下来可以直接请求/.git/config拿到仓库地址和分支信息再看/.git/HEAD、/.git/index整个源码都能被一点点还原出来。4.3 从目录列表到敏感文件中间只隔着一个点击我给大家还原一个真实场景。某系统的/uploads/目录开了autoindex攻击者访问后看到一份文件列表文件名类似2024-09-03_students.xlsx、config.php.bak、sql/backup_20240101.sql。他顺手点开config.php.bak页面直接返回PHP源码数据库连接配置全在里面。整个测试过程可能不到5分钟但这个漏洞让系统最核心的数据库口令直接暴露。除了直接读取目录列表还会泄露目录结构和文件命名规律方便攻击者构造后续的穿越路径。比如看到上传目录下有user/10086/avatar/1.jpg他就能推断出ID规律再结合下载接口做水平越权把其他用户的头像文件也下载下来。所以目录遍历的连锁危害往往比它本身更大。4.4 关闭目录列举的常规操作根据服务器类型不同修复方式如下。Nginx配置location /upload/ { autoindex off; deny all; return 404; }Apache.htaccess或虚拟主机配置Directory /var/www/uploads Options -Indexes /Directory如果这些目录确实需要对外提供访问可以指定默认文档index.html或者把敏感目录移出Web根目录通过后端受控接口下载。需要注意autoindex off只是关闭了文件列表展示并不代表文件本身不可访问。如果目录里有backup.zip攻击者只要知道文件名依然可以构造URL直接下载。真正的防护要靠访问控制加上敏感文件清理。然后要定期扫描Web根目录下的敏感文件。我常用的方法是让扫描器跑一遍常见路径字典.git/HEAD、.svn/entries、.env、*.sql、*.bak、*.zip、config.php.bak。一旦发现立刻移动到隔离区或删除。对象存储桶也要检查ACL建议默认私有只有CDN或后端服务需要访问时才显式授权并且关闭外部用户的列举权限。5. 目录穿越漏洞一次逃出监狱的经典操作5.1 穿越漏洞的触发条件目录穿越漏洞的产生必须同时满足两个条件一是后端把用户可控的参数直接拼进文件路径二是应用没有对../这类相对路径做规范化处理。除了下载、删除上传功能同样会中招。看这段示例代码$targetDir /var/www/uploads/; $target $targetDir . $_POST[savepath]; file_put_contents($target, $data);攻击者构造savepath../../web/shell.php就可能把内容写到Web目录之外甚至可执行脚本目录。解压功能也有类似的坑压缩包里的文件名如果包含../解压时会一路向上穿越业内叫zip-slip。5.2 绕过手法与系统差异做安全测试时不建议上来就只试原始../很多系统已经做了简单过滤这时候要放宽思路。URL编码%2e%2e%2f、..%2f、%2e%2e%5c有些框架会先解码一次再拼路径编码后就能绕过字符串匹配。双重编码%252e%252e%252f如果请求经过WAF解码后再由后端二次解码可以绕过一层过滤。操作系统差异Windows下反斜杠\同样可以作为路径分隔符..\..\经常被过滤规则忽略。绝对路径如果拼接逻辑允许直接传/etc/passwd或C:\Windows\win.ini不一定要用../。空字节截断%00老版本PHP和Java中可以用它截断后续拼接的后缀现在高危场景少很多但遇到老旧系统仍然值得试一下。我在测试时的一个经验是先用原始../确认基础是否存在穿越如果被拦截再逐一尝试编码变体和平台差异。每换一种绕过方式都要观察响应内容里的路径特征而不是盲目批量爆破。5.3 修复方案对比到底选哪种修复目录穿越业内几种主流方案各有取舍整理成表格方便对照方案实现成本安全强度注意事项业务ID映射中等高需要调整业务接口最推荐realpath加前缀校验低高要求文件必须存在适合下载、删除basename过滤极低低会丢失目录结构不建议单独使用黑名单过滤../低低编码绕过多不推荐open_basedir限制低中仅PHP环境生效可作为纵深防御我推荐的做法是组合拳接口层用业务ID映射服务端对每次文件操作做realpath校验同时配置open_basedir或容器只读文件系统作为兜底。以PHP下载为例修复代码在2.3已经给过Java里思路类似Path basePath Paths.get(/data/attach).toAbsolutePath().normalize(); Path targetPath Paths.get(basePath.toString(), userInput).normalize(); if (!targetPath.startsWith(basePath)) { throw new SecurityException(Invalid path); }Java的Path.normalize()会解析../再用startsWith校验前缀效果和PHP的realpath方案一致。注意一定要调用normalize()之后再startsWith不要拿原始字符串直接比较。5.4 穿越漏洞经常和哪些功能联动目录穿越很少单独存在它在攻防里更多扮演“放大器”的角色。穿越加下载变成任意文件读取拿走配置和源码。穿越加删除变成任意文件删除制造服务不可用或重置应用。穿越加上传如果上传目标路径可控可以写到计划任务、开机启动目录、Web可执行目录这是最严重的情况。穿越加解压zip-slip压缩包内的条目覆盖任意路径文件。所以在代码审计时只要看到路径处理不严格的函数我都会先记为高危然后优先找出所有调用点再评估具体能结合哪个功能形成完整利用链。6. 实操避坑与自查清单6.1 漏洞修复顺序怎么定如果你手头已经发现了一堆问题先别急着全都改按照风险排序公网可直接访问的下载接口和删除接口优先修。能读到密钥、配置、源码的点优先修。目录穿越漏洞和下载、删除接口一起处理因为穿越往往是它们的底层原因。目录遍历、敏感文件暴露这类配置问题可以放在同一轮加固里但别拖太久尤其别让.git、.env这类文件继续挂在网上。修复后记得做回归验证不要改完就认为完事了。我建议每修完一个点立刻用原始绕过方式和编码绕过方式各测一遍确认这个点真的堵住了。6.2 我踩过的几个坑这部分是我的真实经验可能比前面所有原理都更直观。第一个坑只做了黑名单过滤。开发在下载接口里过滤了../但忘了过滤URL编码。我直接请求..%2f就穿过去了。后面改成realpath校验才彻底解决。第二个坑用basename裁掉所有目录。刚开始图省事代码只保留basename($_GET[file])直接把子目录结构丢了业务方反馈多级目录附件无法下载。后来有人为了修业务直接把完整路径传给了下载函数又把安全校验绕过了等于白改。所以basename只能用于明确只有单层目录的场景。第三个坑校验时用了strpos($real, $base) 0但没在$base后面拼分隔符。当$base是/data/upload路径是/data/upload_evil/xx时前缀判断也会通过漏洞依然存在。修复时记得在base后面拼上DIRECTORY_SEPARATOR。第四个坑Nginx的alias配置。location /download/ { alias /data/files/; }这种方式看似没什么但如果请求/download../config.php一些Nginx版本或配置组合下会解析到/data/config.php。后来我统一改成了try_files加精确的location并且关闭不必要的目录alias。第五个坑忘了清理CDN和本地缓存。删除或修复漏洞后敏感文件可能仍然在CDN节点缓存里别人拿到URL照样能访问。要主动刷新缓存或者更换文件名和路径。第六个坑没有留日志。出了安全事件后想追溯发现下载、删除接口根本没打日志根本不知道攻击者看了哪些文件。建议在下载、删除、上传这三个方向都保留完整的请求日志记录原始参数但要注意脱敏别把密码本身打进去。6.3 快速自查清单最后给一张可以直接抄的检查清单用来自查文件与目录安全下载、导出、预览接口是否都改为业务ID映射或经过realpath校验删除接口是否只接收业务ID并校验属主权限上传接口是否限制文件类型、大小保存路径是否可预测解压功能是否校验压缩包内文件名是否防zip-slipWeb根目录下是否有备份文件、测试文件、版本管理目录、.envNginx或Apache是否关闭了目录列举对象存储桶是否有公共读或列举权限应用运行用户对敏感目录是否只有只读权限访问日志是否覆盖下载、删除等敏感操作修复后是否尝试过%2e%2e%2f、绝对路径等绕过方式验证建议每季度对全部Web系统执行一次清单扫描尤其是新上线和经历过改版的项目。改动越频繁越容易把这类漏洞重新引入。写到最后分享一下我个人的习惯。我现在看代码的时候只要碰到用户可控的文件名或路径参数脑子里第一反应永远是能不能改成ID不能改的话能不能用白名单实在不行再走realpath前缀校验。这些步骤看着简单但真到了攻击发生的时刻能挡住绝大部分文件读、写、删操作。另外再提一句安全不是一次性工作每次上线新接口前把这份自查清单过一遍比事后打补丁要省事得多。文件与目录漏洞的修复成本很低但漏掉后的代价可能是一个站、一台服务器、一整套业务数据。