文件上传的基本流程相同,客户端使用 JavaScript 验证,服务器端采用随机数来重命名文件,以防止文件重复。
防止上传漏洞:
- 客户端检测 -- JS;
- 服务器端检测 -- 检测文件的 MIME 类型,检测文件扩展名是否合法,甚至有些检测文件中是否嵌入恶意代码。
中国菜刀 和 一句话图片木马
-
软件“中国菜刀”用来管理网站文件,小巧灵活,可以用一段简短的代码方便地管理网站。
-
软件提供的服务器端文件仅有一行代码,目前支持的服务器端脚本包括:PHP、ASP、ASP.NET、JSP 等,并且支持 HTTPS 安全连接的网站。
-
常见代码:
因为代码短小精悍,也被称为一句话木马。 -
用中国菜刀连接网站后返回主界面,选中目标 URL 并单击鼠标右键,可以看到虚拟终端(执行命令操作)、数据库连接、执行自写脚本等选项,可以根据相应的选项使用某些操作。
-
“图片一句话”就是把一句话木马插入在图片文件中,且不损坏图片文件,可以躲过一些防火墙检测。
-
制作图片一句话木马可以使用 Edjpgcom。
1. 客户端检测
客户端验证,作用是在文件发出请求之前,先在用户的浏览器里进行一次简单的检查。
代码示例:
function checkFile() {
var fileInput = document.getElementById('uploadFile');
var filePath = fileInput.value;
// 定义允许的后缀名(白名单)
var allowedExtensions = /(\.jpg|\.jpeg|\.png|\.gif)$/i;
if (!allowedExtensions.exec(filePath)) {
alert('只允许上传图片格式 (jpg/jpeg/png/gif)!');
fileInput.value = ''; // 清空选择
return false;
}
return true; // 允许提交
}
由于这段代码运行在浏览器,攻击者可以通过禁用 JS 或使用 Burp Suite 拦截抓包,轻松绕过这个验证。
最基础的后端处理代码,完全没有安全过滤。会直接接收文件,并以“时间戳+原后缀”重命名,防止文件名冲突。
<?php
// 设定上传目录为当前目录
$uploadPath = "./";
// 获取上传文件的基本信息
$tempFile = $_FILES['file']['tmp_name']; // 临时文件路径
$originName = $_FILES['file']['name']; // 原始文件名
// 提取后缀名(例如:.php)
$fileExt = strrchr($originName, '.');
// 使用时间戳生成新文件名,防止重名
$newName = time() . $fileExt;
$finalPath = $uploadPath . $newName;
// 将临时文件移动到目标路径
if (move_uploaded_file($tempFile, $finalPath)) {
echo "文件上传成功!存储位置: " . $finalPath;
} else {
echo "文件上传失败。";
}
?>
针对客户端验证有很多绕过方法。
1.1 抓包修改
-
将恶意脚本(如
shell.php)暂时改名为合法的图片格式(如shell.jpg)。 -
在浏览器选择该文件,此时 JavaScript 检查后缀名合法,允许通过。
-
点击“上传”按钮,同时使用 Burp Suite 等工具拦截该请求包。
-
在 Burp Suite 中,将文件名手动改回
shell.php,然后放行(Forward)。
1.2 禁用浏览器 JS
如果前端验证完全依赖 JavaScript 脚本且没有其他逻辑,可以直接禁用 JS。
-
打开浏览器开发者工具(F12)。
-
在设置中找到 "Disable JavaScript" 选项并勾选。
-
直接选择
shell.php上传。 -
局限性:如果页面渲染或上传按钮的点击逻辑也依赖 JS,此方法可能导致页面无法正常工作。
1.3 控制台修改逻辑
-
在上传页面打开控制台(Console)。
-
重写函数:如果验证函数叫
checkFile(),直接在控制台输入checkFile = function(){return true;}并回车。 -
删除属性:或者找到 HTML 标签中的
onsubmit="return checkFile()",直接在 DOM 树中删掉这段代码。 -
原理:通过内存修改,让原本的验证逻辑失效或永远返回“真”。
2. 服务器端检测
主要包含:白名单与黑名单扩展名过滤、文件类型检测、文件重命名等。
2.1 白名单与黑名单验证
2.1.1 黑名单过滤
不安全。黑名单定义一系列不安全的扩展名,服务器端在接收文件后,与黑名单扩展名对比。如果匹配,就认为文件不合法,PHP 代码如下:
<?php
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
// 定义黑名单:不允许上传的文件后缀
$deny_ext = array(".php", ".php5", ".php4", ".php3", ".php2", ".html", ".htm", ".phtml", ".pht", ".jsp", ".jspa", ".jspx", ".jsw", ".jsv", ".jspf", ".jtml", ".asp", ".aspx", ".asa", ".asax", ".ascx", ".ashx", ".asmx", ".cer", ".swf", ".htaccess");
// 获取上传文件的名称
$file_name = trim($_FILES['upload_file']['name']);
// 简单的防御措施:去掉文件名末尾的点(防止 Windows 特性绕过)
$file_name = deldot($file_name);
// 提取文件后缀名
$file_ext = strrchr($file_name, '.');
$file_ext = strtolower($file_ext); // 转换为小写,防止大写绕过(如 .PHP)
// 检查后缀是否在黑名单中
if (!in_array($file_ext, $deny_ext)) {
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = "./uploads/". $file_name;
if (move_uploaded_file($temp_file, $img_path)) {
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else {
$msg = '此文件类型不允许上传!';
}
}
?>
绕过思路:
-
名单不全:如果环境支持,可以尝试名单之外的后缀。例如,黑名单漏掉了
.php7、.php8或.phtml。 -
特殊后缀:在某些配置下,
.php. .(点空格点) 或者.php::$DATA(Windows 流文件) 可能会被操作系统处理后变回.php。如果没有大小写转化操作,也可以尝试切换后缀的大小写绕过(此类扩展名在 Windows 平台依然会被 Web 容器解析)。 -
配合解析漏洞:上传一个
.htaccess文件(如果名单没禁),修改服务器的配置,让它把.jpg当作 PHP 执行。
2.1.2 白名单过滤
白名单定义允许上传的扩展名,拥有比黑名单更好的防御机制。
<?php
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
// 1. 定义白名单:只允许这三种后缀
$allow_ext = array('jpg', 'png', 'gif');
// 2. 获取文件名并提取后缀
$file_name = $_FILES['upload_file']['name'];
$file_info = pathinfo($file_name);
$file_ext = $file_info['extension']; // 获取扩展名
// 3. 检查后缀是否在白名单内
if (in_array($file_ext, $allow_ext)) {
// 4. 重命名文件(关键安全步骤)
// 使用随机数或时间戳重命名,彻底抹除原始文件名可能带来的截断攻击风险
$new_file_name = date("YmdHis") . rand(1000, 9999) . "." . $file_ext;
$temp_file = $_FILES['upload_file']['tmp_name'];
$save_path = "./uploads/" . $new_file_name;
if (move_uploaded_file($temp_file, $save_path)) {
$is_upload = true;
$msg = "上传成功!文件名已重命名为: " . $new_file_name;
} else {
$msg = "文件夹移动失败。";
}
} else {
$msg = "只允许上传 .jpg, .png, .gif 格式的文件!";
}
}
?>
绕过思路:
-
配合解析漏洞:虽然文件存成了
.jpg,但如果 Nginx 存在我们之前提到的CGI.fix_pathinfo漏洞,攻击者依然可以通过访问xxx.jpg/.php来强制执行它。或使用pentest.asp;1.jpg等方式。 -
文件头校验绕过:如果后端还检查了文件内容(如
getimagesize()),攻击者会将木马代码藏在正常的图片二进制数据中(制作图片马)。
2.2 MIME 验证
MIME 类型用来设定某种扩展名文件的打开方式,当具有该扩展名的文件被访问时,浏览器会自动使用指定的应用程序打开。如 GIF 图片 MIME 为 image/gif,CSS 文件 MIME 类型为 text/css。
<?php
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
// 1. 定义允许的 MIME 类型(白名单)
$allow_mime = array("image/jpeg", "image/png", "image/gif");
// 2. 获取浏览器发送的 MIME 类型
// 注意:这个值是从 HTTP 请求头的 Content-Type 中获取的
$file_mime = $_FILES['upload_file']['type'];
// 3. 验证 MIME 类型是否合法
if (in_array($file_mime, $allow_mime)) {
$temp_file = $_FILES['upload_file']['tmp_name'];
$save_path = "./uploads/" . $_FILES['upload_file']['name'];
if (move_uploaded_file($temp_file, $save_path)) {
$is_upload = true;
$msg = "MIME 验证通过,上传成功!";
} else {
$msg = "上传失败。";
}
} else {
$msg = "文件类型非法,只允许图片格式 (image/jpeg, image/png, image/gif)!";
}
}
?>
绕过思路:用 Burp Suite 等工具拦截,修改请求头中的 Content-Type。
- 本质上也是一种“客户端检测”的变体。虽然代码写在后端,但它检测的对象(
$_FILES['upload_file']['type'])完全由客户端发出的 HTTP 请求包控制。
更安全的做法:
// 这是一个更安全的做法,服务器会读取文件二进制头来判断类型
$finfo = new finfo(FILEINFO_MIME_TYPE);
$real_mime = $finfo->file($_FILES['upload_file']['tmp_name']);
if (in_array($real_mime, $allow_mime)) {
// 只有真正的图片文件头才能通过
}
2.3 目录验证
<?php
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
// 1. 正常的白名单后缀过滤
$allow_ext = array('jpg', 'png', 'gif');
$file_name = $_FILES['upload_file']['name'];
$file_ext = pathinfo($file_name, PATHINFO_EXTENSION);
if (in_array($file_ext, $allow_ext)) {
// 2. 目录验证:从 POST 请求中获取上传路径
// 攻击点就在这里:$_POST['save_path'] 是用户可控的
$save_path = $_POST['save_path'];
$temp_file = $_FILES['upload_file']['tmp_name'];
// 最终拼接路径:例如 "./uploads/" + "test.jpg"
$img_path = $save_path . "/" . rand(10, 99) . date("His") . "." . $file_ext;
if (move_uploaded_file($temp_file, $img_path)) {
$is_upload = true;
$msg = "文件已上传至: " . $img_path;
} else {
$msg = "上传失败,请检查目录权限或路径!";
}
} else {
$msg = "只允许上传图片格式!";
}
}
?>
假如用户通过 Burp Suite 抓包等方式,把路径规定为 pentest.asp 目录,如果服务器不存在此目录,将会建立此目录,然后把图片木马文件写入该目录。如果 Web 容器为 IIS 6.0,网页木马就会被解析。
2.4 截断上传攻击
2.4.1 核心原理
在 C 语言中,字符串是以 \0(即 0x00)作为结束标志的。由于许多后端语言(如早期版本的 PHP、ASP、Java)的底层是 C 实现的,当它们处理文件名字符串时:
-
逻辑层(代码):可能只看到了文件末尾的
.jpg,认为合法并放行。 -
系统层(保存文件):读取到
0x00时认为字符串结束了,直接丢弃后面的内容。
示例:文件名
webshell.asp%00.jpg
后端验证:后缀是
.jpg,合法。保存文件:系统处理到
%00处停止,最终存为webshell.asp。
2.4.2 典型的存在截断漏洞的 ASP 代码
<%
' 模拟从前端获取上传路径和文件名
Dim uploadDir, fileName, finalPath
uploadDir = Request.Form("save_path") ' 攻击点:路径由用户控制
fileName = "test.jpg" ' 假设后缀已被白名单验证
' 逻辑拼接:uploads/shell.asp[0x00]/test.jpg
finalPath = Server.MapPath(uploadDir & "/" & fileName)
' 使用 FSO 对象保存文件
Set fso = Server.CreateObject("Scripting.FileSystemObject")
' 由于底层的截断,文件会被保存在 shell.asp
' ... 保存逻辑 ...
%>
2.4.3 利用 Burp Suite 进行截断攻击
截断攻击不能直接在文件名里输入 %00(因为那只是三个字符),必须在 Hex(十六进制) 层面修改。
场景: 后端验证文件必须以 .jpg 结尾,但存储路径由 save_path 参数决定。
操作步骤:
-
拦截请求:上传
shell.php,用 Burp Suite 拦截。 -
修改路径:在参数中找到
save_path=./uploads/,修改为save_path=./uploads/shell.asp .(在asp后面加了一个空格和一个点,为了方便定位)。 -
切换到 Hex 视图:
-
在 Hex 编辑器中找到刚才输入的
asp后面那个空格对应的十六进制数值(通常是20)。 -
将
20手动修改为00。
-
-
发送请求:此时请求包中的路径在二进制层面已经变成了
.../shell.asp\0.jpg。 -
结果:服务器绕过了
.jpg的检查,但在写入硬盘时,受0x00影响,文件被命名为shell.asp。 -
PHP 版本限制:PHP 截断漏洞仅存在于 5.3.4 以下版本,且
magic_quotes_gpc需为 Off 状态。 -
现代环境:现在的 Web 容器和编程语言大多已在底层修复了此问题。如果题目环境非常老旧(如 IIS 6.0、PHP 5.2),那么截断攻击是一个思路。