一次暴力枚举攻击的防御实践:从 IP 封禁到 WAF,再到 Nginx+Lua 业务层防御

前言:最近,我的环境遭遇了一场针对性的暴力枚举攻击。

起初,我尝试在云服务器安全组Nginx 中直接封禁攻击 IP,却发现它们像穿了隐身衣------完全无效

经过排查,我揪出了元凶:CDN 代理。它本为加速而生,却在此刻成了攻击者的"天然屏障"。

本文将复盘我从 IP 封禁失效 ,到利用 WAF 识破真实 IP,再到借助 Nginx + Lua + Redis 实现业务级动态封禁的完整防御进化之路。

希望能为遇到类似困境的你,提供一些实战参考。

一、 背景:诡异的日志

最近,我的网关服务器日志里出现了极其规律的恶意扫描流量。攻击者正在对 API 接口进行猛烈的暴力枚举,试图通过猜测商户号或管理员账号来寻找突破口。

以下是经过脱敏处理的真实日志片段:

bash 复制代码
2026-05-20 17:29:36.362 DEBUG [io-8188-exec-11] [com.example.dao.MerchantMapper.selectOne] : ==>  Preparing: SELECT id, merchant_no, merchant_name, [隐去敏感字段] FROM merchant WHERE (merchant_no = ?) 
2026-05-20 17:29:36.362 DEBUG [io-8188-exec-11] [com.example.dao.MerchantMapper.selectOne] : ==> Parameters: adbcmaster(String)
2026-05-20 17:29:36.471 DEBUG [io-8188-exec-29] [com.example.dao.MerchantMapper.selectOne] : <==      Total: 0
2026-05-20 17:29:36.471  INFO [io-8188-exec-29] [com.example.security.ExampleBasicAuth] : 用户不存在: admin ipAddr: 155.94.241.106
2026-05-20 17:29:36.472  INFO [io-8188-exec-30] [com.example.security.ExampleBasicAuth] : 用户不存在, 或未开启
2026-05-20 17:29:36.555  INFO [io-8188-exec-29] [com.example.security.ExampleBasicAuth] : 用户不存在: root ipAddr: 155.94.241.106

同一个 IP 155.94.241.106 在几毫秒内,疯狂尝试 adbcmasteradminroot 等账号,并频繁查询 merchant_no。这明显是一次自动化枚举攻击

那么问题来了:一个显而易见的恶意 IP,为什么我在云服务器安全组和 Nginx 里封禁都无效呢?

二、 复盘:为什么传统 IP 封禁失灵?

1. 部署现状

我的网站启用了 CDN(例如 Cloudflare 或 AgileCDN),所有流量先经过 CDN 节点,再回源到源站 IP。

2. 云防火墙 / 安全组为何无效?

当我在云服务器(Azure/阿里云/腾讯云)的安全组里添加规则 拒绝 IP 155.94.241.106 对所有端口的访问 时,我发现完全无效

原因: 安全组拦截的是 IP 包。攻击者通过 CDN 访问时,安全组看到的源 IP 是 CDN 的边缘节点 IP (如 162.158.175.13),而不是 155.94.241.106。因此,规则无法匹配。

⚠️ 重要提醒:虽然安全组拦不住 CDN 后的 HTTP 请求,但它绝对不能省!

它依然是保护服务器的第一道物理防线。假设攻击者绕过域名,直接扫描源站的 22 端口(SSH)或 3389 端口(RDP)等非 HTTP 服务,安全组是你最后的堡垒。因此,务必在安全组中配置一条拒绝所有 IP 访问源站 非 HTTP/HTTPS 端口 的规则。

✅ 安全组推荐配置(以 Azure NSG 为例):

  • 来源 IP 地址155.94.241.106, 45.205.1.8
  • 目标端口范围*(拒绝所有端口)
  • 协议Any
  • 操作拒绝
  • 优先级100(优先级需小于放行规则,如 HTTP(300))

3. Nginx deny 为何也无效?

很多运维第一时间会想到在 Nginx 里写 deny 155.94.241.106;

原因: 默认情况下,Nginx 的 $remote_addr 记录的是直接发起 TCP 连接的 IP ,即 CDN 节点 IP。因此,deny 同样匹配不到真实 IP。

三、 破局点:Web 应用防火墙 (WAF)

既然传统方法失效,必须引入能识别 CDN 背后真实 IP 的设备 ------Web 应用防火墙 (WAF)

原理分析

Cloudflare WAF 为例,它部署在 CDN 边缘节点。当请求到达 Cloudflare 时,它能直接读取客户端真实 IP(例如 155.94.241.106),无需二次转发。

✅ 实操演示:配置 Cloudflare WAF 拦截真实 IP(操作+参数)

