文件上传漏洞详解——原理、测试、绕过与防御
头像上传接口,直接传 shell.php,返回「仅支持 jpg/png 格式」。Burp Repeater 里把 multipart 表单中自报的 MIME 从 application/octet-stream 改成 image/png,文件名不动:
1 | ------x |
返回上传成功,路径 /uploads/shell.php。浏览器访问,脚本直接执行。服务端校验的是 multipart 里自报的 MIME 类型,这个字段完全由客户端控制——只看它的校验等于没有校验。
同类的站还会拦后缀。把文件名改成 shell.jpg、内容换成 PHP 代码,后缀校验放行,存进去的仍是一段服务器可解析的代码——只要上传目录有执行权限,这就是一个 RCE。
文件上传的问题从来不止「传没传上去」:真正要问的是文件被存成了什么、会不会被当资源执行。OWASP Unrestricted File Upload 把这类问题的危害整理成完整清单:代码执行、客户端攻击(XSS)、文件系统或数据库过载、向后端转发攻击、页面篡改,评级时可以对照。
漏洞原理:存进去,还要被执行
漏洞成立需要两条同时满足:
- 恶意内容能落盘——类型、后缀、内容三层校验至少一层可绕过
- 落盘的文件会被解析执行——存储目录有执行权限、后缀被中间件绑定到脚本 handler,或站点存在文件包含点
第 1 条决定能不能传上去,第 2 条决定传上去有没有用。二者攻击面完全不同:前者攻应用代码的校验逻辑,后者攻部署配置与架构。不少测试卡在第一条打通后没有下文,通常就是第 2 条不成立——目录不给执行权限,或文件被转存到对象存储,这时要转向组合链而不是继续调 payload。
PHP 的上传处理集中在 $_FILES 超全局数组与 move_uploaded_file:前者接收文件名、MIME、大小、临时路径,后者把临时文件移动到指定位置。一个值得记的实现细节:目标文件已存在会被直接覆盖。
校验逻辑几乎都写在「移动到哪里」这一步,漏洞也集中在这——路径参数受 open_basedir 约束的只有目标一侧,源路径不受限,而路径末尾的特殊写法、数组形态的参数都会改变校验与落盘的对应关系,后面几节的多个手法正源于此。
服务端校验按实现层次分五类,测试与绕过都围绕这个分层展开:
| 层 | 校验内容 | 常见实现 |
|---|---|---|
| 前端 JS | 后缀、大小 | 表单事件里正则匹配 |
| HTTP 层 | MIME 类型 | 读 $_FILES['type'] |
| 文件名层 | 后缀黑白名单 | pathinfo 取后缀 + 数组比对 |
| 内容层 | 文件头、图像合法性 | getimagesize、GD 重采样 |
| 架构层 | 存储与执行分离 | 目录禁执行、独立存储域名、OSS |
测试流程:先定位校验在哪一层
黑盒测试四步,顺序不能反:
- 基线——传一个正常图片,记录成功响应、文件返回的 URL 与命名规律(是否保留原名、是否加随机前缀)
- 改后缀——同一请求把
filename改为shell.php重放。数据包还没发出浏览器就提示类型错误,是前端校验;请求正常发出、响应里报类型错误,是服务端校验 - 改 MIME——后缀拦了就单独改
Content-Type为image/gif,定位校验是否只看 MIME - 改内容——MIME 也过了就换文件头,逐层下探到内容校验
前端拦截永远可以绕——禁用 JS 或直接改包即过,它拦的是普通用户,不是测试者。判断它的价值只在于排除一层干扰,省下后面几次无效重放。
每一步都在缩小「校验在哪」的范围。定位之后剩下的事按层展开:文件名层攻校验实现,内容层攻图片处理链,架构层的坑落在配置里。
后缀与类型校验的绕过
绕过方向由校验实现决定,三条主线:换后缀、换写法、改路径归属。
黑名单类校验,攻覆盖面。 黑名单列不全、比较不严、替换不彻底都算覆盖面问题:
- 大小写混写——
shell.pHp,部分实现用不区分大小写的正则拦、却拿原串落盘 - 替换只跑一次——删除关键字的代码只执行一轮替换,提交 pphphp,删完后恰好剩 php。测法是看落盘的文件名:提交
1.pphphp存成1.php即命中 - 名单漏项——PHP 除 .php 外还有 .php3、.php5、.phtml、.pht、.phar 等后缀,是否被解析取决于中间件映射:Debian 系发行版默认只认 .php、.phar、.phtml,其余需额外 AddHandler;IIS 6 默认把 .asa、.cer、.cdx 映射到 asp.dll,IIS 7 起默认只剩 .asp 与 .cer。测试时按目标环境的 handler 映射确认。PayloadsAllTheThings 的 Upload Insecure Files 按 PHP/ASP/JSP 分语言列了默认可执行后缀全集,测黑名单时对着它跑字典
- 路径归一化——黑名单只判扩展名是否在列表里,保存名提交
shell.php/.时 pathinfo 取到的扩展名为空、不在列表,放行;Windows 落盘时末尾的 /. 被归一化掉,文件写成 shell.php。同一原理的变体是末尾带点与末尾带空格的写法
白名单类校验,攻比较逻辑本身。 白名单封住了后缀面,突破口转向「拿什么参与比较、拿什么落盘」:
%00截断——文件名里插一个真实的 0x00 字节再接 .jpg(multipart 的 filename 里打百分号形式的 %00 不会被解码成空字节,要用 Burp 的十六进制编辑插入)。比较发生在 PHP 层、二进制安全,取到 .jpg 过白名单;落盘发生在 C 层、遇空字节截断,写成 shell.php。老版本 PHP 的路径函数接受路径中的空字节:fopen、include 等的空字节检查自 PHP 5.3.4 引入(CVE-2006-7243),而 move_uploaded_file 的同类保护 2015 年才补齐(CVE-2015-2348,PHP 5.6.7 修复)——同一手法在不同函数上的存活版本区间并不一致,遇到老系统值得分开测数组参数——保存名参数提交成数组,比较与拼接用了不同的下标。upload-labs 最后一关的源码里,白名单用
end()取最后一项校验,落盘名却用reset($file) . '.' . $file[count($file) - 1]拼接:1
2save_name[0] = shell.php
save_name[2] = gifend() 取到 gif 过白名单,落盘名却按数组最后一项的下标拼接,指向不存在的 [1]——拼出末尾带点的 shell.php.,Windows 归一化后落成 shell.php。校验和落盘用了两套取值逻辑,是这类问题的共同特征
配置文件当目标。 Apache 启用 AllowOverride 时,.htaccess 本身就是可上传目标——上传一行 AddType application/x-httpd-php .png,此后目录里所有 png 都按 PHP 执行,后缀校验从此形同虚设。IIS 的对应目标是 web.config,uWSGI 是 uwsgi.ini(其 @() 语法可直接执行命令)。这类手法不绕校验,直接改写解析规则。
Windows 系统特性补充
Windows 对文件名末尾字符的处理是多个绕过的根源:
move_uploaded_file与fopen遇到foo.php后跟\x2E(.)、\x2F(/)、\x5C(\)的组合,会归一化回foo.phpinclude/require容忍的是另一组尾部字符:\x20(空格)、\x22(”)、\x2E(.)、\x3C(<)、\x3E(>)——文件包含绕过后缀限制时用这组- 末尾空格与点在创建文件时被系统去除
- NTFS 交替数据流:
shell.php::$DATA落盘为shell.php主流;file.asax:.jpg先创建空文件,再借短文件名改写内容 - PHP on IIS 保存文件时
>与<会分别被转成?与*,web<<可顶替web.config
来源:PayloadsAllTheThings Upload Tricks,其中 IIS 字符转换出自 Soroush Dalili 的研究。
内容校验与执行条件
内容层校验看的是文件本体,绕过思路从「伪装成图片」到「真的当图片」递进。
文件头伪装。 getimagesize 一类函数只读头部字节,文件开头加上 GIF89a(或 JPEG 的 \xff\xd8\xff 起始字节),后面跟 PHP 代码,即通过校验——存成的文件是「带 PHP 代码的图片」,要执行还需解析或包含配合。
二次渲染对抗。 用 GD 库重采样后再存储(上传头像最常见),图片数据被重新编码,简单嵌在正文的代码会被抹掉。对抗方式是先上传正常图片、下载渲染后的成品、对比出未被重写的区域(如 PNG 的 PLTE 块、JPG 的注释段),把代码塞进保留区再传。构造这类文件用现成脚本:PayloadsAllTheThings 的 Picture Compression 目录里放着能在 JPEG 重压缩后存活的 payload 图生成器(createBulletproofJPG.py 等)。二次渲染的文件同样要靠文件包含执行。
图片马 + 包含链。 上面两条产出的图片马本身不执行,常见的执行通道是站点自身的文件包含点(include($_GET['file']))。上传与包含两个弱点单独看都是低危,组合成链就是 RCE——这也是上传漏洞评级时必须查包含点的原因。
条件竞争。 校验逻辑是「先落盘、再检查后缀、不合格就删除」时,落盘到删除之间存在窗口。上传侧用 Burp Intruder 不间断重放上传包,访问侧同时高频请求猜测的文件路径,任一次请求落在窗口内即写入稳定 webshell:
1 | fputs(fopen('x.php','w'),'<?php eval($_REQUEST[1]);?>'); |
1 | 上传线程:POST /upload × N |
文件名可预测是前提——保留原名或时间戳命名的场景才有竞争窗口,随机重命名直接封死这类利用。
文件名本身是输入。 后缀之外,原始文件名经常原样进数据库或页面:进 SQL 语句是注入点,回显在文件列表是存储型 XSS,拼进下载路径是路径穿越。测试时把文件名换成对应 payload,落点在上传记录页、日志页、后台审核页的尤其值得看。SVG 与 HTML 文件上传后能直链访问的,等于站内可执行 JS,利用面见XSS 漏洞详解——原理、类型、测试与绕过。
中间件与解析漏洞
应用代码校验无懈可击,中间件配置照样能送出执行权。三个经典场景:
| 中间件 | 触发配置 | 形态 |
|---|---|---|
| Apache | AddHandler application/x-httpd-php .php |
shell.php.abc 中段含 .php 即被绑定 PHP handler |
| Nginx + FPM | cgi.fix_pathinfo=1 且未启用 FPM 限制 |
/shell.jpg/.php 回退解析成 shell.jpg 执行 |
| IIS 6 | 默认 | shell.asp;.jpg 分号截断;*.asp 目录内任意后缀按 ASP 执行 |
Apache 的多扩展名机制是第一种场景的根源。mod_mime 官方文档写明:多扩展名文件中最右侧扩展名决定 media type,但 handler 优先于 media type——配置用 Add* 指令绑定 PHP 时,文件名中段出现 .php 就足以绑定 handler。官方同时给出修复方向:用 <FilesMatch "\.php$"> 配 SetHandler,并明确「依赖这类合成类型的配置应当避免」。测试时传一个 shell.php.abc,访问后返回 200 且执行即命中。
Nginx 的场景在 FPM 侧。 Nginx 自身不解析 PHP,/shell.jpg/.php 这类路径被原样交给 FastCGI 后,PHP-FPM 在 cgi.fix_pathinfo=1(历史默认值)时把不存在的脚本回退解析成 shell.jpg 执行。官方在 FPM 池配置里提供了兜底项 security.limit_extensions:限制 FPM 允许解析的主脚本扩展名,当前默认 .php 与 .phar——这条链成立的前提是该值被显式置空(置空才允许全部扩展名),或运行在 5.3.9 之前还没有该指令的 FPM 上,默认配置下 /shell.jpg/.php 会被 FPM 拒绝。vulhub 上有这套配置的现成复现环境。
CMS 与第三方编辑器
- 第三方编辑器——UEditor、KindEditor 等自带的上传接口独立于业务代码,UEditor .NET 版的远程抓图接口(
controller.ashx)曾长期存在任意文件上传,只需要能访问controller.ashx且未做鉴权 - 已知 CMS——通达 OA v11.x 的上传利用链(任意文件上传与文件包含组合)、各类建站系统的主题/插件上传点;vulhub 有 Apache/Nginx 解析漏洞与 PHP 包含环境的现成复现(
httpd/apache_parsing_vulnerability、php/inclusion),通达 OA 这类 CMS 需自备安装包 - 判别思路——页面引用了编辑器资源目录(
/ueditor/、/kindeditor/)或指纹识别出 CMS,先查对应版本已公开的上传漏洞再手工测,效率高于黑盒
危害评估与边界
「传上去了」不等于 RCE。评估一个上传点,按三问定级:存到哪、能否直接访问、会不会被解析。
| 场景 | 危害 | 说明 |
|---|---|---|
| 直链可访问且后缀被解析 | 严重 | 直接 RCE |
| SVG/HTML 直链同源可访问 | 高 | 存储型 XSS,打后台价值最高 |
| 仅存储,目录无执行权限 | 低 | 文件是死的,需组合链利用 |
| 文件名进数据库或页面 | 视落点 | SQL 注入、XSS,单独评级 |
架构层的存储方案直接掐断执行链:文件转存到独立域名(上传走 upload.、访问走 images.)或 OSS 对象存储后,上传目录下没有脚本执行的概念,第 2 条成立条件不存在。这类目标上传点的期望值应下调为「存储型利用」——XSS、文件名注入、或寻找下载/包含侧的二次漏洞。
条件竞争与后缀字典爆破在真实目标上的代价很直接:高频请求触发 WAF 告警、IP 封禁,上传行为在日志中全部留痕,后续测试同步受影响。竞争窗口类手法只在确认靶场或授权目标的场景使用。
测试写入的 webshell 与图片马是持久化痕迹,测试结束前全部删除并记录清单——它们留在服务器上既是审计问题,也可能被第三方扫描利用。
防御方案
第一层,白名单 + 服务端重命名。 后缀白名单比对放在服务端,比较与落盘用同一份值(数组绕过的教训);落盘名随机生成、扩展名由白名单映射,原始文件名只作展示用——这一条同时封掉条件竞争、路径归一化与文件名注入。
第二层,存储与执行分离。 上传目录整体不接 PHP 解析——前缀匹配 ^~ 优先于 \.php$ 这类正则,PHP 请求根本到不了 FPM,枚举后缀写 deny 反而容易漏(.phar、.pht):
1 | location ^~ /uploads/ { |
PHP-FPM 侧保持 security.limit_extensions 默认值不动,Apache 侧用 <FilesMatch "\.php$"> 配 SetHandler 替代 Add* 指令,并关闭上传目录的 AllowOverride。条件允许直接把文件转存 OSS 或独立存储域名,执行链从架构上不存在。
第三层,内容与架构校验。 白名单之外再过一遍文件头与图像合法性(注意 getimagesize 可被文件头伪装绕过,只作辅助);图像处理管道及时升级——ImageMagick 的历史漏洞(ImageTragick)证明处理组件本身也是攻击面。
完整的工程清单见 OWASP Unrestricted File Upload 的防护建议与 PortSwigger 的文件上传专题(覆盖校验缺陷、路径穿越、服务端配置缺陷三类场景,附实验室)。
测试速查表
| 场景 | 先看什么 | 优先尝试 |
|---|---|---|
| 只拦后缀 | 黑名单还是白名单 | phtml/php3、大小写、双写、/. 归一化 |
| 拦 MIME | 是否只读 Content-Type |
改包 image/gif |
| 拦文件头 | 只查头还是重采样 | GIF89a 图片马;重采样用保留区构造 |
| 白名单严格 | 保存路径是否可控 | 数组参数、老版本 %00 |
| 传上去不执行 | 目录权限、解析配置 | 中间件解析、文件包含链 |
| 先存后删 | 文件名是否可预测 | 条件竞争(仅授权目标) |
| 名单/环境未知 | 是否部署在 Nginx/Apache/IIS | 对应中间件解析特征探测 |
定位校验层占测试工作量的大头,绕过手法是定位之后的执行问题;防御侧真正起作用的是白名单重命名与存储执行分离,内容校验只作辅助。






