跳至正文
  • 8 views
  • 9 min read

全行为审计与防御插件开发实战 (SBA v1.0)

新浪微博 豆瓣 QQ 百度贴吧 QQ空间

在经营 WordPress 站点的过程中,最头疼的不是没流量,而是那些没完没了的自动化扫描器、CC 攻击和寻找 .env 泄露的机器人。

为了实现毫秒级响应深度行为追踪,我开发了这款名为 Site Behavior Auditor (SBA) 的轻量级插件。它不依赖第三方 API,完全运行在本地,支持 IPv6 归属地识别,并具备三层递进防御体系。


一、 核心防御原理

SBA 的设计哲学是:在 WordPress 加载逻辑的最前端拦截非法请求。

  1. 特征识别 (Fingerprinting):利用 HTTP_USER_AGENT 识别已知的扫描工具(如 sqlmap, nmap)。
  2. 路径诱捕 (Path Trapping):监控敏感路径(如 .git, .env),一旦触碰,立即判定为恶意攻击。
  3. 流量整形 (Rate Limiting):基于本地数据库记录,计算单 IP 每分钟的请求频率,自动阻断 CC 行为。
  4. 身份闭环 (The Gate):重命名登录入口并配合用户名白名单,让站长在防御开启时依然能“横着走”。

二、 核心功能特性

  • 实时访客轨迹:异步加载,精确记录每一个 IP 的访问路径、时间及 PV。
  • IPv4/v6 双栈归属地:支持本地导入 7 列格式 IP 库,毫秒级解析地理位置,不泄露访客隐私。
  • 30天趋势图表:双曲线展示 UV 和 PV 的变化趋势。
  • 拦截日志系统:详细记录每一次拦截的时间、原因以及对方试图访问的目标。
  • 50天审计详表:深度分析历史访问数据与平均访问深度。

三、 插件全量源代码

使用说明:在wp-content/plugins目录中新建一个文件夹 site-behavior-auditor,在其中创建 site-behavior-auditor.php,复制以下全部代码。

<?php
/*
Plugin Name: Site Behavior Auditor (SBA)
Description: v1.0 正式版:全行为审计、路径拦截、CC防御、IPv6归属地解析、30天趋势图。
Version: 1.0
Author: Stone
*/

if (!defined('ABSPATH')) exit;

/* ================= 1. 数据库持久化 ================= */
register_activation_hook(__FILE__, function() {
    global $wpdb; $col = $wpdb->get_charset_collate();
    $sql = [
        "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}dis_stats (id BIGINT AUTO_INCREMENT PRIMARY KEY, ip VARCHAR(45), url TEXT, visit_date DATE, visit_hour TINYINT, pv INT DEFAULT 1, last_visit TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY ip_date_url (ip, visit_date, url(191)), INDEX idx_lookup (visit_date, last_visit DESC)) $col;",
        "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}sba_ip_data (id int(11) NOT NULL AUTO_INCREMENT, ip_type tinyint(1) DEFAULT 4, start_bin varbinary(16) NOT NULL, end_bin varbinary(16) NOT NULL, addr varchar(255) NOT NULL, PRIMARY KEY (id), KEY range_idx (ip_type, start_bin, end_bin)) $col;",
        "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}sba_blocked_log (id BIGINT AUTO_INCREMENT PRIMARY KEY, ip VARCHAR(45), reason VARCHAR(100), target_url TEXT, block_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP) $col;"
    ];
    require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
    foreach($sql as $s) dbDelta($s);
});

/* ================= 2. 核心拦截引擎 ================= */
function sba_get_ip() {
    $ip = $_SERVER['HTTP_CF_CONNECTING_IP'] ?? $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'];
    return filter_var(trim(explode(',', $ip)[0]), FILTER_VALIDATE_IP) ?: '0.0.0.0';
}

function sba_get_opt($k, $d='') {
    $o = get_option('sba_settings');
    return (isset($o[$k]) && $o[$k] !== '') ? $o[$k] : $d;
}

function sba_execute_block($reason) {
    // 权限与白名单校验:确保管理员及白名单用户不被封锁
    $u_white = array_filter(array_map('trim', explode(',', sba_get_opt('user_whitelist', ''))));
    $user = wp_get_current_user();
    if ($user->exists() && in_array($user->user_login, $u_white)) return;
    if (current_user_can('manage_options')) return;

    $ip = sba_get_ip();
    $ip_white = array_filter(array_map('trim', explode(',', sba_get_opt('ip_whitelist', ''))));
    if (in_array($ip, $ip_white)) return;

    global $wpdb;
    $wpdb->insert($wpdb->prefix . "sba_blocked_log", ['ip' => $ip, 'reason' => $reason, 'target_url' => $_SERVER['REQUEST_URI']]);
    
    $target = sba_get_opt('block_target_url', '');
    if (!empty($target) && filter_var($target, FILTER_VALIDATE_URL)) { wp_redirect($target); exit; }
    wp_die("🛡️ SBA 安全拦截:$reason", "Security Block", 403);
}