操作步骤:

  1. 登录 Cloudflare 控制台 -> 安全性 (Security) -> WAF -> 自定义规则 (Custom Rules)
  2. 点击 创建规则
  3. 填写规则名称(如 Block_Malicious_IPs)。
  4. 配置 表达式(核心参数)
    • 字段 :选择 IP 来源地址 (IP Source Address)

    • 运算符 :选择 等于 (eq) 或 属于 (in)

    • :输入 155.94.241.106(如需多个,使用 {"155.94.241.106" "45.205.1.8"}

    • 表达式预览(可直接粘贴到编辑器)

      nginx 复制代码
      ip.src eq "155.94.241.106"

      或(拦截多个 IP):

      nginx 复制代码
      ip.src in {"155.94.241.106" "45.205.1.8"}
    • 💡 注意: Cloudflare 要求 IP 地址必须用双引号包裹,否则规则无法保存。

  5. 然后采取措施 :选择 阻止 (Block)
  6. 放置位置 :建议自定义并设置为 优先级 1(最顶部),确保最先匹配。
  7. 点击 部署

效果验证:

部署后,在 WAF -> 事件 日志里,你会立刻看到 155.94.241.106 被成功拦截的记录。

⚠️ 注意:WAF 只能保护开启了 CDN 代理(橙色云)的域名。如果你的域名仅做 DNS 解析(灰色云),流量不经过 Cloudflare,WAF 无效。

四、 终极方案:Nginx + Lua(针对业务字段的精准防御)

虽然 WAF 解决了 IP 识别问题,但攻击者只要换个 IP 就得重新去 WAF 后台添加规则,运维成本高。如果你有唯一的「商户号」或「渠道号」,Nginx+Lua+Redis 是杀伤力最强的动态防御方案。

为什么 Nginx+Lua 比纯 Nginx if 更好?

维度 纯 Nginx if Nginx + Lua
配置生效 修改文件后必须 nginx -s reload 实时生效(通过 Redis 动态控制)
性能 location 里写复杂 if 会消耗 CPU 使用 access_by_lua 阶段,性能优秀
防御粒度 只能匹配简单的字符串 可以执行复杂的业务逻辑(查数据库、限流等)
IP 依赖性 依赖 IP,变 IP 就失效 不依赖 IP,只要业务字段(商户号)不变,立刻拦截

✅ 实操:Nginx+Lua 配置参数(拦截特定商户号)

前提: 假设你的业务里存在统一的 merchant_id,并且您安装了 lua-resty-redis 库。

1. 在 Nginx 的 http 块中配置 Lua 路径:

nginx 复制代码
lua_package_path "/usr/local/openresty/lualib/?.lua;;";
lua_shared_dict limit_store 10m;

2. 编写高性能 Lua 脚本(例如 /etc/nginx/lua/block_merchant.lua):

lua 复制代码
-- 引入 redis 库
local redis = require "resty.redis"
local red = redis:new()
red:set_timeout(1000)

-- 连接 redis
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
    -- 👇 此处建议加上错误日志,以便运维人员能及时发现 Redis 故障
    ngx.log(ngx.ERR, "Failed to connect to Redis: ", err)
    return
end

-- 从请求参数或 body 中获取 merchant_id(根据实际业务调整)
local args = ngx.req.get_uri_args()
local merchant_id = args["merchant_id"]

if not merchant_id then
    -- 尝试从 POST body 解析
    ngx.req.read_body()
    local body = ngx.req.get_body_data()
    -- 注意:若 body 过大导致 get_body_data() 为 nil,此处暂不处理缓存文件
    if body then
        local pattern = "merchant_id[=: ]([0-9]+)"
        local m, err = ngx.re.match(body, pattern, "jo")
        if m then
            merchant_id = m[1]
        end
    end
end

if merchant_id then
    -- 去 Redis 查这个 merchant_id 是否在黑名单里
    local key = "blocked:merchant:" .. merchant_id
    local is_blocked = red:get(key)
    
    if is_blocked == "1" then
        ngx.log(ngx.INFO, "Blocked request for merchant_id: ", merchant_id)
        -- ✅ 即使被拦截,也要放回连接池,防止连接泄漏
        red:set_keepalive(10000, 100)
        return ngx.exit(403) -- 直接返回 403
    end
end

-- ✅ 核心优化:使用连接池替代 close,提升高并发性能
red:set_keepalive(10000, 100)

📝 总结段落(替换红框内容):

在连接 Redis 失败时,我们引入了 ngx.log(ngx.ERR)。这不仅是为了让运维能第一时间在 error.log 中捕捉到缓存故障,更重要的是,我们选择了 return(放行)而不是 ngx.exit(500)(拦截)。在安全防御中,"降级放行"往往比"全站玉碎"更符合业务连续性要求。

🚀 为什么使用 set_keepalive

在高并发网关场景下,每个请求都 connect() 然后 close() 会频繁触发 TCP 三次握手和四次挥手,导致 Redis 产生大量 TIME_WAIT 连接,最终可能耗尽服务器端口。

使用 red:set_keepalive(10000, 100) 可以建立一个连接池 ,复用已经建立好的 Redis 连接,极大提升性能并降低资源消耗。这是生产环境 Nginx+Lua 脚本的标准写法

3. 在 Nginx 的 serverlocation 中引入脚本:

nginx 复制代码
server {
    listen 443 ssl http2;
    server_name securegtw.example.com;
    
    # ... SSL配置 ...

    location / {
        access_by_lua_file /etc/nginx/lua/block_merchant.lua;
        proxy_pass http://your_backend;
        # ... 其他 proxy 配置 ...
    }
}

4. 运维操作:

当发现商户 1001 异常时,运维只需在 Redis 中执行:

bash 复制代码
redis-cli SET blocked:merchant:1001 1

此时,所有商户 1001 的请求会被 Nginx 直接拦截(返回 403),无需重启 Nginx,秒级生效。

五、 没有统一的商户号,怎么防御?

很多业务初期就是没有统一的商户号/渠道号,或者业务字段是随机的、无法枚举的。这种情况下,你依然可以借助 Nginx 实现很好的防御。

方案一:Nginx 速率限制(防暴力破解/CC 攻击的杀手锏)

这是用来解决"同一个 IP 疯狂试密码"最直接的工具。不需要任何业务 ID,只认 IP。

操作步骤:

  1. nginx.confhttp 块添加限流区域:
nginx 复制代码
# 定义限流区域,基于真实 IP ($remote_addr),每分钟允许 5 次
limit_req_zone $remote_addr zone=login_limit:10m rate=5r/m;
  1. server 块里的登录接口位置应用:
nginx 复制代码
location /login {
    # 启用限流,超过 5 次/分钟,返回 429
    limit_req zone=login_limit burst=3 nodelay;
    proxy_pass http://your_backend;
    # ... 其他配置 ...
}

⚠️ 关键前提: 必须先配置 set_real_ip_from(让 Nginx 识别 CDN 后的真实 IP),否则限流会限到 CDN 节点头上。

方案二:Nginx 的 realip 模块 + deny(手动封 IP)

如果没有业务 ID,那只能手动把抓到 IP 加入黑名单。虽然笨,但有效。

nginx 复制代码
# 必须先配置这个,让 Nginx 识别真实 IP
set_real_ip_from 173.245.48.0/20; # (Cloudflare 的 IP 段)
real_ip_header X-Forwarded-For;
real_ip_recursive on;

# 在 server 块里拒绝特定 IP
deny 155.94.241.106;
deny 45.205.1.8;

方案三:Nginx+Lua 来做「动态 IP 黑名单」(运维进阶)

如果你希望不需要重启 Nginx 就能动态封禁新出现的 IP,那可以用 Lua 脚本配合 Redis 来实现。

Lua 脚本逻辑(简化版):

lua 复制代码
local redis = require "resty.redis"
local red = redis:new()
red:connect("127.0.0.1", 6379)

-- 获取真实 IP
local ip = ngx.var.remote_addr
local is_blocked = red:get("blocked_ip:" .. ip)

if is_blocked == "1" then
    red:set_keepalive(10000, 100)
    return ngx.exit(403) -- 直接拦截
end

red:set_keepalive(10000, 100)

运维操作:

在 Redis 执行以下命令:

bash 复制代码
redis-cli SET blocked_ip:155.94.241.106 1

无需重启 Nginx,新 IP 瞬间被拦截。

六、 总结

这次攻防实战让我深刻领悟到防御的进化路径:

  1. 基础防御云服务器安全组。必须配置,它能挡住针对服务器 IP 的直接扫描,是最后一道防线。
  2. 感知防御WAF。解决了 CDN 隐藏真实 IP 的难题,推荐所有开启 CDN 的域名接入,但通过 IP 封禁仍不够完美。
  3. 逻辑防御Nginx + Lua + Redis 。如果你有商户号或渠道号,一定要在这个层面实现"业务级动态封禁"。它能让你从"追着 IP 跑"的疲于奔命中解脱出来,变成"守株待兔"

记住:真正的安全,是无惧 IP 变换,只在业务逻辑层面做拦截。防御是一场猫鼠游戏,从「封 IP」到「封特征」是运维人员必须面对的进化之路。


(本文日志已进行脱敏处理,仅作技术交流使用。)

相关推荐
不老刘1 小时前
一次一密临时票据:医疗跨系统SSO的安全设计方案
安全·sso
Ether IC Verifier3 小时前
TCP三次握手与四次挥手详解
网络·网络协议·tcp/ip·计算机网络
pengyi87101510 小时前
独享IP池自动化维护方案,智能检测自动延长使用寿命
网络协议·tcp/ip·自动化
Likeadust12 小时前
私有化视频会议系统/智能会议管理系统EasyDSS集群通话助力各行业安全高效远程协作
安全
审判长烧鸡14 小时前
【Go工具】go-playground是什么组织?官方的?
开发语言·安全·go
JiaWen技术圈14 小时前
网站用户注册行为验证码方案
运维·安全
百度智能云技术站14 小时前
百度 Agent 安全中心:构筑企业智能体的安全底座
人工智能·安全·dubbo
视觉&物联智能15 小时前
【杂谈】-企业人工智能超越实验:安全拓展的实践路径
人工智能·安全·aigc·agent·agi
KnowSafe16 小时前
2026年SSL证书市场便宜且安全的SSL证书调研
网络协议·安全·ssl