CC攻击的可怕之处不在于流量有多大,而在于它极其像正常访问。当服务器CPU飙升、数据库连接池耗尽时,运维人员登录服务器,看到的往往是一堆看似正常的GET或POST请求,来源IP分散,请求路径合法,甚至User-Agent都模拟了主流浏览器。如果没有精准的日志特征提取,你根本无法从海量请求中把攻击流量剥离出来。

真正有效的防御不是靠猜,而是从Web访问日志里把攻击流量的“指纹”找出来。这些指纹通常不是单一维度,而是多个维度的组合。一个典型的CC攻击请求在日志中往往会暴露以下特征:请求集中在少数几个URL,且这些URL往往是动态页面或搜索接口;请求频率远高于正常用户,单IP在短时间内达到数百甚至数千次;请求间隔极其规律,像心跳包一样精准;HTTP状态码大量集中在200或302,因为攻击者通常直接命中缓存穿透后的动态页面;User-Agent虽然模拟了浏览器,但往往所有攻击节点使用完全相同的UA字符串;Referer字段要么为空,要么固定指向被攻击的域名本身。

用awk一行命令快速发现高频攻击IP

当你拿到一份访问日志,第一步永远是统计。不要一上来就写复杂的脚本,先用awk把最直观的异常抓出来。假设日志格式为标准Nginx combined格式,下面这条命令能瞬间列出访问最频繁的前20个IP:

awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -20

这条命令的执行结果如果显示某个IP的请求数达到几千甚至上万,而其他正常IP只有几十或几百,那这个IP基本就是攻击源。但这里有个坑:如果攻击者使用了大规模代理池,每个IP的请求量可能并不突出,单靠IP频率会漏掉大量攻击流量。这时候就需要从URL维度切入。

从URL维度锁定攻击目标

CC攻击一定会集中火力打某一个或某几个特定URL。攻击者不会漫无目的地扫描,他们的目标是消耗服务器资源,所以会选择那些执行时间最长、数据库查询最复杂的动态页面。统计URL访问频率的命令如下:

awk '{print $7}' access.log | sort | uniq -c | sort -rn | head -20

执行后你可能会发现,某个搜索接口/api/search或者某个商品详情页/product/detail被访问了数万次,而其他页面只有几百次。这种明显的二八分布就是攻击特征。更狡猾的攻击者会构造随机参数,比如/product/detail?id=12345,每个请求的URL都不完全一样,但路径前缀相同。这时候需要先把查询参数剥离,只统计路径部分:

awk '{print $7}' access.log | awk -F'?' '{print $1}' | sort | uniq -c | sort -rn | head -20

这样即使攻击者在参数上做文章,也逃不出路径维度的统计。一旦锁定了被攻击的URL,后续的封禁策略就可以精准打击。

请求间隔分析揪出机器行为

人类访问网站是有节奏的,点击、阅读、跳转,请求间隔呈随机分布。而CC攻击脚本往往是循环发包,请求间隔高度一致。通过分析日志中相邻请求的时间戳差值,可以轻易识别机器行为。下面这段awk脚本会计算每个IP相邻两次请求的时间间隔,并统计间隔分布:

awk '{print $1, $4}' access.log | sort -k1,1 -k2,2 | awk '{
    if($1==last_ip) {
        split($2, a, ":");
        split(last_time, b, ":");
        diff = (a[1]*3600+a[2]*60+a[3]) - (b[1]*3600+b[2]*60+b[3]);
        if(diff >= 0) print $1, diff
    }
    last_ip=$1; last_time=$2
}' | awk '{sum[$1]+=$2; count[$1]++} END {for(ip in sum) print ip, sum[ip]/count[ip]}' | sort -k2 -n

输出结果中,如果某个IP的平均请求间隔是1秒或0.5秒,且方差极小,那几乎可以肯定是脚本在跑。正常用户的平均间隔可能在10秒到几分钟不等,而且波动很大。这个特征用来识别慢速CC攻击尤其有效,因为慢速攻击的IP频率不高,但间隔极其规律,单靠频率统计根本发现不了。

