用BP抓包手工测试文件上传漏洞的完整记录

一个上传头像引发的漏洞

去年测一个企业官网的后台,有个功能是上传员工头像。我随手传了个PHP文件,改了后缀名,结果服务器直接执行了。

就是这么简单粗暴。

从那以后,每碰到上传功能我都会格外留意。这篇文章把我在文件上传这块积累的测试方法整理一下,都是实际用过的,不是那种教科书式的理论。

一、先搞清楚上传逻辑

拿到上传功能,我第一件事不是动手测,而是先观察。打开F12的Network面板,选一个正常的图片上传,看请求是什么样的。

这次测的是一套内容管理系统,上传接口是这样的:

bash 复制代码
POST /api/upload HTTP/1.1
Host: admin.example.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryABC

------WebKitFormBoundaryABC
Content-Disposition: form-data; name="file"; filename="avatar.jpg"
Content-Type: image/jpeg

(图片二进制数据)
------WebKitFormBoundaryABC--

响应返回:

javascript 复制代码
{
    "code": 0,
    "data": {
        "url": "/uploads/2024/12/avatar_12345.jpg",
        "filename": "avatar_12345.jpg"
    }
}

注意几个关键信息:

  • 上传路径是/uploads/2024/12/

  • 文件名被重命名了,加了个时间戳

  • 响应里没有返回文件类型,只有路径

观察这个接口,我心里大概有底了:路径是固定的,文件名加了时间戳,但后缀名看起来是保留了原始文件的后缀。

二、上手测试:从最简单开始

第一波:直接传脚本

先把正常的图片改成PHP文件。我用Notepad写了个最简单的:

php 复制代码
<?php phpinfo(); ?>

然后把文件名改成test.php,上传。

结果服务器直接拒绝了,返回"文件类型不允许"。看来后端做了MIME类型校验。

用BP改一下请求包,把Content-Type: application/octet-stream改成Content-Type: image/jpeg,再放行。

这次上传成功了,响应返回了路径:

javascript 复制代码
{"url": "/uploads/2024/12/test_12345.php"}

浏览器访问这个路径,phpinfo()页面正常显示出来了。

到这里已经确认了:服务端只校验了前端传上来的Content-Type,根本没检查文件真实内容。这个漏洞太明显了。

📸 此处应有截图:BP里修改Content-Type的请求包

第二波:测试各种后缀绕过

这次系统的开发做得比较糙,我把刚才的后缀绕过方法记录下来:

测试文件名 结果 说明
test.php 成功执行 直接上传PHP成功
test.php.jpg 成功执行 双后缀,服务器解析了.php部分
test.PHP 上传失败 做了大小写过滤,反而暴露了过滤逻辑
test.phtml 成功执行 phtml也在解析范围内

最意外的是双后缀test.php.jpg。这个文件名在服务器上的存储路径是test.php.jpg,但访问的时候Apache按后缀解析,遇到了.php就当成PHP执行了,后面的.jpg被忽略。

这种情况在Nginx上也存在,只要路径里包含能解析的后缀就行。

第三波:绕过内容检测

有的系统会检测文件头部,比如图片会检查FF D8 FF这个JPEG头。这种怎么办?

我找了个真实的图片文件,把PHP代码塞到图片末尾:

bash 复制代码
# 用命令行把PHP代码追加到图片末尾
echo '<?php echo "shell"; ?>' >> real_image.jpg

这个文件有合法的JPEG头,内容检测能过。上传之后如果服务器支持图片马(imagejpeg等函数),代码就能执行。

不过这次的目标系统不是图片处理场景,这个办法没派上用场,但别的项目里我确实用过。

三、深入挖掘:目录跳转和覆盖

文件上传漏洞不只有代码执行这一种,还有两个方向我觉得也值得测。

目录遍历攻击

正常上传的路径是/uploads/2024/12/filename.jpg,如果文件名里包含../呢?

用BP改文件名:

bash 复制代码
filename="../../../wwwroot/shell.php"

如果后端没做过滤,文件就传到网站根目录了。这个漏洞我测试的时候发现系统做了过滤,把../替换成了空字符串。

但有意思的是,过滤只做了一次。我输入....//,过滤后变成../,成功绕过了。

bash 复制代码
原路径:../../etc/passwd
过滤后:../etc/passwd  (过滤掉"../")

原路径:....//etc/passwd  
过滤后:../etc/passwd  (过滤掉"../",剩下的"../"还留着)

文件覆盖

这个不太算安全漏洞,但有时候能找到意外收获。上传一个同名文件,看服务器是覆盖还是重命名。

如果覆盖,而且目标文件是已知路径的配置文件或静态页面,就有可能篡改网站内容。

