一个上传头像引发的漏洞
去年测一个企业官网的后台,有个功能是上传员工头像。我随手传了个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)、不同框架都有自己的特性和坑。多测几个项目,慢慢积累自己的测试用例库,效率会高很多。
文中涉及的测试环境为授权测试,所有数据均为模拟环境生成。请勿在未授权系统上进行文件上传测试。