状态码和Body字节数的异常模式

CC攻击的另一个隐蔽特征是返回状态码和响应体大小的分布异常。正常访问中,200、304、301、404等状态码会有一个自然的分布比例。但CC攻击如果命中了动态页面,状态码会高度集中在200,而且每次返回的Body大小几乎完全一致。用下面这条命令可以统计每个URL的返回体大小分布:

awk '{print $7, $9, $10}' access.log | sort | uniq -c | sort -rn | head -30

如果某个URL的$10字段(响应体字节数)在所有请求中都完全相同,或者只有极小的波动,说明攻击者在反复请求同一个资源,服务器每次返回的内容都一样。这个特征结合URL维度,可以形成非常精准的攻击指纹。

组合特征提取与自动化封禁脚本

单维度分析只能发现一部分攻击,真正的实战是把多个维度组合起来。一个成熟的自动化封禁脚本应该做到:从日志中实时提取高频IP、高频URL、异常间隔、状态码集中度这四个维度的数据,然后根据阈值触发封禁动作。下面是一个可以直接部署在生产环境的基础脚本框架:

#!/bin/bash
LOG_FILE="/var/log/nginx/access.log"
BLOCK_LIST="/etc/nginx/block_ips.conf"
THRESHOLD=200
TIME_WINDOW=60

tail -n 5000 $LOG_FILE | awk -v threshold=$THRESHOLD '{
    ip=$1; url=$7; status=$9;
    ip_count[ip]++;
    url_count[url]++;
    if(status==200) status200[ip]++;
    total[ip]++;
}
END {
    for(ip in ip_count) {
        if(ip_count[ip] > threshold) {
            print ip, "high_freq", ip_count[ip];
        }
        if(total[ip] > 50 && status200[ip]/total[ip] > 0.95) {
            print ip, "abnormal_status_ratio", status200[ip]/total[ip];
        }
    }
}' | while read ip reason value; do
    if ! grep -q "$ip" $BLOCK_LIST 2>/dev/null; then
        echo "deny $ip; # $reason: $value" >> $BLOCK_LIST
        echo "$(date) Blocked $ip due to $reason: $value" >> /var/log/block.log
    fi
done

if [ -s $BLOCK_LIST ]; then
    nginx -s reload
fi

这个脚本每次读取日志尾部5000行,统计每个IP的请求次数和200状态码比例。如果某个IP在窗口内请求超过200次,或者200状态码占比超过95%且请求数超过50次,就自动加入Nginx的封禁列表并重载配置。脚本可以配合crontab每分钟执行一次,实现准实时封禁。

进阶:用滑动窗口和衰减算法避免误封

上面的脚本有个明显缺陷:阈值是硬编码的,而且只看绝对数量。如果某个IP是公司出口IP,背后有几百个正常用户在访问,那它的请求量也会很高。更合理的做法是引入滑动窗口和指数衰减,给最近的请求更高权重,历史请求权重随时间衰减。这样突发流量会被识别,而持续稳定的高流量不会被误封。

下面这段Python脚本实现了基于Redis的滑动窗口计数,每个IP的请求记录带时间戳,窗口外的记录自动过期:

import redis
import time

r = redis.Redis(host='localhost', port=6379, db=0)
WINDOW = 60
THRESHOLD = 150

def check_and_block(ip):
    now = time.time()
    key = f"rate:{ip}"
    r.zadd(key, {now: now})
    r.zremrangebyscore(key, 0, now - WINDOW)
    count = r.zcard(key)
    r.expire(key, WINDOW + 10)
    
    if count > THRESHOLD:
        return True
    return False

with open('/var/log/nginx/access.log', 'r') as f:
    for line in f:
        ip = line.split()[0]
        if check_and_block(ip):
            print(f"Block {ip}")

这个方案的精妙之处在于,每个IP在Redis中维护一个有序集合,score是请求时间戳。每次检查时自动清理60秒窗口之外的记录,然后统计窗口内的请求数。如果一个IP在60秒内请求超过150次,就触发封禁。由于窗口是滑动的,攻击停止后封禁标记可以自动过期,不需要手动解封。

