Skip to content

<!-- title: file-upload-vuln -->

文件上传漏洞详解:原理、利用与防御

一、漏洞原理

文件上传漏洞是指 Web 应用在处理用户上传文件时校验不严,导致攻击者能够上传Web 可执行脚本文件(如 shell.phpshell.jsp),从而获得服务器命令执行权限的漏洞。在各类文件上传漏洞中,"上传 WebShell"是危害最直接的一类——从"能传文件"到"能控制服务器"往往只有一步。

它的产生逻辑是一条信任链的断裂:应用假设"上传的文件只是文件",把它原样存到 Web 可访问目录里;而服务器组件(PHP、JSP、ASP 解析器)却按"这个目录里的特殊后缀文件要当代码执行"。两边的假设一拼接,攻击者上传的 .php 文件就成了服务端的遥控器。

校验薄弱点可能出现在任意一层,教材上按位置分为:前端 JS 校验(可绕过,形同虚设)、Content-Type 校验(抓包即可改)、后缀名黑名单(总有漏网后缀或大小写/解析技巧)、文件头魔数校验(可伪造图片头)等。逐层突破的手法也因此发展成一整套攻防知识体系。

需要强调:文件上传漏洞的危害取决于"上传目录是否可执行"与"后缀是否可控"两个条件,这也是全部防御思路的出发点。

二、示例代码

以下是一个典型的"只在前端校验"的脆弱上传功能(虚构站点 example.com):

html
<!-- 前端:只允许选图片,但这段 JS 只是"礼貌提示",不是防线 -->
<form action="upload.php" method="post" enctype="multipart/form-data">
    <input type="file" name="avatar" onchange="checkExt(this.value)">
    <input type="submit" value="上传">
</form>
<script>
function checkExt(name) {
    if (!/\.(jpg|png|gif)$/i.test(name)) { alert('只能上传图片'); }
    // 注意:没有 return false 阻止提交,后端也未再校验
}
</script>
php
<?php
// upload.php —— 后端原样保存,文件名与后缀完全由用户控制
$target = "uploads/" . $_FILES['avatar']['name'];
move_uploaded_file($_FILES['avatar']['tmp_name'], $target);
echo '上传成功:' . $target;
?>

攻击者抓包把文件名改成 shell.php 直传即可。修复后的安全写法遵循"白名单 + 重命名 + 不可执行"三板斧:

php
<?php
// 安全版上传
$allowed = ['jpg', 'jpeg', 'png', 'gif'];                       // 1. 服务端白名单
$ext = strtolower(pathinfo($_FILES['avatar']['name'], PATHINFO_EXTENSION));

if (!in_array($ext, $allowed, true)) { exit('文件类型不允许'); } // 2. 拒绝非白名单

$newName = bin2hex(random_bytes(16)) . '.' . $ext;              // 3. 随机重命名,防覆盖/猜测
move_uploaded_file($_FILES['avatar']['tmp_name'], "/var/uploads/$newName");
echo '上传成功';                                                 // 4. 存储目录由 Nginx 配置为不可执行 PHP
?>

三、三种典型 Payload(教学演示)

1. 直接上传脚本(绕过前端校验)

前端 JS 校验可通过抓包工具或浏览器禁用 JS 直接绕过:把 shell.php(内容为教材常用的功能演示代码,如 <?php echo system($_GET["cmd"]); ?>)改名 shell.jpg 通过前端,再用抓包工具把文件名改回 .php 送出。上传成功后访问 https://example.com/uploads/shell.php?cmd=whoami 即可执行命令——这解释了为什么"前端校验只是体验优化"。

2. 后缀变形绕过黑名单

若后端用黑名单拦截 php,历史上有大量变形思路(以教学举例为目的):shell.php5shell.phtml(默认配置可被解析的后缀)、shell.php.(Windows 下结尾点号被去除)、%00 截断(旧版本 PHP 的经典问题)。这一类 Payload 说明了黑名单思路的天然缺陷:你永远列不全"危险后缀",而白名单只需要列全"合法后缀"

3. 图片马(伪装文件头)

若服务端校验文件头魔数(前几字节),可将 PHP 代码追加在真实图片末尾制作"图片马":

text
GIF89a
<?php echo system($_GET["cmd"]); ?>

文件头是合法图片、内容藏代码,再配合"文件包含"或服务器解析缺陷被激活。这条 Payload 的教学意义在于:魔数校验也非万无一失,必须配合"存储不执行"才完整

四、防御建议

  1. 白名单校验后缀:只允许业务必需的少数后缀,拒绝一切黑名单方案
  2. 上传目录禁止执行:用 Nginx/Apache 配置把 uploads/ 等目录的脚本解析彻底关掉——这是即使前几层全部失守也不至于沦陷的"保险栓",优先级最高
  3. 随机重命名文件:使用服务端生成的随机文件名(保留白名单后缀),杜绝路径穿越、覆盖与直接访问猜测
  4. 校验文件内容:用图片库真正解码一遍图片(getimagesize/重采样),比单纯比对魔数更可靠,顺便剥离多余数据
  5. 文件与站点分离存储:上传文件放独立存储(对象存储/OSS 或独立域名),即使被传恶意文件也无法触及主站解析环境
  6. 限制大小与做杀毒扫描:限制单文件体积、控制并发,对用户文件接入杀毒引擎扫描,并记录完整上传日志便于追溯

五、总结

文件上传是"功能"与"风险"距离最近的一类 Web 功能:做对了是头像与附件,做错了就是服务器后门。防御口诀可以浓缩成三句话——前端校验当不存在(必须服务端把关)、白名单重命名不可少(不信任何用户输入)、存储目录不执行(最后的安全边界)。层层设防、假设每一层都会失守,这正是纵深防御思想在文件上传场景的最佳实践。

仅用于学习交流的防守型安全知识库