蓝易云CDN:拦截超强cc攻击?案例详解

蓝易云CDN:拦截超强CC攻击实战案例详解

这是一次真实的CC攻击防御过程还原。某客户网站在业务高峰期遭遇持续性大规模CC攻击,攻击峰值QPS突破12万,持续时间超过6小时。蓝易云CDN节点从发现攻击到完全压制,经历了五个阶段的策略升级,最终在源站零感知的状态下完成了全部拦截 🛡️


攻击背景

受攻击的是一个电商类业务平台,源站部署在阿里云杭州节点,日常QPS约800到1200。攻击发生在晚间20:00左右,客户反馈网站加载缓慢,部分用户出现504超时。

蓝易云CDN节点监控率先捕捉到异常——入口QPS在3分钟内从正常水平暴涨至4万,且持续攀升 📈

第一阶段:流量特征分析(20:03)

运维团队第一时间登录CDN节点,通过实时日志分析攻击流量特征:

# 统计近60秒内请求量前20的IP
awk -v start=$(date -d '1 minute ago' '+%d/%b/%Y:%H:%M') \
    '$4 ~ start {print $1}' /var/log/openresty/access.log | \
    sort | uniq -c | sort -rn | head -20

解释: awk 命令过滤出最近一分钟内的访问日志,提取客户端IP字段($1),通过 sort | uniq -c 统计每个IP的出现次数,sort -rn 按数量倒序排列,取前20个。这条命令能在几秒内定位出请求量最大的IP来源。

结果显示并非少量IP的高频攻击,而是来自超过8000个不同IP地址的分布式CC攻击。继续分析UA和请求路径分布:

# 统计UA分布
awk -F'"' '{print $6}' /var/log/openresty/access.log | \
    sort | uniq -c | sort -rn | head -10

# 统计被请求最多的URI
awk '{print $7}' /var/log/openresty/access.log | \
    sort | uniq -c | sort -rn | head -10

解释: 第一条命令以双引号为分隔符提取日志中的UA字段(标准Nginx日志格式中UA是第6个双引号包裹的字段),统计每种UA的出现频次。第二条命令提取请求URI字段,统计被访问最多的路径。

分析结果暴露了几个关键特征:

  • 约65%的请求集中在 /api/search/api/product/detail 两个接口——这两个接口每次请求都会查询数据库,是典型的CC攻击高价值目标
  • UA种类异常集中——超过70%的请求使用了3种高度相似的Chrome UA,版本号完全一致(真实用户群体的Chrome版本分布不可能如此单一)
  • 请求不携带Cookie和Referer——正常用户浏览电商网站时,页面间跳转必然产生Referer,且会话Cookie几乎始终存在 🔍

第二阶段:启动UA过滤和基础限速(20:08)

根据特征分析结果,立即在OpenResty节点部署第一道拦截规则:

-- 阶段一:拦截集中出现的攻击UA
access_by_lua_block {
    local ua = ngx.var.http_user_agent or ""

    -- 精确匹配攻击中高频出现的三个UA
    local attack_ua_hashes = {
        [ngx.md5("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")] = true,
        [ngx.md5("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36")] = true,
    }

    if attack_ua_hashes[ngx.md5(ua)] then
        local ref = ngx.var.http_referer or ""
        local cookie = ngx.var.http_cookie or ""
        -- UA命中 + 无Referer + 无Cookie = 攻击流量
        if #ref == 0 and #cookie == 0 then
            return ngx.exit(444)
        end
    end
}

解释: 这段规则并非简单地封禁某个UA(那样会误伤使用相同浏览器版本的正常用户),而是做了三重条件叠加判断:UA哈希匹配 + Referer为空 + Cookie为空,三个条件同时成立才拦截。正常用户即使UA版本相同,浏览网页时一定会携带Cookie和Referer,因此这种组合判断几乎不会产生误伤 ✅

同时对两个被集中攻击的API接口启动严格限速:

