* 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";
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END








暂无评论内容