这次测试中发现上传相同文件名时服务器会自动加时间戳,不会覆盖,所以这个方向也没戏。

四、碰到的几个意外情况

意外一:上传了一个超大的文件

随手传了个500MB的ISO镜像,结果服务器卡住了。过了半分钟返回500错误。这个虽然不算典型漏洞,但说明了服务端没做文件大小限制,有可能被DoS攻击。

意外二:返回了绝对路径

某个请求上传失败的时候,错误信息里包含了完整的服务器路径:

bash 复制代码
上传失败:无法移动文件到 /var/www/html/uploads/2024/12/temp_123.tmp

这就泄露了网站根目录的物理路径。虽然直接危害不大,但配合其他漏洞可以利用。比如知道了路径,在SQL注入或者文件包含漏洞里就能精准定位文件。

意外三:上传的文件可以访问但没解析

有时候文件传上去了,后缀是.php,但访问的时候浏览器直接显示源码而不是执行。这种情况一般是服务器没把该目录配置成可执行PHP。测到这个程度基本就到位了,剩下的工作可以让运维去处理。

五、测上传漏洞的检查清单

把自己摸索出来的经验整理一下,现在每次测上传都会过一遍下面这些点:

第一步:绕过后缀限制

  • 直接上传目标脚本(.php/.asp/.jsp)

  • 双后缀(.php.jpg)

  • 大小写混写(.pHp)

  • 其他可执行后缀(.phtml/.php5/.shtml)

  • 利用解析漏洞(Apache、Nginx、IIS各自的特性)

第二步:绕过MIME检测

  • 修改Content-Type为image/jpeg

  • 修改Content-Type为application/octet-stream

  • 去掉Content-Type头

  • 提交空Content-Type

第三步:绕过内容检测

  • 图片马(正常图片+代码)

  • GZIP压缩的图片

  • 加注释绕过后端的字符串匹配

第四步:目录和权限

  • 测试../目录跳转

  • 测试文件覆盖

  • 查看响应是否泄露路径信息

  • 确认上传目录是否可访问

第五步:其他异常

  • 超大文件(导致服务器卡顿或崩溃)

  • 空文件

  • 文件名包含特殊字符(空格、换行、Unicode控制字符)

六、修复建议:我给开发写的报告

测试完发现问题不少,我整理了一份修复建议给开发团队,摘几个核心的:

1. 文件类型白名单

只允许特定的文件类型,而不是黑名单。白名单远比比黑名单靠谱:

php 复制代码
$allowed = ['jpg', 'jpeg', 'png', 'gif'];
$ext = pathinfo($filename, PATHINFO_EXTENSION);
if (!in_array(strtolower($ext), $allowed)) {
    die('不允许的文件类型');
}

2. 重命名文件

不要保留用户上传的文件名,用UUID或者时间戳加随机数重新命名,保存的时候用新名称。把后缀名重新拼一遍,确保是白名单里的后缀。

php 复制代码
$new_name = uniqid() . '.' . $allowed_extension;
move_uploaded_file($tmp, $upload_path . '/' . $new_name);

3. 检查真实文件类型

finfo扩展读文件头,不要相信客户端传过来的Content-Type:

php 复制代码
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $_FILES['file']['tmp_name']);
finfo_close($finfo);
if (!in_array($mime, ['image/jpeg', 'image/png'])) {
    die('文件类型不符');
}

4. 上传目录禁止执行脚本

在Apache里配置:

bash 复制代码
<Directory "/var/www/uploads">
    php_flag engine off
</Directory>

Nginx里配置:

html 复制代码
location /uploads/ {
    location ~ \.(php|phtml)$ {
        deny all;
    }
}

5. 限制文件大小

bash 复制代码
upload_max_filesize = 10M
post_max_size = 12M

七、后来

这套系统我前后测了三天,文件上传这块一共报了四个问题:直接上传PHP脚本、双后缀绕过、大小写过滤逻辑泄露、错误信息泄露路径。开发那边用了两天时间全部修完,然后找我复测了一遍才上线。

做安全测试最大的满足感就是这个------发现问题的同时能帮别人把问题解决掉。把具体的修复方案写清楚,开发修复的时候就有依据,而不是丢一句"存在文件上传漏洞"就走了。

另外想说的是,文件上传这个东西,看起来简单,实际上能玩出花来的点特别多。不同语言(PHP、ASP、JSP)、不同容器(Apache、Nginx、IIS)、不同框架都有自己的特性和坑。多测几个项目,慢慢积累自己的测试用例库,效率会高很多。


文中涉及的测试环境为授权测试,所有数据均为模拟环境生成。请勿在未授权系统上进行文件上传测试。