limit_req_zone $binary_remote_addr zone=api_search:20m rate=5r/s;
limit_req_zone $binary_remote_addr zone=api_detail:20m rate=8r/s;

location /api/search {
    limit_req zone=api_search burst=10 nodelay;
    limit_req_status 444;
    proxy_pass http://backend;
}

location /api/product/detail {
    limit_req zone=api_detail burst=15 nodelay;
    limit_req_status 444;
    proxy_pass http://backend;
}

解释: 针对搜索接口限制每IP每秒5次请求、商品详情接口限制每秒8次。burst 值分别设为10和15,允许短时突发。正常用户搜索行为每秒不会超过2到3次,这个阈值留有充足的安全裕度。

效果: 部署后3分钟内,入口QPS从4.5万降至约1.8万。UA过滤拦截了大约60%的攻击流量 📉

第三阶段:攻击升级,启动JS质询(20:25)

攻击者在15分钟后调整了策略——更换了UA库,开始随机使用上百种不同的浏览器UA,并且部分请求开始伪造Referer头。QPS重新攀升到7万 ⚠️

此时UA精确匹配规则失效,需要升级到JS人机验证:

access_by_lua_block {
    -- 仅对被攻击的API路径启用JS验证
    if not ngx.re.find(ngx.var.uri, "^/api/(search|product)", "jo") then
        return  -- 非目标路径直接放行
    end

    local token = ngx.var.cookie_cdn_verify
    local ip = ngx.var.remote_addr
    local hour = os.date("%Y%m%d%H")
    local secret = "lanyiyun_waf_key_2026"
    local expected = ngx.md5(ip .. secret .. hour)

    if token == expected then
        return  -- 验证通过,继续正常处理
    end

    -- 返回JS质询页面
    ngx.header["Content-Type"] = "text/html; charset=utf-8"
    ngx.header["Cache-Control"] = "no-store, no-cache"
    ngx.say(string.format([[
<!DOCTYPE html>
<html><head><meta charset="utf-8">
<title>安全验证中</title></head><body>
<p>正在验证您的访问请求,请稍候...</p>
<script>
(function(){
    var t="%s";
    document.cookie="cdn_verify="+t+";path=/;max-age=3600;SameSite=Lax";
    setTimeout(function(){
        window.location.reload();
    }, 300);
})();
</script>
<noscript><p>请启用JavaScript后刷新页面。</p></noscript>
</body></html>
    ]], expected))
    return ngx.exit(200)
}

逐步解释:

  • 首先判断请求路径是否为被攻击的API接口,非目标路径直接放行,避免影响整站体验。
  • 检查请求是否携带名为 cdn_verify 的Cookie,并且其值是否等于基于客户端IP、密钥和当前小时数生成的MD5哈希。这个设计保证每个IP每小时只需验证一次,且不同IP的验证令牌不同,无法共享。
  • 未通过验证的请求会收到一段HTML页面,其中的JS代码自动设置Cookie并在300毫秒后刷新页面。真实浏览器无感完成验证,而不具备JS执行能力的CC攻击脚本会被持续拦截在此处。
  • Cache-Control: no-store, no-cache 防止验证页面被CDN缓存层或浏览器缓存,确保每次未验证的请求都能触发检查 🔐

效果: JS质询上线后,QPS从7万迅速回落至约2万。大量攻击工具无法执行JS,被彻底阻断。

第四阶段:对抗Headless浏览器(21:10)

攻击者再次升级——引入了Headless浏览器(无头Chrome)来执行JS验证。这类工具能够完整运行JavaScript,成功设置Cookie后通过验证继续发起请求。QPS回升至5万 💥

针对Headless浏览器的检测需要更深层的指纹识别:

