* PbootCMS 木馬掃描與文件完整性檢查腳本
* 用途:對比當前網站 PHP 文件與官方版本,找出多餘/被篡改的 PHP 文件
* 使用方法:將本文件命名爲 check_malware.php 並放置在網站根目錄,通過瀏覽器訪問或命令行運行
* 安全建議:運行後請及時刪除本腳本,避免被惡意利用
<?php
/**
* PbootCMS 文件完整性檢查腳本(自動排除官方基準目錄)
* 功能:對比當前網站 PHP 文件與官方版本,排除官方目錄本身,生成 HTML 報告
* 使用方法:php check_malware.php
* 安全建議:運行後立即刪除本腳本
*/
// ========== 配置區域 ==========
$CONFIG = [
'official_dir' => __DIR__ . '/doc/PbootCMS-3.2.13', // 官方版本解壓路徑
'current_dir' => __DIR__, // 當前網站根目錄
'exclude_dirs' => ['runtime', 'cache', 'temp'], // 額外跳過的一級目錄(相對路徑首段)
'html_output_file' => __DIR__ . '/security_report.html', // HTML 報告保存路徑
'show_diff' => true, // 顯示文件差異
];
// ========== 安全:禁止外網訪問 ==========
if (php_sapi_name() !== 'cli') {
$allowed = ['127.0.0.1', '::1'];
if (!in_array($_SERVER['REMOTE_ADDR'], $allowed)) {
die('Access denied. Please run this script from command line or localhost.');
}
}
// ========== 核心函數 ==========
/**
* 遞歸獲取目錄下所有 PHP 文件的相對路徑及元信息
* @param string $baseDir 掃描的基礎目錄
* @param array $excludeDirs 排除的一級目錄名(僅匹配相對路徑首段)
* @param array $excludePrefixes 排除的路徑前綴(完整相對路徑前綴,如 'doc/PbootCMS-3.2.13')
*/
function getPhpFilesMap($baseDir, $excludeDirs = [], $excludePrefixes = []) {
$map = [];
$baseDir = rtrim($baseDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($baseDir, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::SELF_FIRST
);
foreach ($iterator as $file) {
if (!$file->isFile() || $file->getExtension() !== 'php') {
continue;
}
$realPath = $file->getRealPath();
$relativePath = str_replace($baseDir, '', $realPath);
// 檢查一級目錄排除
$firstDir = strtok($relativePath, DIRECTORY_SEPARATOR);
if (in_array($firstDir, $excludeDirs)) {
continue;
}
// 檢查前綴排除(如官方目錄)
$skip = false;
foreach ($excludePrefixes as $prefix) {
if (strpos($relativePath, $prefix . DIRECTORY_SEPARATOR) === 0 || $relativePath === $prefix) {
$skip = true;
break;
}
}
if ($skip) {
continue;
}
$map[$relativePath] = [
'md5' => md5_file($realPath),
'size' => $file->getSize(),
'mtime' => date('Y-m-d H:i:s', $file->getMTime()),
];
}
return $map;
}
/**
* 對比兩個文件映射,返回差異分類
*/
function compareFiles($official, $current) {
$extra = $modified = $missing = [];
foreach ($current as $path => $info) {
if (isset($official[$path])) {
if ($info['md5'] !== $official[$path]['md5']) {
$modified[] = $path;
}
} else {
$extra[] = $path;
}
}
foreach ($official as $path => $info) {
if (!isset($current[$path])) {
$missing[] = $path;
}
}
return [$extra, $modified, $missing];
}
/**
* 純 PHP 逐行差異對比(基於 LCS)
*/
function computeDiff($oldLines, $newLines) {
if (!is_array($oldLines)) $oldLines = explode("\n", $oldLines);
if (!is_array($newLines)) $newLines = explode("\n", $newLines);
$matrix = [];
$maxlen = 0;
$omax = $nmax = 0;
foreach ($oldLines as $oindex => $ovalue) {
$nkeys = array_keys($newLines, $ovalue);
foreach ($nkeys as $nindex) {
$matrix[$oindex][$nindex] = isset($matrix[$oindex-1][$nindex-1]) ? $matrix[$oindex-1][$nindex-1] + 1 : 1;
if ($matrix[$oindex][$nindex] > $maxlen) {
$maxlen = $matrix[$oindex][$nindex];
$omax = $oindex + 1 - $maxlen;
$nmax = $nindex + 1 - $maxlen;
}
}
}
if ($maxlen == 0) {
return [
['op' => 'del', 'lines' => $oldLines, 'old_ln' => 1, 'new_ln' => 0],
['op' => 'add', 'lines' => $newLines, 'old_ln' => 0, 'new_ln' => 1]
];
}
$oldPrefix = array_slice($oldLines, 0, $omax);
$newPrefix = array_slice($newLines, 0, $nmax);
$oldSuffix = array_slice($oldLines, $omax + $maxlen);
$newSuffix = array_slice($newLines, $nmax + $maxlen);
$common = array_slice($newLines, $nmax, $maxlen);
$result = [];
if (!empty($oldPrefix) || !empty($newPrefix)) {
$result = array_merge($result, computeDiff($oldPrefix, $newPrefix));
}
if (!empty($common)) {
$result[] = ['op' => 'same', 'lines' => $common, 'old_ln' => $omax+1, 'new_ln' => $nmax+1];
}
if (!empty($oldSuffix) || !empty($newSuffix)) {
$result = array_merge($result, computeDiff($oldSuffix, $newSuffix));
}
return $result;
}
/**
* 獲取兩個文件的差異(行數組)
*/
function getFileDiff($officialFile, $currentFile) {
if (!is_readable($officialFile) || !is_readable($currentFile)) {
return false;
}
$oldContent = file_get_contents($officialFile);
$newContent = file_get_contents($currentFile);
$oldLines = explode("\n", $oldContent);
$newLines = explode("\n", $newContent);
return computeDiff($oldLines, $newLines);
}
/**
* 生成完整的 HTML 報告字符串
*/
function generateHtmlReport($extra, $modified, $missing, $officialDir, $currentDir, $showDiff, $excludedPrefixes) {
$html = '<!DOCTYPE html><html><head><meta charset="utf-8"><title>PbootCMS 文件完整性檢查報告</title>';
$html .= '<style>
body{font-family:system-ui,sans-serif;margin:20px;line-height:1.6;background:#fafafa;}
.container{max-width:1400px;margin:0 auto;background:#fff;padding:20px;border-radius:8px;box-shadow:0 2px 8px rgba(0,0,0,0.1);}
h1{font-size:28px;border-bottom:2px solid #eee;padding-bottom:10px;}
.bad{color:#d32f2f;} .good{color:#2e7d32;} .warn{color:#ed6c02;}
table{border-collapse:collapse;width:100%;margin:15px 0;}
td,th{border:1px solid #ddd;padding:8px 12px;text-align:left;vertical-align:top;}
th{background:#f5f5f5;}
.file-path{font-family:monospace;word-break:break-all;}
details{margin:8px 0;}
summary{cursor:pointer;color:#0066cc;font-weight:bold;}
.diff-box{background:#f8f8f8;border:1px solid #ddd;padding:10px;overflow:auto;max-height:400px;font-size:13px;font-family:monospace;}
.diff-box .same{color:#333;}
.diff-box .del{color:#d32f2f;background:#ffebee;}
.diff-box .add{color:#2e7d32;background:#e8f5e9;}
.count-badge{font-size:14px;font-weight:normal;margin-left:10px;background:#eee;padding:2px 10px;border-radius:12px;}
.footer{margin-top:30px;border-top:1px solid #eee;padding-top:15px;font-size:14px;color:#666;}
.exclude-info{background:#f0f0f0;padding:8px 15px;border-radius:4px;margin:10px 0;}
</style></head><body>
<div class="container">
<h1>🔍 PbootCMS 文件完整性檢查報告</h1>
<p><strong>生成時間:</strong>' . date('Y-m-d H:i:s') . '</p>
<p><strong>官方版本路徑:</strong>' . htmlspecialchars($officialDir) . '<br>
<strong>當前網站路徑:</strong>' . htmlspecialchars($currentDir) . '</p>
<div class="exclude-info"><strong>已排除的路徑前綴:</strong>' . htmlspecialchars(implode(', ', $excludedPrefixes)) . '</div>';
// 額外文件
$html .= '<h2 class="warn">⚠️ 額外文件(官方版本中不存在) <span class="count-badge">共 ' . count($extra) . ' 個</span></h2>';
if ($extra) {
$html .= '<table><tr><th>文件路徑</th><th>文件大小</th><th>修改時間</th><th>建議操作</th></tr>';
foreach ($extra as $file) {
$filePath = $currentDir . DIRECTORY_SEPARATOR . $file;
$size = is_file($filePath) ? number_format(filesize($filePath)) . ' B' : 'N/A';
$mtime = is_file($filePath) ? date('Y-m-d H:i:s', filemtime($filePath)) : 'N/A';
$html .= "<tr><td class='file-path'>$file</td><td>$size</td><td>$mtime</td><td>🔍 人工覈查,極可能是木馬或非法文件</td></tr>";
}
$html .= '</table>';
} else {
$html .= '<p class="good">✅ 未發現多餘 PHP 文件</p>';
}
// 被篡改的文件
$html .= '<h2 class="bad">🔧 被篡改的文件(與官方版本內容不同) <span class="count-badge">共 ' . count($modified) . ' 個</span></h2>';
if ($modified) {
$html .= '<table><tr><th>文件路徑</th><th>文件大小</th><th>修改時間</th><th>差異詳情</th></tr>';
foreach ($modified as $file) {
$officialFile = $officialDir . DIRECTORY_SEPARATOR . $file;
$currentFile = $currentDir . DIRECTORY_SEPARATOR . $file;
$size = is_file($currentFile) ? number_format(filesize($currentFile)) . ' B' : 'N/A';
$mtime = is_file($currentFile) ? date('Y-m-d H:i:s', filemtime($currentFile)) : 'N/A';
$html .= "<tr><td class='file-path'>$file</td><td>$size</td><td>$mtime</td><td>";
if ($showDiff) {
$diff = getFileDiff($officialFile, $currentFile);
if ($diff) {
$html .= "<details><summary>📄 顯示差異(行號標記)</summary>";
$html .= "<div class='diff-box'>";
$oldLine = 1; $newLine = 1;
foreach ($diff as $chunk) {
switch ($chunk['op']) {
case 'same':
foreach ($chunk['lines'] as $line) {
$html .= "<span class='same'> " . str_pad($oldLine, 4, ' ', STR_PAD_LEFT) . " " . htmlspecialchars($line) . "</span><br>";
$oldLine++; $newLine++;
}
break;
case 'del':
foreach ($chunk['lines'] as $line) {
$html .= "<span class='del'>- " . str_pad($oldLine, 4, ' ', STR_PAD_LEFT) . " " . htmlspecialchars($line) . "</span><br>";
$oldLine++;
}
break;
case 'add':
foreach ($chunk['lines'] as $line) {
$html .= "<span class='add'>+ " . str_pad($newLine, 4, ' ', STR_PAD_LEFT) . " " . htmlspecialchars($line) . "</span><br>";
$newLine++;
}
break;
}
}
$html .= "</div></details>";
} else {
$html .= "⚠️ 無法讀取文件內容";
}
} else {
$html .= "差異對比已禁用";
}
$html .= "</td></tr>";
}
$html .= '</table>';
} else {
$html .= '<p class="good">✅ 無核心文件被篡改</p>';
}
// 缺失文件
$html .= '<h2 class="warn">📁 缺失的文件(官方存在但當前網站丟失) <span class="count-badge">共 ' . count($missing) . ' 個</span></h2>';
if ($missing) {
$html .= '<table><tr><th>文件路徑</th><th>官方大小</th><th>建議操作</th></tr>';
foreach ($missing as $file) {
$officialFile = $officialDir . DIRECTORY_SEPARATOR . $file;
$size = is_file($officialFile) ? number_format(filesize($officialFile)) . ' B' : 'N/A';
$html .= "<tr><td class='file-path'>$file</td><td>$size</td><td>📥 從官方版本補回</td></tr>";
}
$html .= '</table>';
} else {
$html .= '<p class="good">✅ 無核心文件缺失</p>';
}
$html .= '<div class="footer">';
$html .= '<p>📌 <strong>安全建議</strong></p><ul>';
$html .= '<li>「額外文件」中的 <code>template/</code> 或 <code>static/uploads/</code> 下如有 PHP 文件,<strong>極可能是木馬後門</strong>,應立即刪除。</li>';
$html .= '<li>「被篡改的文件」若確認非自己修改,請務必用官方版本覆蓋,並檢查差異內容是否包含惡意代碼。</li>';
$html .= '<li>修復後請立即更改服務器密碼、檢查系統日誌、更新到最新版 PbootCMS。</li>';
$html .= '</ul>';
$html .= '<p>報告生成時間:' . date('Y-m-d H:i:s') . ' | 腳本僅作參考,請結合實際情況處理。</p>';
$html .= '</div></div></body></html>';
return $html;
}
/**
* 輸出命令行文本報告
*/
function renderCliReport($extra, $modified, $missing) {
echo "\n========== PbootCMS 文件完整性檢查報告 ==========\n";
echo "\n🔴 額外文件(官方版本中不存在): " . count($extra) . " 個\n";
if ($extra) { foreach ($extra as $file) echo " + $file\n"; } else echo " ✅ 無\n";
echo "\n🟠 被篡改的文件(內容不同): " . count($modified) . " 個\n";
if ($modified) { foreach ($modified as $file) echo " * $file\n"; } else echo " ✅ 無\n";
echo "\n🔵 缺失的文件(官方存在但當前丟失): " . count($missing) . " 個\n";
if ($missing) { foreach ($missing as $file) echo " - $file\n"; } else echo " ✅ 無\n";
echo "\n==================================================\n";
}
// ========== 主流程 ==========
if (!is_dir($CONFIG['official_dir'])) {
die("錯誤:官方版本目錄不存在,請將 PbootCMS 官方完整包解壓到 " . $CONFIG['official_dir'] . "\n");
}
// 計算官方目錄相對於當前目錄的相對路徑(用於排除)
$officialRelPath = str_replace($CONFIG['current_dir'] . DIRECTORY_SEPARATOR, '', $CONFIG['official_dir']);
if ($officialRelPath == $CONFIG['official_dir']) {
// 如果官方目錄不在當前目錄下(絕對路徑不同),則不自動排除
$officialRelPath = null;
}
$excludePrefixes = [];
if ($officialRelPath) {
$excludePrefixes[] = $officialRelPath;
}
// 可額外手動添加排除前綴(例如用戶自定義)
// $excludePrefixes[] = 'some/other/dir';
echo "正在掃描 PHP 文件,請稍候...\n";
echo "排除前綴:" . implode(', ', $excludePrefixes) . "\n";
// 掃描官方目錄(不排除自己,因爲它是基準)
$officialFiles = getPhpFilesMap($CONFIG['official_dir'], $CONFIG['exclude_dirs'], []);
// 掃描當前目錄,排除官方目錄及其他指定前綴
$currentFiles = getPhpFilesMap($CONFIG['current_dir'], $CONFIG['exclude_dirs'], $excludePrefixes);
list($extra, $modified, $missing) = compareFiles($officialFiles, $currentFiles);
// 生成 HTML 報告並保存爲文件
$htmlContent = generateHtmlReport(
$extra, $modified, $missing,
$CONFIG['official_dir'],
$CONFIG['current_dir'],
$CONFIG['show_diff'],
$excludePrefixes
);
$outputFile = $CONFIG['html_output_file'];
if (file_put_contents($outputFile, $htmlContent) !== false) {
echo "✅ HTML 報告已生成:{$outputFile}\n";
} else {
echo "❌ 寫入報告文件失敗,請檢查目錄權限。\n";
}
// 命令行下輸出簡潔文本報告
renderCliReport($extra, $modified, $missing);
echo "\n掃描完成。請打開 {$outputFile} 查看詳細差異。\n";
echo "建議及時刪除本腳本以免被濫用。\n";