add_action('init', function() {
    if (is_admin()) return;
    $ip = sba_get_ip(); $uri = strtolower($_SERVER['REQUEST_URI']); $ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
    
    // 1. 指纹拦截
    $scan_tools = ['sqlmap','nmap','dirbuster','nikto','zgrab','python-requests','go-http-client','java/','curl/','wget','masscan'];
    foreach($scan_tools as $tool) { if (stripos($ua, $tool) !== false) sba_execute_block("自动化扫描器: $tool"); }

    // 2. 路径拦截
    $fixed_evil = ['/.env', '/.git', '/.sql', '/.ssh', '/wp-config.php.bak', '/phpinfo.php', '/config.php.swp', '/.vscode', '/xmlrpc.php'];
    $custom_evil = array_filter(array_map('trim', explode(',', sba_get_opt('evil_paths', ''))));
    $all_evil = array_unique(array_merge($fixed_evil, $custom_evil));
    foreach ($all_evil as $path) { if (!empty($path) && strpos($uri, $path) !== false) sba_execute_block("非法路径探测: $path"); }

    // 3. 频率拦截 (CC防御)
    $limit = (int)sba_get_opt('auto_block_limit', 0);
    if ($limit > 0) {
        global $wpdb;
        $count = $wpdb->get_var($wpdb->prepare("SELECT SUM(pv) FROM {$wpdb->prefix}dis_stats WHERE ip = %s AND last_visit > DATE_SUB(NOW(), INTERVAL 1 MINUTE)", $ip));
        if ($count > $limit) sba_execute_block("频率超限 (CC风险)");
    }

    // 4. 后台 Gate 锁
    $slug = sba_get_opt('login_slug', '');
    if (!empty($slug) && strpos($uri, 'wp-login.php') !== false && (!isset($_GET['gate']) || $_GET['gate'] !== $slug)) sba_execute_block("Gate 钥匙错误");

    global $wpdb;
    $wpdb->query($wpdb->prepare("INSERT INTO {$wpdb->prefix}dis_stats (ip, url, visit_date, visit_hour, pv) VALUES (%s, %s, CURDATE(), %d, 1) ON DUPLICATE KEY UPDATE pv = pv + 1, last_visit = NOW()", $ip, $_SERVER['REQUEST_URI'], date('G')));
});

/* ================= 3. AJAX 异步处理 ================= */
add_action('wp_ajax_sba_get_geo', function() {
    global $wpdb; $ips = (array)$_POST['ips']; $cache = get_option('sba_geo_cache', []); $results = [];
    foreach($ips as $ip) {
        if (isset($cache[$ip])) { $results[$ip] = $cache[$ip]; continue; }
        $loc = ""; $ip_bin = @inet_pton($ip);
        if ($ip_bin) {
            $type = (strpos($ip, ':') !== false) ? 6 : 4;
            $loc = $wpdb->get_var($wpdb->prepare("SELECT addr FROM {$wpdb->prefix}sba_ip_data WHERE ip_type = %d AND %s BETWEEN start_bin AND end_bin LIMIT 1", $type, $ip_bin));
        }
        $final = ($loc ?: "未知位置"); $results[$ip] = $final; $cache[$ip] = $final;
    }
    update_option('sba_geo_cache', $cache, false); wp_send_json_success($results);
});

add_action('wp_ajax_sba_load_tracks', function() {
    global $wpdb; $p = intval($_POST['page'] ?? 1); $per = 50; $off = ($p - 1) * $per;
    $total = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}dis_stats WHERE visit_date = CURDATE()");
    $pages = max(1, ceil($total / $per));
    $rows = $wpdb->get_results($wpdb->prepare("SELECT ip, url, pv, last_visit FROM {$wpdb->prefix}dis_stats WHERE visit_date = CURDATE() ORDER BY last_visit DESC LIMIT %d, %d", $off, $per));
    $html = "";
    if($rows) { foreach($rows as $r) {
        $html .= "<tr><td>".date('H:i',strtotime($r->last_visit))."</td><td><code>".$r->ip."</code></td><td><small class='geo-tag' data-ip='".$r->ip."'>解析中...</small></td><td><div class='sba-cell-wrap'><small>".esc_html($r->url)."</small></div></td><td><b>".$r->pv."</b></td></tr>";
    } }
    wp_send_json_success(['html' => $html, 'pages' => $pages, 'total' => $total]);
});

