普通视图

Received before yesterday

基于 wp-cron.php 的拒绝服务攻击

作者obaby
2025年11月12日 14:02

这几天不知道是发生什么事了,说是不知道什么事情,但是大概率是被打了。只是这次打的挺高级的,外层的 eo 貌似也没什么反应。只是那个访问量通过 umami 看,直接爆炸了。

平常几百的访问量,昨天的时候,结果到了 2000 多,当然这不是最奇怪的,奇怪的是服务器过了会儿卡死了。之前都是因为请求太多 php-fpm 耗尽 cpu 资源卡死了,这次以为还是同样的问题。然而,并不是,发现 mysql 把 cpu 跑满了,查看日志的时候发现大量的 wp-cron.php 的请求,这尼玛,请求直接透传过来了。

另外还有一大堆 bot 的请求,包括 bing 以及一些乱起八糟的爬虫遍历。

最开始没想到什么好办法,简单粗暴的把 wp-cron.php 改名了,暂时解决了这个问题。

不过这个方法的确是高明,带着参数透传过来,wp 就是疯狂的执行,一条没执行完就到了下一条。然而,对于这种事情直接改名的确是可以解决办法,不过后来想了一下还是直接从 eo 下手吧。

尽管 eo 防住了 22 万次的攻击,但是,这些透传的请求,直接让 mysql 耗尽了 cpu 资源,也是个不错的办法,甚至请求频率都不用太高。流量到了 144g,这也不知道是哪个哥们又闲的蛋疼了,如果真的蛋疼来找姐姐啊,姐姐帮你治疗,直接给你割下来,塞你自己嘴里!

昨天晚上发现这个情况的时候,本来是想去处理下的,结果对象在用电脑,自己又不想去开笔记本,就用手机处理了一下,简单的改下了文件名。

今天早上才处理了一下,加到了 eo 的访问规则里:

尽管如此,还是对这几天的访问记录比较好奇,想看看请求了多少次。去拉 nginx 日志的时候发现文件已经 1.5G 了。直接截取这几天的记录,用 goaccess 跑了一下,但是比较奇怪的是这个 wp-cron.php 的请求竟然没有。