URL特征正则提取与精准打击

很多时候攻击者会不断变换IP,但攻击的URL模式是固定的。比如所有攻击请求都包含/api/search?keyword=xxx,其中keyword参数值是随机生成的。这时候最有效的策略不是封IP,而是封URL模式。从日志中提取出攻击URL的正则特征,然后在Nginx层面直接返回403或444。

先用awk统计出被攻击的URL前缀:

awk '{print $7}' access.log | awk -F'?' '{print $1}' | sort | uniq -c | sort -rn | head -10

假设发现/api/search被集中攻击,进一步分析参数特征:

grep "/api/search" access.log | awk -F'?' '{print $2}' | awk -F'&' '{for(i=1;i<=NF;i++) print $i}' | awk -F'=' '{print $1}' | sort | uniq -c | sort -rn

如果发现keyword参数出现在90%以上的攻击请求中,而且参数值的长度或字符分布有明显规律,就可以在Nginx中配置精准拦截:

location /api/search {
    if ($arg_keyword ~* "^[a-z0-9]{32}$") {
        return 444;
    }
    proxy_pass http://backend;
}

这条规则的意思是,如果keyword参数的值恰好是32位小写字母和数字的组合,直接断开连接。因为正常用户的搜索词不会是这个格式,这明显是攻击脚本生成的随机字符串。这种基于URL特征的封禁比封IP有效得多,因为攻击者换IP也没用,除非他们连攻击脚本的参数生成逻辑也改掉。

User-Agent和Referer的异常聚类

攻击者为了伪装,通常会设置一个看起来正常的User-Agent,比如某个版本的Chrome浏览器。但他们往往犯一个错误:所有攻击节点使用完全相同的UA字符串。正常访问中,即使是同一个浏览器的用户,UA字符串也会因为操作系统版本、补丁级别、语言设置的不同而有细微差异。如果日志中出现大量完全一致的UA字符串,而且这些请求都集中在某个URL上,那就是攻击特征。

统计UA分布的命令:

awk -F'"' '{print $6}' access.log | sort | uniq -c | sort -rn | head -20

如果某个UA出现了数千次,而其他UA只有几十次,这个UA就值得怀疑。结合Referer字段一起看:

awk -F'"' '{print $6, $8}' access.log | sort | uniq -c | sort -rn | head -20

如果攻击请求的Referer为空或者固定为被攻击域名本身,而正常请求的Referer来自搜索引擎、外部链接等多样化来源,这个差异就是自动化封禁的绝佳特征。在脚本中加入UA和Referer的异常检测,可以大幅降低误封率。

将分析结果固化为自动化防御体系

单次的手动分析只能解决当下的攻击,真正成熟的运维体系应该把日志特征提取和自动封禁做成一个持续运行的闭环系统。这个系统的核心流程是:日志实时采集进入分析管道,多维特征提取引擎对IP频率、URL集中度、请求间隔、状态码分布、UA聚类、Referer异常进行并行计算,每个维度产生一个风险评分,最终加权汇总。当某个IP或某个URL模式的总风险分超过阈值时,自动生成封禁规则并下发到网关层。

这个系统不需要多复杂,用Filebeat采集日志送入Logstash或自研的Go程序做流式处理,结果写入Redis,封禁动作通过API调用Nginx或iptables即可。关键在于特征提取的维度要全,阈值要根据业务实际情况动态调整,而不是拍脑袋定一个数字。比如电商网站的商品详情页,正常QPS可能就很高,阈值要设高一些;而后台管理系统的登录接口,正常QPS很低,阈值就要设低一些。

日志里藏着攻击者的一切痕迹,就看你能不能从噪音中把信号分离出来。CC攻击的防御本质上是一场特征工程竞赛,攻击者不断变换手法,防御方不断提取新的特征。掌握了从日志中提取多维特征并自动响应的能力,就掌握了这场竞赛的主动权。