/* ================= 4. 后台 UI 看板 ================= */
add_action('admin_menu', function() {
    add_menu_page('全行为审计', '全行为审计', 'manage_options', 'sba_audit', 'sba_render_dashboard', 'dashicons-shield-alt');
    add_submenu_page('sba_audit', '防御设置', '防御设置', 'manage_options', 'sba_settings', 'sba_render_settings');
});

function sba_render_dashboard() {
    global $wpdb;
    $online = $wpdb->get_var("SELECT COUNT(DISTINCT ip) FROM {$wpdb->prefix}dis_stats WHERE last_visit > DATE_SUB(NOW(), INTERVAL 5 MINUTE)");
    $today = $wpdb->get_row("SELECT COUNT(DISTINCT ip) as uv, SUM(pv) as pv FROM {$wpdb->prefix}dis_stats WHERE visit_date = CURDATE()");
    $history_50 = $wpdb->get_results("SELECT visit_date, COUNT(DISTINCT ip) as uv, SUM(pv) as pv FROM {$wpdb->prefix}dis_stats GROUP BY visit_date ORDER BY visit_date DESC LIMIT 50", OBJECT_K);
    $chart_labels = []; $chart_uv = []; $chart_pv = [];
    for ($i = 29; $i >= 0; $i--) { 
        $date = date('Y-m-d', strtotime("-$i days")); 
        $chart_labels[] = $date; 
        $chart_uv[] = $history_50[$date]->uv ?? 0;
        $chart_pv[] = $history_50[$date]->pv ?? 0;
    }
    $blocks = $wpdb->get_results("SELECT * FROM {$wpdb->prefix}sba_blocked_log ORDER BY block_time DESC LIMIT 15");
    ?>
    <style>
        .sba-wrap { max-width: 1400px; margin-top: 15px; font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif; }
        .sba-card { background:#fff; padding:20px; border-radius:12px; margin-bottom:20px; box-shadow:0 4px 15px rgba(0,0,0,0.05); }
        .sba-grid { display:grid; grid-template-columns: 1fr 1fr; gap:20px; }
        @media (max-width: 1000px) { .sba-grid { grid-template-columns: 1fr; } }
        .sba-scroll-x { width: 100%; overflow-x: auto; border: 1px solid #eee; border-radius:8px; }
        .sba-table { width: 100%; min-width: 850px; border-collapse: collapse; table-layout: fixed; }
        .sba-table th, .sba-table td { text-align: left; padding: 12px 10px; border-bottom: 1px solid #f9f9f9; }
        .sba-cell-wrap { white-space: normal; word-break: break-all; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; line-height: 1.4; font-size:12px; }
        .stat-val { font-size: 26px; font-weight: bold; display: block; margin-top: 5px; }
    </style>
    <div class="wrap sba-wrap">
        <h2>🚀 SBA 站点审计概览 v1.0</h2>
        <div style="display:flex; gap:15px; margin-bottom:20px; flex-wrap:wrap;">
            <div class="sba-card" style="flex:1; border-left:4px solid #46b450;">当前在线: <span class="stat-val" style="color:#46b450;"><?php echo $online?:0; ?></span></div>
            <div class="sba-card" style="flex:1; border-left:4px solid #2271b1;">今日 UV: <span class="stat-val" style="color:#2271b1;"><?php echo $today->uv?:0; ?></span></div>
            <div class="sba-card" style="flex:1; border-left:4px solid #4fc3f7;">今日 PV: <span class="stat-val" style="color:#4fc3f7;"><?php echo $today->pv?:0; ?></span></div>
        </div>
        <div class="sba-grid">
            <div class="sba-card"><h3>📈 30天访问趋势</h3><div style="height:250px;"><canvas id="sbaChart10"></canvas></div></div>
            <div class="sba-card"><h3>📊 50天审计详表</h3>
                <div class="sba-scroll-x" style="height:250px;"><table class="sba-table" style="min-width:400px;">
                <thead><tr><th>日期</th><th>UV (人)</th><th>PV (次)</th><th>深度</th></tr></thead>
                <tbody><?php for ($j = 0; $j < 50; $j++): $d = date('Y-m-d', strtotime("-$j days")); $u = $history_50[$d]->uv ?? 0; $p = $history_50[$d]->pv ?? 0; ?>
                <tr><td><b><?php echo $d; ?></b></td><td><?php echo $u; ?></td><td><?php echo $p; ?></td><td><code><?php echo round($p/max(1,$u), 1); ?></code></td></tr><?php endfor; ?></tbody>
                </table></div>
            </div>
        </div>
        <div class="sba-card">
            <h3>👣 实时访客轨迹</h3>
            <div class="sba-scroll-x"><table class="sba-table">
                <thead><tr><th width="80">时间</th><th width="150">IP 地址</th><th width="200">归属地</th><th>访问路径</th><th width="60">PV</th></tr></thead>
                <tbody id="track-body"></tbody>
            </table></div>
            <div style="margin-top:15px; display:flex; justify-content: space-between;">
                <div>总计: <b id="total-rows">0</b></div>
                <div><button id="prev-page" class="button">上页</button> 第 <b id="current-page">1</b> / <b id="total-pages">1</b> 页 <button id="next-page" class="button">下页</button></div>
            </div>
        </div>
        <div class="sba-card" style="border-top:3px solid #d63638;">
            <h3>🚫 拦截日志 (Security Log)</h3>
            <div class="sba-scroll-x"><table class="sba-table">
                <thead><tr><th width="100">时间</th><th width="150">拦截 IP</th><th>拦截原因与目标</th></tr></thead>
                <tbody><?php foreach($blocks as $b): ?><tr><td><?php echo date('m-d H:i', strtotime($b->block_time)); ?></td><td><code><?php echo $b->ip; ?></code></td><td class="sba-cell-wrap" style="color:#d63638;"><?php echo $b->reason; ?> ⚡ <?php echo esc_html($b->target_url); ?></td></tr><?php endforeach; ?></tbody>
            </table></div>
        </div>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    <script>
    new Chart(document.getElementById('sbaChart10'), { 
        type:'line', data:{
            labels:<?php echo json_encode($chart_labels); ?>, 
            datasets:[
                {label:'UV', data:<?php echo json_encode($chart_uv); ?>, borderColor:'#2271b1', tension:0.1, fill:false},
                {label:'PV', data:<?php echo json_encode($chart_pv); ?>, borderColor:'#4fc3f7', tension:0.1, fill:false}
            ]
        }, options:{maintainAspectRatio:false} 
    });
    let curP = 1, maxP = 1;
    const loadT = (p) => {
        fetch(ajaxurl, { method: 'POST', body: new URLSearchParams({action:'sba_load_tracks', page:p}) }).then(r => r.json()).then(res => {
            if(res.success) { document.getElementById('track-body').innerHTML = res.data.html; curP = p; maxP = res.data.pages; document.getElementById('current-page').innerText = p; document.getElementById('total-pages').innerText = maxP; document.getElementById('total-rows').innerText = res.data.total; processGeos(); }
        });
    };
    async function processGeos() {
        const badges = Array.from(document.querySelectorAll('.geo-tag')).filter(b => b.innerText === '解析中...');
        for (let i = 0; i < badges.length; i += 5) {
            const chunk = badges.slice(i, i + 5);
            const fd = new FormData(); fd.append('action', 'sba_get_geo'); chunk.forEach(b => fd.append('ips[]', b.dataset.ip));
            fetch(ajaxurl, { method: 'POST', body: fd }).then(r => r.json()).then(j => { if(j.success) chunk.forEach(b => b.innerText = j.data[b.dataset.ip]); });
        }
    }
    document.getElementById('prev-page').onclick = () => { if(curP > 1) loadT(curP - 1); };
    document.getElementById('next-page').onclick = () => { if(curP < maxP) loadT(curP + 1); };
    loadT(1);
    </script>
    <?php
}

/* ================= 5. 设置页面 ================= */
function sba_render_settings() {
    $opts = get_option('sba_settings');
    global $wpdb; 
    $v4 = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}sba_ip_data WHERE ip_type = 4");
    $v6 = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}sba_ip_data WHERE ip_type = 6");
    ?>
    <div class="wrap sba-wrap">
        <h1>🛠️ SBA 设置与说明手册</h1>
        <div class="sba-card" style="background:#fffbe6; border-left:5px solid #faad14;">
            <h3>📖 使用说明 (必读)</h3>
            <p>1. <b>用户名白名单</b>:填入管理员用户名,登录状态下免除所有拦截,防止站长误杀。</p>
            <p>2. <b>Gate 钥匙</b>:若设为 <code>xyz</code>,则登录地址强制变为 <code>wp-login.php?gate=xyz</code>。</p>
            <p>3. <b>IP 库导入</b>:上传 7 列格式的 TXT 文件,系统自动区分 IPv4 与 IPv6 载入。</p>
        </div>
        <form method="post" action="options.php">
            <?php settings_fields('sba_settings_group'); register_setting('sba_settings_group', 'sba_settings'); ?>
            <div class="sba-grid">
                <div class="sba-card">
                    <h3>✅ 信任通道</h3>
                    <table class="form-table">
                        <tr><th>用户名白名单</th><td><input type="text" name="sba_settings[user_whitelist]" value="<?php echo esc_attr($opts['user_whitelist'] ?? ''); ?>" placeholder="Stone" class="regular-text" /><br><small>登录此用户时,系统自动信任。</small></td></tr>
                        <tr><th>IP 白名单</th><td><textarea name="sba_settings[ip_whitelist]" rows="3" style="width:100%"><?php echo esc_textarea($opts['ip_whitelist'] ?? ''); ?></textarea></td></tr>
                    </table>
                </div>
                <div class="sba-card">
                    <h3>🚫 防御配置</h3>
                    <table class="form-table">
                        <tr><th>CC 封禁阈值</th><td><input type="number" name="sba_settings[auto_block_limit]" value="<?php echo esc_attr($opts['auto_block_limit'] ?? '60'); ?>" /> 次/分</td></tr>
                        <tr><th>Gate 钥匙</th><td><input type="text" name="sba_settings[login_slug]" value="<?php echo esc_attr($opts['login_slug'] ?? ''); ?>" /></td></tr>
                        <tr><th>追加拦截路径</th><td><input type="text" name="sba_settings[evil_paths]" value="<?php echo esc_attr($opts['evil_paths'] ?? ''); ?>" style="width:100%" /></td></tr>
                    </table>
                    <?php submit_button('保存核心配置'); ?>
                </div>
            </div>
        </form>
        <div class="sba-card">
            <h3>📡 IP 库同步 (v4: <?php echo number_format($v4); ?> | v6: <?php echo number_format($v6); ?>)</h3>
            <form method="post" enctype="multipart/form-data">
                <input type="file" name="sba_ip_file" accept=".txt">
                <input type="submit" name="sba_import_action" class="button button-primary" value="载入 7 列 IP 数据">
            </form>
        </div>
    </div>
    <?php
}

/* ================= 6. 后台逻辑同步 ================= */
add_action('admin_init', function() {
    register_setting('sba_settings_group', 'sba_settings');
    if(isset($_POST['sba_import_action']) && !empty($_FILES['sba_ip_file']['tmp_name'])) {
        global $wpdb; $table = $wpdb->prefix . "sba_ip_data";
        $handle = fopen($_FILES['sba_ip_file']['tmp_name'], "r");
        if ($handle) {
            $first = fgets($handle); 
            if ($first) {
                $p = explode('|', trim($first));
                $type = (strpos($p[0] ?? '', ':') !== false) ? 6 : 4;
                $wpdb->query($wpdb->prepare("DELETE FROM $table WHERE ip_type = %d", $type));
                rewind($handle);
            }
            $batch = []; $i = 0;
            while (($line = fgets($handle)) !== false) {
                $p = explode('|', trim($line));
                if (count($p) >= 3) {
                    $s_bin = @inet_pton($p[0]); $e_bin = @inet_pton($p[1]);
                    if (!$s_bin || !$e_bin) continue;
                    $batch[] = $wpdb->prepare("(%d, %s, %s, %s)", $type, $s_bin, $e_bin, implode('·', array_slice($p,2)));
                    if (count($batch) >= 800) { $wpdb->query("INSERT INTO $table (ip_type, start_bin, end_bin, addr) VALUES " . implode(',', $batch)); $batch = []; }
                    $i++;
                }
            }
            if (!empty($batch)) $wpdb->query("INSERT INTO $table (ip_type, start_bin, end_bin, addr) VALUES " . implode(',', $batch));
            fclose($handle);
            add_settings_error('sba_settings', 'import', "同步成功:载入 $i 条记录。", 'updated');
        }
    }
});

然后在wp-admin管理界面插件中启用并配置插件。


四、 使用与常见问题

1. 为什么启用后我被拦截了?

大概率是因为你还没来得及在设置里填入用户名白名单解决方法

  1. 通过 FTP 临时编辑 site-behavior-auditor.php,在 sba_execute_block 函数第一行加入 return;
  2. 刷新后台,进入“防御设置”填好你的用户名并保存。
  3. 删除代码里的 return;

2. 数据库性能

插件使用了 ON DUPLICATE KEY UPDATEVARBINARY 索引优化,即使每天有数万次访问,对服务器的负担也极小。


结语: 安全不应该是一个黑盒。通过这套系统,我们可以清晰地看到每一个请求的意图。这不仅是防御工具,更是理解站点运行状态的窗口。

发表回复

联系站长