-- 在JS验证页面中嵌入浏览器环境探测
ngx.say([[
<script>
(function(){
    var isBot = false;

    // 检测WebDriver属性(Headless浏览器特征)
    if (navigator.webdriver === true) isBot = true;

    // 检测Chrome DevTools协议特征
    if (window.chrome && !window.chrome.app) isBot = true;

    // 检测屏幕和窗口尺寸异常
    if (screen.width === 0 || screen.height === 0) isBot = true;
    if (window.outerWidth === 0 || window.outerHeight === 0) isBot = true;

    // 检测插件数量(Headless通常为0)
    if (navigator.plugins.length === 0) isBot = true;

    // 检测WebGL渲染器
    try {
        var canvas = document.createElement('canvas');
        var gl = canvas.getContext('webgl');
        var renderer = gl.getParameter(gl.RENDERER);
        if (renderer.indexOf('SwiftShader') !== -1) isBot = true;
    } catch(e) { isBot = true; }

    if (isBot) {
        // 探测到自动化工具,发送标记
        document.cookie = "cdn_verify=invalid;path=/;max-age=1";
    } else {
        var t = "]] .. expected .. [[";
        document.cookie = "cdn_verify=" + t + ";path=/;max-age=3600;SameSite=Lax";
    }
    setTimeout(function(){ location.reload(); }, 500);
})();
</script>
]])

关键检测点解释:

  • navigator.webdriver 属性在通过WebDriver协议控制的浏览器中为 true,这是区分Headless Chrome和真实Chrome最直接的特征。
  • navigator.plugins.length === 0 检测浏览器插件数量。真实桌面Chrome至少有PDF Viewer等默认插件,而Headless模式下插件列表通常为空。
  • WebGL渲染器检测中,SwiftShader 是Chrome Headless模式使用的软件GPU渲染引擎名称,真实桌面浏览器使用的是实际显卡名称(如"NVIDIA GeForce...")。
  • 探测到自动化工具时,故意设置一个无效的Cookie值,使其在下次请求时被服务端判定为验证失败,形成拦截闭环 🎯

效果: Headless浏览器检测上线后,又拦截了约60%的残余攻击流量,QPS降至约2万。

第五阶段:全局IP信誉封禁收尾(21:40)

经过前四阶段的逐层过滤,仍有部分高级攻击流量(使用了反检测框架绕过Headless指纹探测)持续以约2万QPS冲击。此时启动最后一道防线——基于行为信誉的动态IP封禁:

local counter = ngx.shared.ip_reputation
local ip = ngx.var.remote_addr
local fail_key = "fail:" .. ip

-- 统计该IP被JS验证拦截的次数
local fails = counter:get(fail_key) or 0

if fails > 20 then
    -- 20次验证失败 → 封禁30分钟
    counter:set("ban:" .. ip, 1, 1800)
    ngx.log(ngx.ERR, "[IP-BAN] ", ip, " fails=", fails)
end

local banned = counter:get("ban:" .. ip)
if banned then
    return ngx.exit(444)
end

解释: 对每个IP记录其JS验证失败的累计次数。当某个IP验证失败超过20次时,判定为攻击源,加入封禁名单1800秒(30分钟)。封禁期间该IP的所有请求直接以444断开,不再进入任何处理流程。这一措施将残余攻击流量在30分钟内逐步清零 🚫

最终结果

指标 数值
攻击峰值QPS ~120,000
攻击持续时间 约6小时
攻击源IP总数 ~35,000
源站感知到的异常请求 0
正常用户误拦截率 < 0.1%
CDN节点CPU峰值 45%

整个攻击过程中源站的QPS始终维持在正常的800到1200之间,终端用户无感知。蓝易云CDN五层递进式防护策略——UA过滤、路径限速、JS质询、Headless检测、IP信誉封禁——逐级升级响应强度,在节点侧完成了全部攻击流量的消化 ✅


这个案例的核心经验是:超强CC攻击没有一招制敌的银弹,只有分层递进、动态对抗的持续博弈。 攻击者会不断升级手段,防御方需要预先准备好多级防护预案,根据攻击特征的变化快速切换和叠加策略。蓝易云CDN将这套多级响应机制内置在边缘节点,确保面对任何强度的CC攻击都能在源站前方完成拦截 🔥

THE END