阅读视图
基于 wp-cron.php 的拒绝服务攻击
这几天不知道是发生什么事了,说是不知道什么事情,但是大概率是被打了。只是这次打的挺高级的,外层的 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 资源耗尽了,这的确不失为一个低成本的攻击方式。
爬虫占比:
这几天也不知道爬虫是发什么疯
今天的访问量:
百度的统计:
咱就是说,有点时间干点正事不好吗?真是闲的。
再谈WP更新逻辑
最近发现一个问题就是,wp 的后台打开速度越来越慢了,不过用屁股想想也能猜到肯定是 wp 后台的各种更新检查导致的。
网上通用的办法是直接禁用掉各种更新检查,但是鉴于安全问题其实我并不想直接这么干。现在各种漏洞扫描利用太频繁了,新版本能一定程度上降低被利用的风险。
wp 的更新的代码,本质上是有缓存机制的:
function _maybe_update_core() {
$current = get_site_transient( 'update_core' );
if ( isset( $current->last_checked, $current->version_checked )
&& 12 * HOUR_IN_SECONDS > ( time() - $current->last_checked )
&& wp_get_wp_version() === $current->version_checked
) {
return;
}
wp_version_check();
}
检查更新的时候会判断是否在 12 小时内已经执行过,然而,这个破玩儿的问题在于,写入transient的时候不知道为什么失败了,于是每次进入后台都看到一堆 http 请求。
这 tm 就贼啦智障了,多的时候七八个请求,一个请求一秒钟,全部请求完都十几秒了。
所以,我直接基于 redis 做了个缓存机制,避免重复请求。在 24 小时内请求过就不会再次请求了。直接对请求进行拦截。
既避免了无法检查更新,也解决了每次都检查更新的问题。
代码如下:
<?php
/**
* 使用 Redis 缓存控制所有 WordPress 更新检查
*
* 控制以下更新检查的频率:
* 1. wp_version_check() - WordPress 核心版本检查
* 2. wp_update_themes() - 主题更新检查
* 3. wp_update_plugins() - 插件更新检查
* 4. wp_check_browser_version() - 浏览器版本检查
* 5. wp_check_php_version() - PHP 版本检查
*
* 不依赖 WordPress 的 transient,直接使用 Redis 缓存
* 如果 24 小时内更新过,跳过更新检查
*
* 使用方法:
* 将此代码添加到主题的 functions.php 文件末尾
* 或添加到插件的初始化函数中
* https://h4ck.org.cn/wp-admin/index.php?debug_all_checks=1#qm-overview
*/
// ==========================================
// 配置项
// ==========================================
// 缓存键名
define('WP_CORE_UPDATE_CHECK_KEY', 'wp_core_update_check_time');
define('WP_THEMES_UPDATE_CHECK_KEY', 'wp_themes_update_check_time');
define('WP_PLUGINS_UPDATE_CHECK_KEY', 'wp_plugins_update_check_time');
define('WP_BROWSER_VERSION_CHECK_KEY', 'wp_browser_version_check_time');
define('WP_PHP_VERSION_CHECK_KEY', 'wp_php_version_check_time');
// 缓存有效期(24 小时)
define('WP_UPDATE_CHECK_EXPIRATION', DAY_IN_SECONDS);
// ==========================================
// 1. WordPress 核心版本检查 (wp_version_check)
// ==========================================
/**
* 拦截 _maybe_update_core 函数
* 在 admin_init 优先级 0 中移除 _maybe_update_core,使用 Redis 缓存控制
*/
add_action('admin_init', function() {
// 移除 WordPress 原来的 _maybe_update_core 钩子
remove_action('admin_init', '_maybe_update_core', 1);
// 检查 Redis 缓存,看是否在 24 小时内更新过
$last_check_time = wp_cache_get(WP_CORE_UPDATE_CHECK_KEY);
if ($last_check_time !== false && is_numeric($last_check_time)) {
$time_since_check = time() - $last_check_time;
// 如果在 24 小时内,直接 return,跳过更新检查
if ($time_since_check < WP_UPDATE_CHECK_EXPIRATION) {
error_log('[Redis Core Check] 跳过核心版本检查 - 距离上次检查: ' . human_time_diff($last_check_time, time()));
return; // 直接返回,不执行更新检查
}
}
// 如果缓存过期或不存在,执行原来的更新检查逻辑
_maybe_update_core();
}, 0); // 优先级 0,在 _maybe_update_core (优先级 1) 之前执行
/**
* 监控 wp_version_check 的执行,设置 Redis 缓存
* 注意:只有在缓存不存在时才设置,避免频繁刷新时重置缓存时间
*/
add_action('wp_version_check', function() {
$existing_cache = wp_cache_get(WP_CORE_UPDATE_CHECK_KEY);
if ($existing_cache === false) {
wp_cache_set(
WP_CORE_UPDATE_CHECK_KEY,
time(),
'',
WP_UPDATE_CHECK_EXPIRATION
);
error_log('[Redis Core Check] wp_version_check 执行,已设置 Redis 缓存');
}
}, 10);
/**
* 通过过滤器拦截 update_core transient 设置,设置 Redis 缓存
* 注意:只有在缓存不存在时才设置,避免频繁刷新时重置缓存时间
*/
add_filter('pre_set_site_transient_update_core', function($value, $transient_name) {
$existing_cache = wp_cache_get(WP_CORE_UPDATE_CHECK_KEY);
if ($existing_cache === false) {
wp_cache_set(
WP_CORE_UPDATE_CHECK_KEY,
time(),
'',
WP_UPDATE_CHECK_EXPIRATION
);
}
return $value;
}, 999, 2);
// ==========================================
// 2. 主题更新检查 (wp_update_themes)
// ==========================================
/**
* 拦截 _maybe_update_themes 函数
* 在 admin_init 优先级 0 中移除 _maybe_update_themes,使用 Redis 缓存控制
*/
add_action('admin_init', function() {
// 移除 WordPress 原来的 _maybe_update_themes 钩子
remove_action('admin_init', '_maybe_update_themes', 1);
// 检查 Redis 缓存,看是否在 24 小时内更新过
$last_check_time = wp_cache_get(WP_THEMES_UPDATE_CHECK_KEY);
if ($last_check_time !== false && is_numeric($last_check_time)) {
$time_since_check = time() - $last_check_time;
// 如果在 24 小时内,直接 return,跳过更新检查
if ($time_since_check < WP_UPDATE_CHECK_EXPIRATION) {
error_log('[Redis Themes Check] 跳过主题更新检查 - 距离上次检查: ' . human_time_diff($last_check_time, time()));
return; // 直接返回,不执行更新检查
}
}
// 如果缓存过期或不存在,执行原来的更新检查逻辑
_maybe_update_themes();
}, 0); // 优先级 0,在 _maybe_update_themes (优先级 1) 之前执行
/**
* 监控 wp_update_themes 的执行,设置 Redis 缓存
* 注意:只有在缓存不存在时才设置,避免频繁刷新时重置缓存时间
*/
add_action('wp_update_themes', function() {
$existing_cache = wp_cache_get(WP_THEMES_UPDATE_CHECK_KEY);
if ($existing_cache === false) {
wp_cache_set(
WP_THEMES_UPDATE_CHECK_KEY,
time(),
'',
WP_UPDATE_CHECK_EXPIRATION
);
error_log('[Redis Themes Check] wp_update_themes 执行,已设置 Redis 缓存');
}
}, 10);
/**
* 通过过滤器拦截 update_themes transient 设置,设置 Redis 缓存
* 注意:只有在缓存不存在时才设置,避免频繁刷新时重置缓存时间
*/
add_filter('pre_set_site_transient_update_themes', function($value, $transient_name) {
$existing_cache = wp_cache_get(WP_THEMES_UPDATE_CHECK_KEY);
if ($existing_cache === false) {
wp_cache_set(
WP_THEMES_UPDATE_CHECK_KEY,
time(),
'',
WP_UPDATE_CHECK_EXPIRATION
);
}
return $value;
}, 999, 2);
// ==========================================
// 3. 插件更新检查 (wp_update_plugins)
// ==========================================
/**
* 方法1:在 HTTP 请求级别拦截插件更新 API
* 直接拦截到 api.wordpress.org/plugins/update-check/ 的请求
*/
add_filter('pre_http_request', function($preempt, $args, $url) {
// 检查是否是插件更新检查的 API 请求
if (strpos($url, 'api.wordpress.org/plugins/update-check/') !== false) {
// 检查 Redis 缓存,看是否在 24 小时内更新过
$last_check_time = wp_cache_get(WP_PLUGINS_UPDATE_CHECK_KEY);
if ($last_check_time !== false && is_numeric($last_check_time)) {
$time_since_check = time() - $last_check_time;
// 如果在 24 小时内,返回假数据,跳过 API 请求
if ($time_since_check < WP_UPDATE_CHECK_EXPIRATION) {
error_log('[Redis Plugins Check] 跳过插件更新检查 API 请求 - 距离上次检查: ' . human_time_diff($last_check_time, time()));
// 返回一个符合 WordPress API 格式的响应
// WordPress 期望的完整格式:
// - plugins: 需要更新的插件数组
// - no_update: 不需要更新的插件数组(必需!)
// - translations: 翻译更新数组
$fake_response_data = array(
'plugins' => array(), // 需要更新的插件(空数组表示没有更新)
'no_update' => array(), // 不需要更新的插件(必需字段)
'translations' => array() // 翻译更新(空数组表示没有更新)
);
// 编码为 JSON,确保是数组格式
$fake_response = json_encode($fake_response_data, JSON_UNESCAPED_SLASHES);
// 验证 JSON 编码是否成功
if ($fake_response === false || json_last_error() !== JSON_ERROR_NONE) {
error_log('[Redis Plugins Check] JSON 编码失败: ' . json_last_error_msg());
// 如果编码失败,返回一个包含所有必需字段的简单有效 JSON
$fake_response = '{"plugins":[],"no_update":[],"translations":[]}';
}
return array(
'body' => $fake_response,
'response' => array('code' => 200, 'message' => 'OK'),
'headers' => array('Content-Type' => 'application/json; charset=UTF-8'),
);
}
}
}
return $preempt;
}, 5, 3); // 提高优先级,确保在其他过滤器之前执行
/**
* 方法2:拦截 _maybe_update_plugins 函数
* 在 admin_init 优先级 0 中移除 _maybe_update_plugins,使用 Redis 缓存控制
*/
add_action('admin_init', function() {
// 移除 WordPress 原来的 _maybe_update_plugins 钩子
remove_action('admin_init', '_maybe_update_plugins', 1);
// 检查 Redis 缓存,看是否在 24 小时内更新过
$last_check_time = wp_cache_get(WP_PLUGINS_UPDATE_CHECK_KEY);
if ($last_check_time !== false && is_numeric($last_check_time)) {
$time_since_check = time() - $last_check_time;
// 如果在 24 小时内,直接 return,跳过更新检查
if ($time_since_check < WP_UPDATE_CHECK_EXPIRATION) {
error_log('[Redis Plugins Check] 跳过插件更新检查 - 距离上次检查: ' . human_time_diff($last_check_time, time()));
return; // 直接返回,不执行更新检查
}
}
// 如果缓存过期或不存在,执行原来的更新检查逻辑
_maybe_update_plugins();
}, 0); // 优先级 0,在 _maybe_update_plugins (优先级 1) 之前执行
/**
* 在 HTTP 响应时设置 Redis 缓存(插件检查)
* 只有在实际执行了 API 请求并成功获取响应后,才设置缓存
* 同时确保响应格式正确
*/
add_filter('http_response', function($response, $args, $url) {
// 只有在缓存不存在或已过期时,才设置缓存
// 这样可以避免频繁刷新时不断重置缓存时间
$existing_cache = wp_cache_get(WP_PLUGINS_UPDATE_CHECK_KEY);
if ($existing_cache === false) {
// 缓存不存在,说明这是真正的更新检查,设置缓存
wp_cache_set(
WP_PLUGINS_UPDATE_CHECK_KEY,
time(),
'',
WP_UPDATE_CHECK_EXPIRATION
);
error_log('[Redis Plugins Check] 插件更新检查 API 请求完成,已设置 Redis 缓存');
} else {
error_log('[Redis Plugins Check] 缓存已存在,不更新缓存时间');
}
// 检查是否是插件更新检查的 API 请求
if (strpos($url, 'api.wordpress.org/plugins/update-check/') !== false) {
if (!is_wp_error($response)) {
// 确保响应体格式正确
$body = wp_remote_retrieve_body($response);
if (!empty($body)) {
$decoded = json_decode($body, true);
// 如果解析失败或格式不正确,修复它
if (!is_array($decoded)) {
error_log('[Redis Plugins Check] JSON 解析失败,创建默认响应');
$decoded = array();
}
// 确保所有必需字段存在且是正确的类型
if (!isset($decoded['plugins']) || !is_array($decoded['plugins'])) {
$decoded['plugins'] = array();
}
if (!isset($decoded['no_update']) || !is_array($decoded['no_update'])) {
$decoded['no_update'] = array();
}
if (!isset($decoded['translations']) || !is_array($decoded['translations'])) {
$decoded['translations'] = array();
}
// 重新编码响应体(只有在需要修复时才重新编码)
$response['body'] = json_encode($decoded, JSON_UNESCAPED_SLASHES);
}
}
}
return $response;
}, 10, 3);
/**
* 监控 wp_update_plugins 的执行,设置 Redis 缓存
* 注意:只有在缓存不存在时才设置,避免频繁刷新时重置缓存时间
*/
add_action('wp_update_plugins', function() {
// 只有在缓存不存在时,才设置缓存
// 这样可以避免在缓存有效期内不断重置缓存时间
$existing_cache = wp_cache_get(WP_PLUGINS_UPDATE_CHECK_KEY);
if ($existing_cache === false) {
wp_cache_set(
WP_PLUGINS_UPDATE_CHECK_KEY,
time(),
'',
WP_UPDATE_CHECK_EXPIRATION
);
error_log('[Redis Plugins Check] wp_update_plugins 执行,已设置 Redis 缓存');
}
}, 10);
/**
* 通过过滤器拦截 update_plugins transient 设置,设置 Redis 缓存
* 同时确保 transient 数据格式正确
* 注意:只有在缓存不存在时才设置,避免频繁刷新时重置缓存时间
*/
add_filter('pre_set_site_transient_update_plugins', function($value, $transient_name) {
// 确保格式正确
if ($value && is_object($value)) {
// 确保 response 是数组
if (isset($value->response) && !is_array($value->response)) {
error_log('[Redis Plugins Check] 修复 response 格式,从 ' . gettype($value->response) . ' 转换为数组');
$value->response = is_array($value->response) ? $value->response : array();
}
// 确保 translations 是数组
if (isset($value->translations) && !is_array($value->translations)) {
$value->translations = is_array($value->translations) ? $value->translations : array();
}
// 确保 checked 是数组
if (isset($value->checked) && !is_array($value->checked)) {
$value->checked = is_array($value->checked) ? $value->checked : array();
}
}
// 只有在缓存不存在时,才设置 Redis 缓存
// 这样可以避免在缓存有效期内不断重置缓存时间
$existing_cache = wp_cache_get(WP_PLUGINS_UPDATE_CHECK_KEY);
if ($existing_cache === false) {
wp_cache_set(
WP_PLUGINS_UPDATE_CHECK_KEY,
time(),
'',
WP_UPDATE_CHECK_EXPIRATION
);
error_log('[Redis Plugins Check] transient 设置,已设置 Redis 缓存');
}
return $value;
}, 999, 2);
// ==========================================
// 4. 浏览器版本检查 (wp_check_browser_version)
// ==========================================
/**
* 拦截 wp_check_browser_version 函数
* 这个函数通过 HTTP 请求到 api.wordpress.org/core/browse-happy/
* 我们使用过滤器拦截 HTTP 请求
*/
add_filter('pre_http_request', function($preempt, $args, $url) {
// 检查是否是浏览器版本检查的 API 请求
if (strpos($url, 'api.wordpress.org/core/browse-happy/') !== false) {
// 检查 Redis 缓存,看是否在 24 小时内更新过
$last_check_time = wp_cache_get(WP_BROWSER_VERSION_CHECK_KEY);
if ($last_check_time !== false && is_numeric($last_check_time)) {
$time_since_check = time() - $last_check_time;
// 如果在 24 小时内,返回假数据,跳过 API 请求
if ($time_since_check < WP_UPDATE_CHECK_EXPIRATION) {
error_log('[Redis Browser Check] 跳过浏览器版本检查 - 距离上次检查: ' . human_time_diff($last_check_time, time()));
// 返回一个成功的响应(避免重复请求)
return array(
'body' => '{"success":true}',
'response' => array('code' => 200, 'message' => 'OK'),
'headers' => array(),
);
}
}
}
return $preempt;
}, 10, 3);
/**
* 在 HTTP 响应时设置 Redis 缓存(浏览器检查)
* 注意:只有在缓存不存在时才设置,避免频繁刷新时重置缓存时间
*/
add_filter('http_response', function($response, $args, $url) {
// 检查是否是浏览器版本检查的 API 请求
if (strpos($url, 'api.wordpress.org/core/browse-happy/') !== false && !is_wp_error($response)) {
$existing_cache = wp_cache_get(WP_BROWSER_VERSION_CHECK_KEY);
if ($existing_cache === false) {
wp_cache_set(
WP_BROWSER_VERSION_CHECK_KEY,
time(),
'',
WP_UPDATE_CHECK_EXPIRATION
);
error_log('[Redis Browser Check] wp_check_browser_version 执行,已设置 Redis 缓存');
}
}
return $response;
}, 10, 3);
/**
* 监控 wp_check_browser_version 的执行,设置 Redis 缓存
* 作为备用方案,通过动作钩子捕获
* 注意:只有在缓存不存在时才设置,避免频繁刷新时重置缓存时间
*/
add_action('wp_check_browser_version', function() {
$existing_cache = wp_cache_get(WP_BROWSER_VERSION_CHECK_KEY);
if ($existing_cache === false) {
wp_cache_set(
WP_BROWSER_VERSION_CHECK_KEY,
time(),
'',
WP_UPDATE_CHECK_EXPIRATION
);
error_log('[Redis Browser Check] wp_check_browser_version 执行,已设置 Redis 缓存');
}
}, 10);
// ==========================================
// 5. PHP 版本检查 (wp_check_php_version)
// ==========================================
/**
* 拦截 wp_check_php_version 函数
* 这个函数通过 HTTP 请求到 api.wordpress.org/core/serve-happy/
* 我们使用过滤器拦截 HTTP 请求
*/
add_filter('pre_http_request', function($preempt, $args, $url) {
// 检查是否是 PHP 版本检查的 API 请求
if (strpos($url, 'api.wordpress.org/core/serve-happy/') !== false) {
// 检查 Redis 缓存,看是否在 24 小时内更新过
$last_check_time = wp_cache_get(WP_PHP_VERSION_CHECK_KEY);
if ($last_check_time !== false && is_numeric($last_check_time)) {
$time_since_check = time() - $last_check_time;
// 如果在 24 小时内,返回假数据,跳过 API 请求
if ($time_since_check < WP_UPDATE_CHECK_EXPIRATION) {
error_log('[Redis PHP Check] 跳过 PHP 版本检查 - 距离上次检查: ' . human_time_diff($last_check_time, time()));
// 返回一个成功的响应(避免重复请求)
return array(
'body' => '{"success":true}',
'response' => array('code' => 200, 'message' => 'OK'),
'headers' => array(),
);
}
}
}
return $preempt;
}, 10, 3);
/**
* 在 HTTP 响应时设置 Redis 缓存(PHP 检查)
* 注意:只有在缓存不存在时才设置,避免频繁刷新时重置缓存时间
*/
add_filter('http_response', function($response, $args, $url) {
// 检查是否是 PHP 版本检查的 API 请求
if (strpos($url, 'api.wordpress.org/core/serve-happy/') !== false && !is_wp_error($response)) {
$existing_cache = wp_cache_get(WP_PHP_VERSION_CHECK_KEY);
if ($existing_cache === false) {
wp_cache_set(
WP_PHP_VERSION_CHECK_KEY,
time(),
'',
WP_UPDATE_CHECK_EXPIRATION
);
error_log('[Redis PHP Check] wp_check_php_version 执行,已设置 Redis 缓存');
}
}
return $response;
}, 10, 3);
/**
* 监控 wp_check_php_version 的执行,设置 Redis 缓存
* 作为备用方案,通过动作钩子捕获
* 注意:只有在缓存不存在时才设置,避免频繁刷新时重置缓存时间
*/
add_action('wp_check_php_version', function() {
$existing_cache = wp_cache_get(WP_PHP_VERSION_CHECK_KEY);
if ($existing_cache === false) {
wp_cache_set(
WP_PHP_VERSION_CHECK_KEY,
time(),
'',
WP_UPDATE_CHECK_EXPIRATION
);
error_log('[Redis PHP Check] wp_check_php_version 执行,已设置 Redis 缓存');
}
}, 10);
// ==========================================
// 兜底方案:在 shutdown 时检查并设置缓存
// ==========================================
/**
* 在页面加载结束时检查
* 如果更新检查执行了但没有设置缓存,在这里设置
*/
add_action('shutdown', function() {
// 检查核心更新
$core_transient = get_site_transient('update_core');
if ($core_transient && is_object($core_transient)) {
$last_check = wp_cache_get(WP_CORE_UPDATE_CHECK_KEY);
if ($last_check === false && isset($core_transient->last_checked)) {
wp_cache_set(
WP_CORE_UPDATE_CHECK_KEY,
$core_transient->last_checked,
'',
WP_UPDATE_CHECK_EXPIRATION
);
}
}
// 检查主题更新
$themes_transient = get_site_transient('update_themes');
if ($themes_transient && is_object($themes_transient)) {
$last_check = wp_cache_get(WP_THEMES_UPDATE_CHECK_KEY);
if ($last_check === false && isset($themes_transient->last_checked)) {
wp_cache_set(
WP_THEMES_UPDATE_CHECK_KEY,
$themes_transient->last_checked,
'',
WP_UPDATE_CHECK_EXPIRATION
);
}
}
// 检查插件更新
$plugins_transient = get_site_transient('update_plugins');
if ($plugins_transient && is_object($plugins_transient)) {
$last_check = wp_cache_get(WP_PLUGINS_UPDATE_CHECK_KEY);
if ($last_check === false && isset($plugins_transient->last_checked)) {
wp_cache_set(
WP_PLUGINS_UPDATE_CHECK_KEY,
$plugins_transient->last_checked,
'',
WP_UPDATE_CHECK_EXPIRATION
);
}
}
// 浏览器和 PHP 检查可能没有标准的 transient,使用当前时间
// 但这里我们已经在动作钩子中处理了
}, 999);
// ==========================================
// 在后台显示状态(可选,用于调试)
// ==========================================
add_action('admin_notices', function() {
// 只在管理员权限时显示
if (!current_user_can('manage_options')) {
return;
}
// 只在 URL 参数包含 debug_all_checks 时显示
// 或者在后台任何页面都显示(方便调试)
$show_debug = isset($_GET['debug_all_checks']) ||
(defined('WP_DEBUG') && WP_DEBUG && defined('WP_DEBUG_DISPLAY') && WP_DEBUG_DISPLAY);
if (!$show_debug) {
return;
}
// 确保常量已定义
if (!defined('WP_CORE_UPDATE_CHECK_KEY')) {
return;
}
$checks = array(
'WordPress 核心版本检查' => WP_CORE_UPDATE_CHECK_KEY,
'主题更新检查' => WP_THEMES_UPDATE_CHECK_KEY,
'插件更新检查' => WP_PLUGINS_UPDATE_CHECK_KEY,
'浏览器版本检查' => WP_BROWSER_VERSION_CHECK_KEY,
'PHP 版本检查' => WP_PHP_VERSION_CHECK_KEY,
);
echo '<div class="notice notice-info is-dismissible">';
echo '<p><strong>Redis 更新检查状态:</strong></p>';
echo '<ul style="margin-left: 20px;">';
foreach ($checks as $name => $key) {
$last_check = wp_cache_get($key);
if ($last_check !== false && is_numeric($last_check)) {
$time_since = time() - $last_check;
$hours_remaining = (WP_UPDATE_CHECK_EXPIRATION - $time_since) / 3600;
echo '<li><strong>' . esc_html($name) . ':</strong>';
echo '上次检查:' . date('Y-m-d H:i:s', $last_check) . ' | ';
echo '距离现在:' . human_time_diff($last_check, time()) . ' | ';
if ($hours_remaining > 0) {
echo '剩余时间:' . number_format($hours_remaining, 1) . ' 小时 | ';
} else {
echo '已过期 | ';
}
echo '下次检查:' . date('Y-m-d H:i:s', $last_check + WP_UPDATE_CHECK_EXPIRATION);
echo '</li>';
} else {
echo '<li><strong>' . esc_html($name) . ':</strong>未设置缓存,下次访问后台将执行检查。</li>';
}
}
echo '</ul>';
echo '<p><small>提示:访问 <code>?debug_all_checks=1</code> 可查看此状态,或启用 WP_DEBUG 模式</small></p>';
echo '</div>';
}, 10); // 使用优先级 10,确保在 admin_init 之后执行
// ==========================================
// 工具函数:手动清除所有缓存(用于测试)
// ==========================================
/**
* 手动清除所有 Redis 缓存,强制下次检查更新
* 访问:/?clear_all_check_cache=1
*/
add_action('init', function() {
if (isset($_GET['clear_all_check_cache']) && current_user_can('manage_options')) {
wp_cache_delete(WP_CORE_UPDATE_CHECK_KEY);
wp_cache_delete(WP_THEMES_UPDATE_CHECK_KEY);
wp_cache_delete(WP_PLUGINS_UPDATE_CHECK_KEY);
wp_cache_delete(WP_BROWSER_VERSION_CHECK_KEY);
wp_cache_delete(WP_PHP_VERSION_CHECK_KEY);
wp_die('所有 Redis 缓存已清除,下次访问后台将执行更新检查。', '缓存已清除', array('back_link' => true));
}
}, 1);
也可以直接用插件,插件地址:
https://github.com/obaby/baby-wp-update-config-tool
在使用本插件之前,您需要首先配置 WordPress 的 Redis 缓存功能。
本插件依赖于 Redis 缓存系统来存储更新检查的时间戳。如果您的 WordPress 站点未配置 Redis 缓存,插件将无法正常工作。请确保:
- 已安装并配置 Redis 服务器
- 已安装 WordPress Redis 缓存插件(如 Redis Object Cache、WP Redis 等)
- Redis 缓存功能已正常启用并运行
如果 Redis 缓存未配置,插件在初始化时会记录警告信息,但不会阻止插件运行。
WordPress登录界面美化
手搓一个博客文章推送机器人 将你的博客和QQ群连接
iOS26 以及 其他
苹果的 iOS26 貌似出来了很长时间了,包括之前的各种体验版。虽然自己也是一名“合格”的 iOS 开发者,但是对于各种体验版其实没太大的兴趣。直到前段时间上了正式版之后,才更新到 26。不着急升级的另外一个原因是,那个透明玻璃效果确实也没太大的兴趣。
iOS26
不过实际升级之后,感觉体验也还算 ok。长时间使用,目前还没发现什么卡顿的问题,不过升级之后各种 app 的 ui 和操作都变化不小,需要适应一下。
当然,随之而来就是carplay 也有了小组件,可以自己添加小组件了。不过,系统自带的小组件比较少。
可以考虑使用 top widgets 来添加各种小组件。
实际在车上的效果感觉也还可以。不过不知道为什么不能添加大号组件,只能添加小号组件,可能是车机屏幕限制了吧。
添加小组件之后就可以在车机上实现小组件切换了:
同时,这个东西更新之后,现在也能设置图标风格了:
鸿蒙 Next
P70不断的提示系统更新,已经提示了无数次,然而,我并不想更新所谓的 5.0.之前手表坏了,去华为体验店买手表的时候,问了下店员,小哥哥的意思是,他们作为店员没办法必须升级体验,但是作为普通用户,不建议升级。
至于鸿蒙系统的升级,跟 iOS 的升级想法还是有比较大的区别。毕竟,iOS 升级兼容性不会有大问题,但是鸿蒙的升级不一样,很可能升级了之后会出现各种的兼容性问题,并且这个已经听到有人买了新手机去降级系统了。
为了不升级,网上搜了一下所谓的禁止升级的办法。
要关闭华为手机总是提示升级鸿蒙系统5的提示,可以采取以下方法: 通过手机本机设置操作: 打开手机“设置”应用,点击“系统和更新”选项。 在该页面选择“软件更新”选项。 进入后点击右上角四个点图标,选择“本机设置”。 关闭“wlan下自动下载”、“夜间安装”和“协同更新”三个选项,以避免系统自动下载和安装更新。 使用adb工具禁用系统更新(此方法可能影响手机系统,操作前要备份重要数据): 在电脑上下载并安装adb工具包。 打开手机“设置”中的“关于手机”页面,连续点击“版本号”7次开启开发者模式,并打开“开发者选项”中的“usb调试”权限。 用数据线将手机连接到电脑,确保adb工具正确识别手机设备。 打开电脑的命令提示符(cmd),定位到adb工具所在目录。 输入命令adb shell pm disable - user com.huawei.android.hwouc停用系统更新服务。 取消HarmonyOS Next版本报名: 打开“我的华为”app进入首页,点击“升级尝鲜”选项。 在“升级尝鲜”页面选择取消报名。 通过应用管理进行设置: 进入华为手机的“设置”界面,并选择“应用管理”选项。 定位并点击“系统更新”选项。 对流量使用、权限以及通知这三大项目进行逐一设置。 在“通知方式”选项中,将相关选项关闭。 清空“系统更新”的缓存。 在“手机管家”的联网管理部分,找到并取消系统更新应用的联网权限。
然而可以确定的是,上面的方法没什么大用,依然会提示更新,不过解决了偷偷摸摸更新的问题,于是我直接把系统更新服务给停止了:
希望后面别再出现那种烦人的更新提示了,现在已经不在单纯是通知栏提示,开始弹窗提示更新了。这就离谱。
其他
最近有时候自己感觉博客速度也变慢了,itdog 的测试普遍到了 1-3 秒,这就有些奇怪了。前几天 w4j1e 也反馈有点慢:
昨天晚上打开 query monitor分析了一下,发现不知道什么时候那个 yoast seo 插件更新时候,在后台会请求更新接口,导致后台打开速度慢。
至于前台打开速度慢的主要原因竟然是因为微软的统计插件导致的:
禁用之后,一切又回归正常了。
然而初次之外,还有另外一个问题,就是有人先后反馈 rss 报错了,最开始是萧俊介 :
第一次反馈的时候,自己尝试预览了一下 rss,给的检测链接也看了,但是没找出问题出在哪里。
然后陶其的个人博客 也反馈了这个问题:
这件事情就变得有些诡异了,到底是谁修改了 rss。按理说这个东西应该不会出问题。并且后续收到反馈在某篇文章之后就接收不到新的文章了。
所以,最开始是感觉某篇文章有问题,于是改了文章。但是依然报错,看错误也很奇怪:
appeared first on <a href="https://h4ck.org.cn">obaby@mars</a>.
feed 中插入了上面的这些代码,这尼玛就离谱了,去问了下百度的 ai:
注里面提到了需要注意检查数据源,想到这几天更新的那个 seo 插件,难道是因为这个东西,果然进入后台看到了插入的这段内容:
去掉这个 rss 源的修改就好了,这真 tm 是闲的蛋疼,你去修改 rss 干嘛?
这绵绵的秋雨,延续了一个多星期了,每天晚上都下,白天有时候晴,有时候继续下。这一周时间过去,温度也降到 13 度了,早上出门甚至有了冷了。今天白天的雨还在继续,没有停歇的意思。
秋雨绵绵无绝期:
只是从下周开始,晚上加班的时间要继续延长了,到八点半。
Baby WP 评论强化拦截插件 — 再战 WP 垃圾评论
这种生态成熟,或者说受众较大的产品,难免惦记的人就多。之前已经增加了很多方法,来弥补 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 食用。
友情链接检测面板 LinkChecker
偶然?还是必然? — 谁是罪魁祸首
宝子放假,转眼假期已经过了一半,想着找个周末带宝子回老家住几天,大姐家的孩子周日要办升学宴,既然都回来了,不去也不合适,刚好就可以周六回老家,周日去淄博。
对象说,周日青岛会下大暴雨,还是下班之后直接回吧,不然第二天很可能走不了。
就这样,马不停蹄,下班买点东西就直接接上孩子往回走。天气预报零点的雨,在九点多到潍坊的时候就开始逐渐大了。
对象开车的时候,自己打开手机想回复下评论,但是却发现一个问题,那就是页面非常卡。刚开始还以为是网络问题,但是切到短视频却异常流畅。
瞬间有种不祥的预感,那就是服务器的 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 # 静默模式 ```
最终效果:
这,文正写完了,雨也停了。

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















重要提示





