暂时放弃 goaccess 直接使用 ngxtop 进行数据分析:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
使用ngxtop分析Nginx日志中的POST请求
提供交互式菜单和多种分析选项
"""

import subprocess
import sys
import os
from pathlib import Path


def run_ngxtop(cmd_args):
    """运行ngxtop命令"""
    venv_python = Path(__file__).parent / "venv" / "bin" / "python"
    ngxtop_script = Path(__file__).parent / "venv" / "bin" / "ngxtop"
    
    if not ngxtop_script.exists():
        print("错误: ngxtop未安装,请先运行: source venv/bin/activate && pip install ngxtop")
        sys.exit(1)
    
    try:
        result = subprocess.run(
            [str(ngxtop_script)] + cmd_args,
            capture_output=True,
            text=True,
            check=False
        )
        print(result.stdout)
        if result.stderr and "error" in result.stderr.lower():
            print(result.stderr, file=sys.stderr)
        return result.returncode == 0
    except Exception as e:
        print(f"错误: {e}", file=sys.stderr)
        return False


def show_menu():
    """显示菜单"""
    print("\n" + "="*60)
    print("Nginx日志POST请求分析 - ngxtop工具")
    print("="*60)
    print("1. POST请求总览")
    print("2. 按URL统计POST请求 (Top 20)")
    print("3. 按IP统计POST请求 (Top 20)")
    print("4. 按状态码统计POST请求")
    print("5. POST请求中状态码为404的URL")
    print("6. POST请求中状态码为200的URL")
    print("7. 可疑POST请求 (xmlrpc, wp-login等)")
    print("8. POST请求详情示例")
    print("9. 自定义查询")
    print("0. 退出")
    print("="*60)


def analyze_post_requests(log_file):
    """分析POST请求"""
    if not os.path.exists(log_file):
        print(f"错误: 日志文件 {log_file} 不存在")
        return
    
    base_args = ["-l", log_file, "--no-follow", "-i", 'request.startswith("POST")']
    
    while True:
        show_menu()
        choice = input("\n请选择分析选项 (0-9): ").strip()
        
        if choice == "0":
            print("退出分析")
            break
        elif choice == "1":
            print("\n【POST请求总览】")
            print("-" * 60)
            run_ngxtop(base_args + ["--limit", "0"])
        elif choice == "2":
            print("\n【按URL统计POST请求 (Top 20)】")
            print("-" * 60)
            run_ngxtop(base_args + ["--group-by", "request_path", "--limit", "20"])
        elif choice == "3":
            print("\n【按IP统计POST请求 (Top 20)】")
            print("-" * 60)
            run_ngxtop(base_args + ["--group-by", "remote_addr", "--limit", "20"])
        elif choice == "4":
            print("\n【按状态码统计POST请求】")
            print("-" * 60)
            run_ngxtop(base_args + ["--group-by", "status", "--limit", "0"])
        elif choice == "5":
            print("\n【POST请求中状态码为404的URL (Top 10)】")
            print("-" * 60)
            run_ngxtop(["-l", log_file, "--no-follow", 
                       "-i", 'request.startswith("POST") and status == 404',
                       "--group-by", "request_path", "--limit", "10"])
        elif choice == "6":
            print("\n【POST请求中状态码为200的URL (Top 10)】")
            print("-" * 60)
            run_ngxtop(["-l", log_file, "--no-follow",
                       "-i", 'request.startswith("POST") and status == 200',
                       "--group-by", "request_path", "--limit", "10"])
        elif choice == "7":
            print("\n【可疑POST请求统计】")
            print("-" * 60)
            run_ngxtop(["-l", log_file, "--no-follow",
                       "-i", 'request.startswith("POST") and (request_path == "/xmlrpc.php" or request_path == "/wp-login.php" or request_path.startswith("/wp-admin"))',
                       "--group-by", "request_path", "--limit", "0"])
        elif choice == "8":
            print("\n【POST请求详情示例 (前10条)】")
            print("-" * 60)
            run_ngxtop(base_args + ["print", "remote_addr", "time_local", "request", "status", "bytes_sent", "--limit", "10"])
        elif choice == "9":
            print("\n【自定义查询】")
            print("-" * 60)
            print("示例查询:")
            print("  - 查看特定URL: ngxtop -l <file> -i 'request.startswith(\"POST\") and request_path == \"/wp-cron.php\"'")
            print("  - 查看特定IP: ngxtop -l <file> -i 'request.startswith(\"POST\") and remote_addr == \"114.66.247.160\"'")
            print("  - 查看错误请求: ngxtop -l <file> -i 'request.startswith(\"POST\") and status >= 400'")
            print("\n请输入自定义ngxtop命令参数 (用空格分隔):")
            custom_args = input("> ").strip().split()
            if custom_args:
                run_ngxtop(["-l", log_file, "--no-follow"] + custom_args)
        else:
            print("无效的选择,请重试")
        
        input("\n按回车键继续...")


def main():
    """主函数"""
    if len(sys.argv) < 2:
        # 查找默认日志文件
        log_files = list(Path(".").glob("*.txt"))
        if log_files:
            default_log = str(log_files[0])
            print(f"未指定日志文件,使用默认: {default_log}")
            log_file = default_log
        else:
            print("用法: python analyze_with_ngxtop.py <日志文件路径>")
            print("示例: python analyze_with_ngxtop.py 11-08_org.txt")
            sys.exit(1)
    else:
        log_file = sys.argv[1]
    
    analyze_post_requests(log_file)


if __name__ == "__main__":
    main()

运行命令:

python3 analyze_with_ngxtop.py 11-08_org.txt

分析结果:

【按URL统计POST请求 (Top 20)】
------------------------------------------------------------

running for 7 seconds, 23670 records processed: 3508.50 req/sec

Summary:
|   count |   avg_bytes_sent |   2xx |   3xx |   4xx |   5xx |
|---------+------------------+-------+-------+-------+-------|
|   23670 |         2381.924 |  4678 |    21 | 18574 |   397 |

Detailed:
| request_path                    |   count |   avg_bytes_sent |   2xx |   3xx |   4xx |   5xx |
|---------------------------------+---------+------------------+-------+-------+-------+-------|
| /wp-cron.php                    |   16454 |          731.309 |  3413 |     0 | 13034 |     7 |
| /xmlrpc.php                     |    3102 |          416.754 |   248 |     0 |  2853 |     1 |
| /wp-login.php                   |    2519 |        15204.250 |     0 |     0 |  2519 |     0 |
| /wp-admin/admin-ajax.php        |    1017 |          542.043 |   971 |     0 |    44 |     2 |
| /wp-comments-post.php           |     401 |         2551.357 |     0 |    14 |     0 |   387 |
| /xmrpc.php                      |      41 |          915.000 |     0 |     0 |    41 |     0 |
| /tslogin                        |      20 |        30543.150 |    16 |     4 |     0 |     0 |
| /alfacgiapi/perl.alfa           |      11 |        51292.455 |     0 |     0 |    11 |     0 |
| /ALFA_DATA/alfacgiapi/perl.alfa |      11 |        51323.636 |     0 |     0 |    11 |     0 |
| /index.php                      |      10 |        34570.900 |    10 |     0 |     0 |     0 |
| /wp-plain.php                   |       9 |         1331.000 |     0 |     0 |     9 |     0 |
| /                               |       9 |        28609.556 |     7 |     0 |     2 |     0 |
|                                 |       8 |          415.000 |     8 |     0 |     0 |     0 |
| /flow.php                       |       7 |          915.000 |     0 |     0 |     7 |     0 |
| /wp-admin/async-upload.php      |       5 |          736.000 |     5 |     0 |     0 |     0 |
| /php-cgi/php-cgi.exe            |       4 |        33911.500 |     0 |     0 |     4 |     0 |
| /graphql                        |       4 |        33469.750 |     0 |     0 |     4 |     0 |
| /wp-admin/post.php              |       3 |            5.000 |     0 |     3 |     0 |     0 |
| /member/success.aspx            |       2 |        16784.500 |     0 |     0 |     2 |     0 |
| /e/aspx/upload.aspx             |       2 |        16628.500 |     0 |     0 |     2 |     0 |

【按IP统计POST请求 (Top 20)】
------------------------------------------------------------
running for 7 seconds, 23670 records processed: 3586.40 req/sec

Summary:
|   count |   avg_bytes_sent |   2xx |   3xx |   4xx |   5xx |
|---------+------------------+-------+-------+-------+-------|
|   23670 |         2381.924 |  4678 |    21 | 18574 |   397 |

Detailed:
| remote_addr    |   count |   avg_bytes_sent |   2xx |   3xx |   4xx |   5xx |
|----------------+---------+------------------+-------+-------+-------+-------|
| 221.204.26.162 |    4407 |          696.960 |  1125 |     1 |  3279 |     2 |
| 221.204.26.233 |    4291 |          738.947 |  1054 |     1 |  3235 |     1 |
| 101.71.101.44  |    3168 |          686.088 |   911 |     4 |  2252 |     1 |
| 101.71.101.106 |    2564 |          868.693 |   183 |     2 |  2379 |     0 |
| 43.174.53.229  |    2094 |         7795.611 |     6 |     0 |  2088 |     0 |
| 43.174.53.236  |    2090 |         7811.496 |     4 |     0 |  2086 |     0 |
| 114.66.247.160 |    1810 |          743.818 |   520 |     1 |  1288 |     1 |
| 114.66.246.149 |    1123 |          507.375 |   538 |     1 |   582 |     2 |
| 101.71.105.47  |     104 |          574.404 |    57 |     0 |    47 |     0 |
| 43.175.19.192  |      29 |         5430.241 |     1 |     0 |    15 |    13 |
| 43.175.17.169  |      26 |         2520.500 |     0 |     0 |     8 |    18 |
| 43.175.18.81   |      25 |         2049.720 |     1 |     0 |     6 |    18 |
| 43.175.18.253  |      25 |         1835.800 |     1 |     0 |     8 |    16 |
| 43.175.18.195  |      25 |         5997.720 |     0 |     0 |     8 |    17 |
| 43.175.18.137  |      25 |         2101.840 |     1 |     0 |     5 |    19 |
| 43.175.17.87   |      24 |         2210.208 |     0 |     0 |     5 |    19 |
| 43.175.17.47   |      23 |         7488.043 |     0 |     0 |     9 |    14 |
| 43.175.18.51   |      22 |         3213.455 |     0 |     0 |     8 |    14 |
| 43.175.17.205  |      21 |         7011.381 |     1 |     0 |    10 |    10 |
| 43.175.169.137 |      16 |         1386.562 |     3 |     0 |     6 |     7 |

而至于这些 IP 地址,多数都是国内的,这个倒是也在意料之内,毕竟国外的被拦截的概率会更高一些。

然而,goaccess 就无法分析吗?也可以,添加忽略请求参数的参数就可以了:

#!/bin/bash
# 使用goaccess的--no-query-string参数移除查询参数
# 不需要修改日志文件!

LOG_FILE="${1:-11-08_org.txt}"
OUTPUT_FILE="${2:-goaccess_no_query_report.html}"

if [ ! -f "$LOG_FILE" ]; then
    echo "错误: 日志文件 $LOG_FILE 不存在"
    exit 1
fi

echo "=========================================="
echo "使用GoAccess分析(移除查询参数)"
echo "=========================================="
echo "日志文件: $LOG_FILE"
echo "输出文件: $OUTPUT_FILE"
echo ""
echo "使用参数: --no-query-string (或 -q)"
echo "这将移除URL中的查询参数,只保留路径"
echo ""

# 使用--no-query-string参数
goaccess "$LOG_FILE" \
  --log-format='%h %^[%d:%t %^] "%r" %s %b "%R" "%u"' \
  --date-format='%d/%m/%Y' \
  --time-format='%H:%M:%S' \
  --no-query-string \
  -o "$OUTPUT_FILE"

if [ $? -eq 0 ]; then
    echo ""
    echo "✅ 报告生成成功: $OUTPUT_FILE"
    echo ""
    echo "现在wp-cron.php应该能正确合并统计了!"
    echo ""
    echo "在浏览器中打开报告查看:"
    echo "  open $OUTPUT_FILE    # macOS"
    echo "  xdg-open $OUTPUT_FILE  # Linux"
    echo ""
    echo "在交互界面中使用:"
    echo "  goaccess $LOG_FILE \\"
    echo "    --log-format='%h %^[%d:%t %^] \"%r\" %s %b \"%R\" \"%u\"' \\"
    echo "    --date-format='%d/%m/%Y' \\"
    echo "    --time-format='%H:%M:%S' \\"
    echo "    --no-query-string"
else
    echo "❌ 报告生成失败"
    exit 1
fi

主要就是:–no-query-string参数。

实际效果:

文件没改名之前:

文件改名之后:

虽然加起来之后不到两万次,但是却让 mysql 把 cpu 资源耗尽了,这的确不失为一个低成本的攻击方式。

爬虫占比:

这几天也不知道爬虫是发什么疯

今天的访问量:

百度的统计:

咱就是说,有点时间干点正事不好吗?真是闲的。

 

Baby WP 评论强化拦截插件 — 再战 WP 垃圾评论

作者obaby
2025年9月18日 16:56

这种生态成熟,或者说受众较大的产品,难免惦记的人就多。之前已经增加了很多方法,来弥补 akismet的不足,包括禁止非中文评论,评论长度限制,是否包含中文等等。

虽然拦截了很多垃圾评论,但是还有一些显而易见的垃圾,却还是要进审核或者回收站,这就让人的确不爽。

至于 wp 自带的评论设置,只能说没什么大用,设置了,还是一样要手工删除,这就 tmd 贼恶心。

设置了关键字之后,还是难以直接屏蔽广西的这个屌毛,天天来发币安的广告,之前已经删了无数次,加到禁止评论关键词里面还是继续发。

并且,找到了规律之后,还会带着中文发,这就很 tm 操蛋了。

还有这种来法验证码广告的:

我之所以不加验证码,主要还是觉得这个东西体验太差了。然而为了屏蔽这些傻屌,随之而来的另外一个问题就是需要在 functions.php 中改的次数越来越多了,现在已经变成了下面的样子:

/**
 * 检查评论内容是否包含禁用词
 * @param string $content 评论内容
 * @param array $banned_words 禁用词数组
 * @return bool 是否包含禁用词
 */
function has_banned_word($content, $banned_words) {
    foreach ($banned_words as $word) {
        if (stripos($content, $word) !== false) {
            return true;
        }
    }
    return false;
}


/*
 * WordPress控制文章评论最少字数
 */
function custom_comment_length( $commentdata ) {
        $max_length = 1800;
        // 设置最大字数限制
        if ( mb_strlen( $commentdata['comment_content'] ) > $max_length ) {
                wp_die( '额,你评论的内容太多啦,最多可以输入1800个字,不要再评论区写论文啊!' ,'宝贝,出错了哦 - obaby@mars', array( 'back_link'=>true ) );
        }

        if ( ! is_admin() ) {
                $comment_content = $commentdata['comment_content'];
                if ( preg_match( '/[\x{4e00}-\x{9fa5}]/u', $comment_content ) === 0) {
                        //if (strpos($commentdata['comment_author_url'],'http')!==false || strpos($comment_content,'http')!==false){
                        //      wp_die( '不要乱发哦,让姐姐我不开心就不好了嘛!','姐姐我不开心啦! - obaby@mars', array( 'back_link'=>true ) );
                        //}
                        //if (strpos($commentdata['comment_author_url'],'http')!==false || strpos($comment_content,'http')!==false){
                                wp_die( '不要乱发哦,让姐姐我不开心就不好了嘛!(评论禁止纯英文字符、数字内容)','姐姐我不开心啦! - obaby@mars', array( 'back_link'=>true ) );
                        //}
                }
                $banned_words = ['binance.info', 'binance.com','xrumersale.site'];
                if (has_banned_word($comment_content, $banned_words)){
                        wp_die( '不要乱发哦,让姐姐我不开心就不好了嘛!(你tmd别发广告了ok?你是傻逼吗?!)','姐姐我不开心啦! - obaby@mars', array( 'back_link'=>true ) );
                }
        }
        return $commentdata;
}



add_filter( 'preprocess_comment', 'custom_comment_length' );

每次要屏蔽一个傻逼,就要修改一次:banned_words。的确是有些烦人,所以,直接弄了个插件出来,当然还是得感谢 cursor,写了 99% 的代码。哈哈哈。 

功能页面:

代码开源地址:

https://github.com/obaby/baby-wp-comment-filter

插件 zip 下载:

https://github.com/obaby/baby-wp-comment-filter/releases/tag/wp

无法访问的,搭配https://ghproxy.link 食用。

偶然?还是必然? — 谁是罪魁祸首

作者obaby
2025年8月9日 11:43

宝子放假,转眼假期已经过了一半,想着找个周末带宝子回老家住几天,大姐家的孩子周日要办升学宴,既然都回来了,不去也不合适,刚好就可以周六回老家,周日去淄博。

对象说,周日青岛会下大暴雨,还是下班之后直接回吧,不然第二天很可能走不了。

就这样,马不停蹄,下班买点东西就直接接上孩子往回走。天气预报零点的雨,在九点多到潍坊的时候就开始逐渐大了。

对象开车的时候,自己打开手机想回复下评论,但是却发现一个问题,那就是页面非常卡。刚开始还以为是网络问题,但是切到短视频却异常流畅。

瞬间有种不祥的预感,那就是服务器的 cpu 肯定又跑满了。

之前看到威言威语发的《暂时停用腾讯EdgeOne了》 ,感觉自己的系统貌似没什么太大的问题。但是,实际上是在这之前,切换到 eo 之后,也出现过一次 cpu 跑满的情况,当时重启 php 之后一切正常了,以为是偶发事件。所以也没再关注,但是这次尝试远程重启服务之后,只是瞬间缓解。几分钟之后就又卡死了,所以昨天能发评论的宝子真的都是真爱,么么哒(๑•́₃•̀๑),因为我自己都打不开我的后台。😂

这种感觉还有一个前兆,那就是后台资源库突然多了个文件。

不过这个文件看起来是某个插件的压缩包,但是描述是另外一个域名,猜测是另外一个域名传上来的。但是。搜了下这个用户不存在,所以最后直接把插件和文件一起删除了。

但是这个 cpu 跑满的感觉还是感觉是 cc,不然不至于 php 能直接占满了全部资源。

到家之后,看了下 eo 的后台没什么能配置地方。但是,通过服务器的访问记录却发现,有几个地址在频繁请求登录页面:

当然,更诡异的是这里面好几个 ip 竟然都是腾讯的海外节点。这就有点奇怪了。

尤其是 163 这个,按照常理来说,如果是 cdn 节点回源,应该不会摁着这一个地址回源,并且这个请求频率大概率会被 cdn 拦截,然而,现在的现象是不单没被拦截,还高频访问,一个地址在一秒内发起了几十个请求。这尼玛就离谱了,查了一下加速域名的节点,发现这些都不在 eo 的节点列表内。

那么,此时会合理的猜的就是 eo 的机房内或者是在某些地方的节点回源会泄露原始服务器地址,这样这一系列的请求就说的通了。这些请求本身并没有结果 cdn,所以直接是从机房发起的请求。

在回源地址泄露之后,有人通过 bot 控制了大量机房内的机器,尝试对 wp 的登录地址进行暴力破解,而这告诉 cc 也导致了家里的 mac mini 的服务器资源瞬间被耗尽了。

所以这个问题不是 eo 本身导致的,但是确实是套了 eo 的国外节点之后,有 eo 的某些请求泄露回源地址导致出现了直接针对服务器的 cc 攻击。这个攻击目标很明确,就是破解登录密码。本来想直接封锁那几个 ip,但是封锁之后发现还是不断的有新的开始尝试,于是,最新版本的工具就出现了,直接写个脚本,通过检测 nginx 访问日志,实现对 ip 动态风控。

代码如下:

#!/bin/bash

# 实时监控日志文件并自动封锁恶意IP
# 使用方法: sudo ./realtime_block.sh

LOG_FILE="/home/wwwlogs/h4ck.org.cn.log"
BLOCKED_IPS_FILE="/tmp/blocked_ips.txt"
MAX_REQUESTS_PER_MINUTE=10
BLOCK_DURATION=3600  # 封锁时间(秒)
DEBUG_MODE=false  # 设置为true启用调试模式
QUIET_MODE=false  # 设置为true启用静默模式(只显示新增封锁)
ATTACK_HISTORY_FILE="/tmp/attack_history.txt"  # 攻击历史记录文件

# 检查是否以root权限运行
if [ "$EUID" -ne 0 ]; then
    echo "请使用sudo运行此脚本"
    exit 1
fi

# 检查inotify-tools是否安装
if ! command -v inotifywait &> /dev/null; then
    echo "请先安装inotify-tools:"
    echo "Ubuntu/Debian: sudo apt-get install inotify-tools"
    echo "CentOS/RHEL: sudo yum install inotify-tools"
    exit 1
fi

# 检查日志文件是否存在
if [ ! -f "$LOG_FILE" ]; then
    echo "错误: 日志文件 $LOG_FILE 不存在"
    exit 1
fi

# 创建iptables链(如果不存在)
iptables -N BLOCK_MALICIOUS_IPS 2>/dev/null

# 确保BLOCK_MALICIOUS_IPS链在INPUT链的最前面
iptables -C INPUT -j BLOCK_MALICIOUS_IPS 2>/dev/null || iptables -I INPUT 1 -j BLOCK_MALICIOUS_IPS

# 初始化文件
touch "$BLOCKED_IPS_FILE"
touch "$ATTACK_HISTORY_FILE"

# 函数:检查并修复iptables配置
check_iptables_config() {
    echo "检查iptables配置..."
    
    # 检查BLOCK_MALICIOUS_IPS链是否存在
    if ! iptables -L BLOCK_MALICIOUS_IPS >/dev/null 2>&1; then
        echo "创建BLOCK_MALICIOUS_IPS链..."
        iptables -N BLOCK_MALICIOUS_IPS
    fi
    
    # 检查INPUT链中是否包含BLOCK_MALICIOUS_IPS
    if ! iptables -C INPUT -j BLOCK_MALICIOUS_IPS 2>/dev/null; then
        echo "将BLOCK_MALICIOUS_IPS链插入到INPUT链的最前面..."
        iptables -I INPUT 1 -j BLOCK_MALICIOUS_IPS
    fi
    
    # 显示当前配置
    echo "当前iptables配置:"
    iptables -L INPUT -n --line-numbers | head -10
    echo "BLOCK_MALICIOUS_IPS链规则:"
    iptables -L BLOCK_MALICIOUS_IPS -n
}

# 函数:重新加载已封锁的IP到iptables
reload_blocked_ips() {
    echo "重新加载已封锁的IP到iptables..."
    
    if [ -f "$BLOCKED_IPS_FILE" ]; then
        local count=0
        while read -r ip; do
            if [ -n "$ip" ]; then
                # 检查iptables中是否已有此规则
                if ! iptables -C BLOCK_MALICIOUS_IPS -s "$ip" -j DROP 2>/dev/null; then
                    iptables -A BLOCK_MALICIOUS_IPS -s "$ip" -j DROP
                    count=$((count + 1))
                fi
            fi
        done < "$BLOCKED_IPS_FILE"
        echo "重新加载了 $count 个IP到iptables"
    fi
}

# 检查并修复iptables配置
check_iptables_config

# 重新加载已封锁的IP
reload_blocked_ips

echo "开始实时监控日志文件: $LOG_FILE"
echo "最大请求频率: $MAX_REQUESTS_PER_MINUTE 次/分钟"
echo "封锁时间: $BLOCK_DURATION 秒"

# 函数:封锁IP
block_ip() {
    local ip=$1
    local reason=$2
    
    # 检查IP是否已经被封锁
    if ! grep -q "^$ip$" "$BLOCKED_IPS_FILE"; then
        echo "$(date '+%Y-%m-%d %H:%M:%S') - 封锁IP: $ip (原因: $reason)"
        
        # 添加到iptables(确保规则正确添加)
        iptables -A BLOCK_MALICIOUS_IPS -s "$ip" -j DROP
        
        # 验证规则是否添加成功
        if iptables -C BLOCK_MALICIOUS_IPS -s "$ip" -j DROP 2>/dev/null; then
            echo "$(date '+%Y-%m-%d %H:%M:%S') - 成功添加iptables规则: $ip"
        else
            echo "$(date '+%Y-%m-%d %H:%M:%S') - 警告: iptables规则添加失败: $ip"
        fi
        
        # 记录到文件
        echo "$ip" >> "$BLOCKED_IPS_FILE"
        
        # 记录日志
        echo "$(date '+%Y-%m-%d %H:%M:%S') - 封锁IP: $ip (原因: $reason)" >> /var/log/realtime_block.log
    else
        echo "$(date '+%Y-%m-%d %H:%M:%S') - IP $ip 已经被封锁"
    fi
}

# 函数:记录攻击历史
record_attack() {
    local ip=$1
    local attack_type=$2
    local timestamp=$(date +%s)
    echo "$ip|$attack_type|$timestamp" >> "$ATTACK_HISTORY_FILE"
}

# 函数:检查攻击历史
check_attack_history() {
    local ip=$1
    local attack_type=$2
    local current_time=$(date +%s)
    local time_window=300  # 5分钟时间窗口
    
    # 清理过期的攻击记录
    local temp_file="/tmp/temp_attack_history.txt"
    while IFS='|' read -r record_ip record_type record_time; do
        if [ $((current_time - record_time)) -lt $time_window ]; then
            echo "$record_ip|$record_type|$record_time" >> "$temp_file"
        fi
    done < "$ATTACK_HISTORY_FILE"
    
    mv "$temp_file" "$ATTACK_HISTORY_FILE" 2>/dev/null
    
    # 统计该IP在时间窗口内的攻击次数
    local attack_count=$(grep "^$ip|" "$ATTACK_HISTORY_FILE" | wc -l)
    echo "$attack_count"
}

# 函数:分析新日志条目
analyze_new_logs() {
    local new_lines="$1"
    
    if [ -n "$new_lines" ]; then
        if [ "$DEBUG_MODE" = true ]; then
            echo "DEBUG: 分析新日志行数: $(echo "$new_lines" | wc -l)"
        fi
        
        # 统计IP请求频率
        local ip_counts=$(echo "$new_lines" | awk '{print $1}' | sort | uniq -c | sort -nr)
        
        if [ "$DEBUG_MODE" = true ] && [ -n "$ip_counts" ]; then
            echo "DEBUG: IP统计结果:"
            echo "$ip_counts"
        fi
        
        # 检查每个IP的请求频率
        while read -r count ip; do
            if [ -n "$ip" ] && [ "$count" -gt "$MAX_REQUESTS_PER_MINUTE" ]; then
                record_attack "$ip" "high_frequency"
                local total_attacks=$(check_attack_history "$ip" "high_frequency")
                if [ "$total_attacks" -gt 2 ]; then
                    block_ip "$ip" "请求频率过高: ${count}次/分钟 (累计${total_attacks}次)"
                fi
            fi
        done <<< "$ip_counts"
        
        # 检查WordPress登录尝试
        local wp_login_attempts=$(echo "$new_lines" | grep "POST /wp-login.php" | awk '{print $1}' | sort | uniq -c | sort -nr)
        
        if [ "$DEBUG_MODE" = true ] && [ -n "$wp_login_attempts" ]; then
            echo "DEBUG: WordPress登录尝试统计:"
            echo "$wp_login_attempts"
        fi
        
        if [ -n "$wp_login_attempts" ]; then
            while read -r count ip; do
                if [ -n "$ip" ]; then
                    record_attack "$ip" "wp_login"
                    local total_attacks=$(check_attack_history "$ip" "wp_login")
                    if [ "$total_attacks" -gt 1 ]; then  # 只要超过1次WordPress登录尝试就封锁
                        block_ip "$ip" "WordPress暴力破解: ${count}次尝试 (累计${total_attacks}次)"
                    fi
                fi
            done <<< "$wp_login_attempts"
        fi
        
        # 检查404错误(可能的扫描行为)
        local error_404_attempts=$(echo "$new_lines" | grep " 404 " | awk '{print $1}' | sort | uniq -c | sort -nr)
        
        if [ -n "$error_404_attempts" ]; then
            while read -r count ip; do
                if [ -n "$ip" ] && [ "$count" -gt 5 ]; then  # 降低阈值到5次404错误
                    record_attack "$ip" "error_404"
                    local total_attacks=$(check_attack_history "$ip" "error_404")
                    if [ "$total_attacks" -gt 2 ]; then
                        block_ip "$ip" "扫描行为: ${count}次404错误 (累计${total_attacks}次)"
                    fi
                fi
            done <<< "$error_404_attempts"
        fi
        
        # 检查可疑的扫描行为(访问不存在的文件)
        local suspicious_requests=$(echo "$new_lines" | grep -E "(\.php|\.asp|\.jsp|\.exe|\.bat|\.cmd)" | awk '{print $1}' | sort | uniq -c | sort -nr)
        
        if [ -n "$suspicious_requests" ]; then
            while read -r count ip; do
                if [ -n "$ip" ] && [ "$count" -gt 3 ]; then  # 降低阈值到3次可疑请求
                    record_attack "$ip" "suspicious"
                    local total_attacks=$(check_attack_history "$ip" "suspicious")
                    if [ "$total_attacks" -gt 1 ]; then
                        block_ip "$ip" "可疑扫描: ${count}次可疑请求 (累计${total_attacks}次)"
                    fi
                fi
            done <<< "$suspicious_requests"
        fi
    fi
}

# 函数:监控日志文件变化
monitor_log_file() {
    echo "开始实时监控..."
    
    # 记录当前文件行数
    local last_line_count=$(wc -l < "$LOG_FILE" 2>/dev/null || echo 0)
    
    # 使用inotifywait监控文件变化
    inotifywait -m -e modify "$LOG_FILE" | while read -r directory events filename; do
        # 获取当前文件行数
        local current_line_count=$(wc -l < "$LOG_FILE" 2>/dev/null || echo 0)
        
        if [ "$current_line_count" -gt "$last_line_count" ]; then
            if [ "$DEBUG_MODE" = true ]; then
                echo "DEBUG: 检测到新日志行,从第 $((last_line_count + 1)) 行开始"
            fi
            
            # 获取新增的日志行
            local new_lines=$(tail -n +$((last_line_count + 1)) "$LOG_FILE" 2>/dev/null)
            
            if [ -n "$new_lines" ]; then
                # 记录分析前的封锁数量
                local old_blocked_count=$(wc -l < "$BLOCKED_IPS_FILE" 2>/dev/null || echo 0)
                
                # 分析新日志
                analyze_new_logs "$new_lines"
                
                # 获取分析后的封锁数量
                local new_blocked_count=$(wc -l < "$BLOCKED_IPS_FILE" 2>/dev/null || echo 0)
                
                # 根据模式输出不同的信息
                if [ "$new_blocked_count" -gt "$old_blocked_count" ]; then
                    # 有新增封锁
                    if [ "$QUIET_MODE" = true ]; then
                        # 静默模式:只显示新增数量
                        echo "$(date '+%Y-%m-%d %H:%M:%S') - 新增封锁 $(($new_blocked_count - $old_blocked_count)) 个IP"
                    else
                        # 正常模式:显示详细信息
                        echo "$(date '+%Y-%m-%d %H:%M:%S') - 新增封锁 $(($new_blocked_count - $old_blocked_count)) 个IP,总计 $new_blocked_count 个"
                    fi
                elif [ "$QUIET_MODE" = false ] && [ "$DEBUG_MODE" = false ]; then
                    # 非静默且非调试模式:显示处理状态(可选)
                    echo "$(date '+%Y-%m-%d %H:%M:%S') - 处理日志,当前封锁 $new_blocked_count 个IP"
                fi
            fi
            
            # 更新行数
            last_line_count=$current_line_count
        fi
    done
}

# 函数:定期清理过期封锁
cleanup_expired_blocks() {
    while true; do
        sleep 3600  # 每小时执行一次
        
        local current_time=$(date +%s)
        local temp_file="/tmp/temp_blocked_ips.txt"
        
        # 重新创建封锁文件(简化处理)
        if [ -f "$BLOCKED_IPS_FILE" ]; then
            # 这里可以根据需要实现更精确的时间管理
            # 目前简化处理,每小时清理一次
            echo "$(date '+%Y-%m-%d %H:%M:%S') - 清理过期封锁" >> /var/log/realtime_block.log
        fi
    done
}

# 函数:测试模式
test_mode() {
    echo "测试模式: 模拟恶意请求..."
    
    # 创建测试日志条目
    local test_log_entries=(
        "192.168.1.100 - - [$(date '+%d/%b/%Y:%H:%M:%S')] \"GET / HTTP/1.1\" 200 1234"
        "192.168.1.100 - - [$(date '+%d/%b/%Y:%H:%M:%S')] \"GET / HTTP/1.1\" 200 1234"
        "192.168.1.100 - - [$(date '+%d/%b/%Y:%H:%M:%S')] \"GET / HTTP/1.1\" 200 1234"
        "192.168.1.100 - - [$(date '+%d/%b/%Y:%H:%M:%S')] \"GET / HTTP/1.1\" 200 1234"
        "192.168.1.100 - - [$(date '+%d/%b/%Y:%H:%M:%S')] \"GET / HTTP/1.1\" 200 1234"
        "192.168.1.100 - - [$(date '+%d/%b/%Y:%H:%M:%S')] \"GET / HTTP/1.1\" 200 1234"
        "192.168.1.100 - - [$(date '+%d/%b/%Y:%H:%M:%S')] \"GET / HTTP/1.1\" 200 1234"
        "192.168.1.100 - - [$(date '+%d/%b/%Y:%H:%M:%S')] \"GET / HTTP/1.1\" 200 1234"
        "192.168.1.100 - - [$(date '+%d/%b/%Y:%H:%M:%S')] \"GET / HTTP/1.1\" 200 1234"
        "192.168.1.100 - - [$(date '+%d/%b/%Y:%H:%M:%S')] \"GET / HTTP/1.1\" 200 1234"
        "192.168.1.100 - - [$(date '+%d/%b/%Y:%H:%M:%S')] \"GET / HTTP/1.1\" 200 1234"
        "192.168.1.100 - - [$(date '+%d/%b/%Y:%H:%M:%S')] \"GET / HTTP/1.1\" 200 1234"
    )
    
    # 将测试数据写入日志文件
    for entry in "${test_log_entries[@]}"; do
        echo "$entry" >> "$LOG_FILE"
        sleep 0.1
    done
    
    echo "测试完成,请检查是否封锁了IP 192.168.1.100"
}

# 检查命令行参数
if [ "$1" = "--test" ]; then
    test_mode
    exit 0
elif [ "$1" = "--status" ]; then
    echo "当前封锁状态:"
    if [ -f "$BLOCKED_IPS_FILE" ]; then
        local blocked_count=$(wc -l < "$BLOCKED_IPS_FILE" 2>/dev/null || echo 0)
        echo "已封锁IP数量: $blocked_count"
        if [ "$blocked_count" -gt 0 ]; then
            echo "封锁的IP列表:"
            cat "$BLOCKED_IPS_FILE"
        fi
    else
        echo "暂无封锁的IP"
    fi
    exit 0
elif [ "$1" = "--debug" ]; then
    DEBUG_MODE=true
    echo "调试模式已启用"
elif [ "$1" = "--quiet" ]; then
    QUIET_MODE=true
    echo "静默模式已启用(只显示新增封锁)"
elif [ "$1" = "--minimal" ]; then
    QUIET_MODE=true
    DEBUG_MODE=false
    echo "最小输出模式已启用(只显示新增封锁)"
elif [ "$1" = "--check-iptables" ]; then
    echo "检查iptables配置..."
    check_iptables_config
    echo ""
    echo "当前封锁的IP列表:"
    if [ -f "$BLOCKED_IPS_FILE" ]; then
        cat "$BLOCKED_IPS_FILE"
    else
        echo "暂无封锁的IP"
    fi
    exit 0
elif [ "$1" = "--attack-history" ]; then
    echo "攻击历史记录:"
    if [ -f "$ATTACK_HISTORY_FILE" ]; then
        echo "IP地址 | 攻击类型 | 时间戳"
        echo "------------------------"
        while IFS='|' read -r ip type timestamp; do
            time_str=$(date -d "@$timestamp" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo "未知时间")
            echo "$ip | $type | $time_str"
        done < "$ATTACK_HISTORY_FILE"
    else
        echo "暂无攻击历史记录"
    fi
    exit 0
fi

# 启动清理进程
cleanup_expired_blocks &
CLEANUP_PID=$!

# 启动监控
monitor_log_file

# 清理进程
kill $CLEANUP_PID 2>/dev/null

使用说明:

### 基本使用
```bash
sudo ./realtime_block.sh
```

### 调试模式
```bash
sudo ./realtime_block.sh --debug
```

### 静默模式(只显示新增封锁)
```bash
sudo ./realtime_block.sh --quiet
```

### 最小输出模式(最简洁的输出)
```bash
sudo ./realtime_block.sh --minimal
```

### 查看当前状态
```bash
sudo ./realtime_block.sh --status
```

### 测试模式
```bash
sudo ./realtime_block.sh --test
```

## 配置参数

在脚本开头可以修改以下参数:

```bash
LOG_FILE="/home/wwwlogs/h4ck.org.cn.log"  # 日志文件路径
BLOCKED_IPS_FILE="/tmp/blocked_ips.txt"    # 封锁IP记录文件
MAX_REQUESTS_PER_MINUTE=10                 # 每分钟最大请求数
BLOCK_DURATION=3600                        # 封锁时间(秒)
DEBUG_MODE=false                           # 调试模式
QUIET_MODE=false                           # 静默模式
```

最终效果:

这,文正写完了,雨也停了。

_cuva

The post 偶然?还是必然? — 谁是罪魁祸首 appeared first on obaby@mars.

❌