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

PHP 檢測圖片木馬(WebShell)實戰:十六進制特徵掃描、GD 重繪與多層防禦體系

     編輯:PHP 綜合知識

威脅背景

圖片木馬(Polyglot / Stego WebShell)利用圖片格式容錯特性,將 PHP/ASP/JSP 腳本注入圖片文件(如 JPEG 的 APP 段、PNG 的 tEXt/iTXt 塊、GIF 的註釋塊)。上傳後,若伺服器配置不當(如 AddHandler php-script .jpg 或解析漏洞),可直接執行;或通過 include、require、imagecreatefromjpeg 等函數觸發二次渲染執行。

核心檢測策略:多層防禦

層級手段原理攔截率誤報率
1. 文件頭驗證Magic Bytes 檢查確認真實格式,防偽裝副檔名高低
2. 結構完整性GD/Imagick 讀取測試損壞/畸形圖片無法正常解碼中低
3. 特徵掃描十六進制模式匹配檢測 <?php、<%、<script 等腳本標籤高中
4. 重繪凈化GD 重新繪製保存剝離所有非圖像數據(元數據、隱寫、殘留代碼)極高極低
5. 存儲隔離非 Web 目錄、隨機文件名、禁用執行權限即使漏網,無法直接訪問執行根本無

完整上傳檢測類(生產就緒)

<?php
class SecureImageUpload {
    const MAX_SIZE = 10 * 1024 * 1024; // 10MB
    const ALLOWED_TYPES = [IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_PNG, IMAGETYPE_WEBP];
    const ALLOWED_EXT = ['gif', 'jpg', 'jpeg', 'png', 'webp'];
    const SCAN_CHUNK = 8192; // 掃描塊大小

    // 十六進制惡意模式: <% %>  (不區分大小寫)
    private static $hexPatterns = [
        //   3C 3F ... 3F 3E
        '/3c3f.{0,100}?3f3e/is',
        // <% ... %>  3C 25 ... 25 3E
        '/3c25.{0,100}?253e/is',
        //   3C 2F 53 43 52 49 50 54 3E
        '/3c2f5343524950543e/is',
        // eval(  65 76 61 6C 28
        '/6576616c28/is',
        // assert(  61 73 73 65 72 74 28
        '/61737365727428/is',
        // base64_decode(  62 61 73 65 36 34 5F 64 65 63 6F 64 65 28
        '/6261736536345f6465636f646528/is',
    ];

    public static function upload(array $file, string $saveDir): array {
        // 1. 基礎驗證
        $err = self::validateBase($file);
        if ($err) return ['success' => false, 'msg' => $err];

        // 2. 文件頭驗證
        $type = self::detectTrueType($file['tmp_name']);
        if ($type === false || !in_array($type, self::ALLOWED_TYPES)) {
            return ['success' => false, 'msg' => '文件類型不合法或偽裝'];
        }

        // 3. GD 讀取測試(結構完整性)
        $im = @self::createImageResource($file['tmp_name'], $type);
        if (!$im) {
            return ['success' => false, 'msg' => '圖片損壞或格式錯誤'];
        }
        imagedestroy($im);

        // 4. 十六進制特徵掃描
        if (self::scanHex($file['tmp_name'])) {
            return ['success' => false, 'msg' => '檢測到疑似腳本代碼特徵'];
        }

        // 5. GD 重繪凈化(核心防禦)
        $im = self::createImageResource($file['tmp_name'], $type);
        $w = imagesx($im);
        $h = imagesy($im);
        $cleanIm = imagecreatetruecolor($w, $h);
        // 保持透明度
        if ($type === IMAGETYPE_PNG || $type === IMAGETYPE_GIF || $type === IMAGETYPE_WEBP) {
            imagealphablending($cleanIm, false);
            imagesavealpha($cleanIm, true);
            $transparent = imagecolorallocatealpha($cleanIm, 255, 255, 255, 127);
            imagefill($cleanIm, 0, 0, $transparent);
        }
        imagecopyresampled($cleanIm, $im, 0, 0, 0, 0, $w, $h, $w, $h);
        imagedestroy($im);

        // 6. 安全保存
        $ext = self::ALLOWED_EXT[array_search($type, self::ALLOWED_TYPES)];
        $fileName = bin2hex(random_bytes(16)) . '.' . $ext;
        $savePath = rtrim($saveDir, '/') . '/' . $fileName;
        
        if (!is_dir($saveDir)) {
            mkdir($saveDir, 0755, true);
        }

        $saved = self::saveImage($cleanIm, $savePath, $type);
        imagedestroy($cleanIm);

        if (!$saved) {
            return ['success' => false, 'msg' => '保存失敗'];
        }

        // 7. 設置權限:僅讀,無執行
        @chmod($savePath, 0644);

        return ['success' => true, 'path' => $savePath, 'name' => $fileName];
    }

    private static function validateBase(array $file): ?string {
        if ($file['error'] !== UPLOAD_ERR_OK) {
            return '上傳錯誤代碼: ' . $file['error'];
        }
        if ($file['size'] > self::MAX_SIZE) {
            return '文件過大';
        }
        if (!is_uploaded_file($file['tmp_name'])) {
            return '非合法上傳文件';
        }
        return null;
    }

    private static function detectTrueType(string $path): ?int {
        $info = @getimagesize($path);
        return $info ? $info[2] : false;
    }

