pbootcms官方文件与网站文件对比,检测异常文件

* 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
喜欢就支持一下吧
点赞14 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容