    private static function createImageResource(string $path, int $type) {
        switch ($type) {
            case IMAGETYPE_GIF: return @imagecreatefromgif($path);
            case IMAGETYPE_JPEG: return @imagecreatefromjpeg($path);
            case IMAGETYPE_PNG: return @imagecreatefrompng($path);
            case IMAGETYPE_WEBP: return @imagecreatefromwebp($path);
            default: return false;
        }
    }

    private static function saveImage($im, string $path, int $type): bool {
        switch ($type) {
            case IMAGETYPE_GIF: return imagegif($im, $path);
            case IMAGETYPE_JPEG: return imagejpeg($im, $path, 90);
            case IMAGETYPE_PNG: return imagepng($im, $path, 6);
            case IMAGETYPE_WEBP: return imagewebp($im, $path, 90);
            default: return false;
        }
    }

    /** 掃描文件頭尾各 8KB 十六進制特徵 */
    private static function scanHex(string $path): bool {
        $size = filesize($path);
        if ($size === 0) return true; // 空文件視為可疑

        $fp = fopen($path, 'rb');
        $hex = '';

        // 讀取頭部
        $headSize = min(self::SCAN_CHUNK, $size);
        $hex .= bin2hex(fread($fp, $headSize));

        // 讀取尾部(若文件較大)
        if ($size > self::SCAN_CHUNK * 2) {
            fseek($fp, -self::SCAN_CHUNK, SEEK_END);
            $hex .= bin2hex(fread($fp, self::SCAN_CHUNK));
        } elseif ($size > self::SCAN_CHUNK) {
            // 中等文件:讀取剩餘部分
            $hex .= bin2hex(fread($fp, $size - $headSize));
        }
        fclose($fp);

        // 多模式匹配
        foreach (self::$hexPatterns as $pattern) {
            if (preg_match($pattern, $hex)) {
                error_log("[SecureUpload] Hex pattern matched: $pattern in $path");
                return true; // 發現可疑
            }
        }
        return false;
    }
}

// 使用範例
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['image'])) {
    $result = SecureImageUpload::upload($_FILES['image'], __DIR__ . '/uploads/images');
    header('Content-Type: application/json');
    echo json_encode($result);
}
?>

關鍵技術點深度解析

1. 為什麼掃描頭尾各 8KB?

  • 圖片文件頭包含格式標識、元數據(EXIF、ICC Profile)、註釋塊——木馬常藏於此
  • 文件尾可能追加腳本代碼(Polyglot 技術)
  • 中間為圖像數據流,極難藏匿可執行代碼且不破壞渲染
  • 全文件掃描對大文件(如 50MB)效能影響大,頭尾掃描兼顧安全與性能

2. GD 重繪為何能徹底清除木馬?

imagecreatefrom* 只解析圖像數據(像素),忽略所有非圖像塊(註釋、EXIF、隱寫數據、追加代碼)。imagecopyresampled 將純像素繪製到新畫布,image* 保存時僅寫入標準圖像數據。任何非像素內容全部丟失。

3. 十六進制模式設計原則

  • 使用 bin2hex 將二進制轉為十六進制字串,避免多位元組編碼干擾
  • 模式採用「關鍵字 + 通配符 + 結束符」結構,如 3c3f.{0,100}?3f3e 匹配 <? ... ?>
  • 限制通配符長度({0,100})防止回溯災難
  • 覆蓋 PHP/ASP/JSP/JS/HTML 常見標籤及危險函數

4. 繞過檢測的高級攻擊與對策

攻擊手法原理對策
圖像數據域隱寫 (LSB)修改像素最低有效位藏匿代碼重繪時重新量化像素,LSB 信息丟失
PNG tEXt/iTXt/zTXt 塊注入標準元數據塊存放腳本GD 讀取時忽略文本塊,重繪不保留
JPEG APP 段注入APP0-APP15 區段藏代碼同理,GD 僅解碼掃描數據
多格式 Polyglot同時是合法圖片和合法腳本重繪破壞腳本結構;存儲隔離防執行
編碼混淆 (Base64, Rot13, XOR)腳本經編碼存放,運行時解碼掃描常見編碼函數特徵;重繪徹底清除

部署清單(必須配合)

  1. [ ] 上傳目錄 非 Web 可訪問路徑(如 /var/www/data/uploads 而非 /var/www/html/uploads)
  2. [ ] Nginx/Apache 禁用上傳目錄腳本執行:
    # Nginx
    location /uploads/ {
        location ~ \.(php|php5|phtml|asp|jsp)$ { deny all; return 403; }
    }
    # Apache .htaccess
    
        Require all denied
    
  3. [ ] 文件名 隨機重命名(bin2hex(random_bytes(16))),無副檔名執行風險
  4. [ ] 文件權限 0644,目錄 0755,無執行位
  5. [ ] 使用獨立域名/CDN 服務靜態資源(Cookie 隔離、CSP 策略)
  6. [ ] 定期掃描存量文件(ClamAV、自定義掃描腳本)

總結

單一檢測手段皆可被繞過。**GD 重繪凈化 + 十六進制特徵掃描 + 存儲隔離 + 執行權限禁用**構成纵深防禦體系。上述 SecureImageUpload 類整合了核心邏輯,可直接投入生產。記住:上傳安全的本質是「不信任任何客戶端數據」,在伺服端徹底中性化處理後再存儲。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved