一、背景

目前国内WAF接入成本过高,考虑自建WAF实现7层防御。这里使用nginx_lua + ModSecurity进行组合防御。其中lua用做防御cc攻击,ModSecurity用来防御其他7层攻击。以下环境搭建使用centos7.9+openresty

cc攻击脚本参考

  • https://github.com/loveshell/ngx_lua_waf
  • https://github.com/unixhot/waf

ModSecurity防护参考

  • https://github.com/SpiderLabs/ModSecurity
  • https://github.com/SpiderLabs/ModSecurity-nginx
  • https://github.com/SpiderLabs/ModSecurity/wiki/Compilation-recipes-for-v3.x

二、安装ModSecurity

2.1 依赖软件安装

参考依赖软件安装,地址:https://github.com/SpiderLabs/ModSecurity/wiki/Compilation-recipes-for-v3.x
[root@iZj6cae06zxj2lpnpoomzwZ ~]# yum install gcc-c++ flex bison yajl yajl-devel curl-devel curl GeoIP-devel doxygen zlib-devel pcre-devel -y
[root@iZj6cae06zxj2lpnpoomzwZ ~]# yum -y install autoconf automake libtool git

2.2 安装ModSecurity

[root@iZj6cae06zxj2lpnpoomzwZ ~]# git clone https://github.com/SpiderLabs/ModSecurity.git
[root@iZj6cae06zxj2lpnpoomzwZ ~]# cd ModSecurity/
[root@iZj6cae06zxj2lpnpoomzwZ ModSecurity]# git branch        #v3/master 分支
[root@iZj6cae06zxj2lpnpoomzwZ ModSecurity]# ./build.sh
[root@iZj6cae06zxj2lpnpoomzwZ ModSecurity]# git submodule init
[root@iZj6cae06zxj2lpnpoomzwZ ModSecurity]# git submodule update
[root@iZj6cae06zxj2lpnpoomzwZ ModSecurity]# ./configure
[root@iZj6cae06zxj2lpnpoomzwZ ModSecurity]# yum install https://archives.fedoraproject.org/pub/archive/fedora/linux/updates/23/x86_64/b/bison-3.0.4-3.fc23.x86_64.rpm
[root@iZj6cae06zxj2lpnpoomzwZ ModSecurity]# make
[root@iZj6cae06zxj2lpnpoomzwZ ModSecurity]# make install

2.3 下载ModSecurity-nginx

[root@iZj6cae06zxj2lpnpoomzwZ tmp]# git clone https://github.com/SpiderLabs/ModSecurity-nginx.git
[root@iZj6cae06zxj2lpnpoomzwZ tmp]# mv ModSecurity-nginx /usr/local/

三、安装OpenResty

[root@iZt4n2w8edhjcih8oyjcl3Z ~]# wget -c http://mirrors.linuxeye.com/oneinstack-full.tar.gz && tar xzf oneinstack-full.tar.gz
[root@iZj6cae06zxj2lpnpoomzwZ ~]# cd oneinstack/include/
[root@iZj6cae06zxj2lpnpoomzwZ include]# vim openresty.sh
添加如下编译参数:
--add-module=/usr/local/ModSecurity-nginx

[root@iZj6cae06zxj2lpnpoomzwZ ~]#  ./oneinstack/install.sh --nginx_option 3 --reboot    #安装

# 备注:脚本参考https://github.com/oneinstack/oneinstack

四、配置检查

4.1 检测日志是否正常

[root@iZt4n2w8edhjcih8oyjcl3Z ~]# cat /usr/local/openresty/nginx/logs/error.log 
2023/04/23 10:59:47 [notice] 11277#0: ModSecurity-nginx v1.0.3 (rules loaded inline/local/remote: 0/0/0)
2023/04/23 10:59:47 [notice] 11280#0: ModSecurity-nginx v1.0.3 (rules loaded inline/local/remote: 0/0/0)
2023/04/23 10:59:59 [notice] 887#0: ModSecurity-nginx v1.0.3 (rules loaded inline/local/remote: 0/0/0)
2023/04/23 10:59:59 [notice] 942#0: ModSecurity-nginx v1.0.3 (rules loaded inline/local/remote: 0/0/0)
2023/04/23 11:00:55 [notice] 1317#0: ModSecurity-nginx v1.0.3 (rules loaded inline/local/remote: 0/0/0)

# 备注:ModSecurity-nginx v1.0.3 表示ModSecurity-nginx已经安装完成

4.2 nginx检测

[root@iZj6cae06zxj2lpnpoomzwZ ~]# nginx -V
nginx version: openresty/1.21.4.1
built by gcc 4.8.5 20150623 (Red Hat 4.8.5-44) (GCC) 
built with OpenSSL 1.1.1t  7 Feb 2023
TLS SNI support enabled
configure arguments: --prefix=/usr/local/openresty/nginx ...
--add-module=/usr/local/ModSecurity-nginx

# 备注:add-module=/usr/local/ModSecurity-nginx 表示已经添加了此模块,同时在之后的步骤不会写关于ModSecurity OWASP CRS的集成

4.3ModSecurity配置

五、Lua防御cc攻击配置

5.1 下载waf脚本

[root@iZt4n2w8edhjcih8oyjcl3Z ~]# git clone https://gitee.com/xiangys0134/deploy.git
[root@iZt4n2w8edhjcih8oyjcl3Z ~]# cd deploy/nginx-lua/ngx_lua_waf
[root@iZt4n2w8edhjcih8oyjcl3Z ngx_lua_waf]# cp -r waf /usr/local/openresty/nginx/conf/

# 个人仓库已由github迁移至码云

5.2 配置lua防护规则

[root@iZt4n9mhb57oqffa6jxg2yZ conf]# vim nginx.conf
http区块下添加如下内容:
  lua_package_path "/usr/local/openresty/nginx/conf/waf/?.lua;;";
  lua_shared_dict limit 50m;
  init_by_lua_file /usr/local/openresty/nginx/conf/waf/init.lua;
  access_by_lua_file /usr/local/openresty/nginx/conf/waf/waf.lua;

# 备注:这里默认所有站点在access_by_lua这块都会进行waf防御,也可以将其写入到对应的server区块中。具体看自己需求

[root@iZt4n2w8edhjcih8oyjcl3Z conf]# nginx -t
[root@iZt4n2w8edhjcih8oyjcl3Z conf]# systemctl restart nginx

5.3 测试cc攻击

[root@iZt4n2w8edhjcih8oyjcl3Z conf]# vim waf/config.lua # 配置符合自己的规则
ipWhitelist={"127.0.0.1"}
ipBlocklist={"1.0.0.1"}
CCDeny="on"
CCrate="100/60"

# 这里我简化了源作者的以下功能,所以只用以上几个核心参数就够了。CCrate="100/60"表示60秒内有超过100个请求则拦截1分钟

ubuntu@VM-12-8-ubuntu:~ab -c 600 -n 600 http://8.222.192.68/  # ab测试
ubuntu@VM-12-8-ubuntu:~ curl -I http://8.222.192.68/   # 客户端测试访问,返回状态码429,拦截成功
HTTP/1.1 429 Too Many Requests
Server: openresty
Date: Sun, 23 Apr 2023 03:16:57 GMT
Content-Type: text/html
Content-Length: 166
Connection: keep-alive

六、写在最后

6.1函数获取客户ip

# 获取客户端ip函数如下:
function ltrim(input)
    return (string.gsub(input, "^[ \t\n\r]+", ""))
end

function getClientIp()
    -- 如果waf面向client,则优先使用ngx.var.remote_addr,防止被伪造ip,注释掉X_Forwarded_For标头的获取
    local IP = ngx.req.get_headers()["X_Forwarded_For"]
    if IP then
        local IPS, err = ngx_re.split(IP, ",")
        local ips_len = table.getn(IPS)
        IP, err = ngx_re.split(IP, ",")[ips_len]
        IP = ltrim(IP)
    end
    --ngx.log(ngx.ERR, "X_Forwarded_For_IP: ", IP)
    --local IP = ngx.var.remote_addr
    if IP == nil then
        IP  = ngx.var.remote_addr
    end
    if IP == nil then
        IP  = "unknown"
    end
    --ngx.log(ngx.ERR, "remote_addr: ", IP)
    return IP
end

6.2 其他场景验证

[root@iZt4n2w8edhjcih8oyjcl3Z conf]# vim nginx.conf # 新增配置
    location /test1 {
      content_by_lua_block {
        -- local CLIENT_IP= ngx.req.get_headers(0)["x_forwarded_for"]
        local CLIENT_IP= ngx.req.get_headers()["x_forwarded_for"]
        --local CLIENT_IP= ngx.req.get_headers()["X_real_ip"]
        ngx.say(CLIENT_IP);
      }
    }

[root@iZt4n2w8edhjcih8oyjcl3Z conf]# nginx -t
[root@iZt4n2w8edhjcih8oyjcl3Z conf]# systemctl restart nginx

# 客户端测试
ubuntu@VM-12-8-ubuntu:~curl -H 'X-Forwarded-For: 1.2.3.4 ' http://8.222.192.68/test1
1.2.3.4
ubuntu@VM-12-8-ubuntu:~ curl -H 'X-Forwarded-For: 1.2.3.4,2.3.3.3 ' http://8.222.192.68/test1
1.2.3.4,2.3.3.3

# 备注:这里获取到x_forwarded_for标头信息时还需要做一下字符串切割,以后再补充

6.3 完整脚本

-- config.lua
bot_agent = {"googlebot","yahoo","bingbot","yandex","baiduspider","facebookexternalhit","twitterbot","rogerbot","linkedinbot","petalbot","telegrambot","applebot"}
static_file=".(gif|jpg|jpeg|png|bmp|swf|flv|mp4|ico|js|webp|svg|css|ttf|otf|fon|json|woff2|woff|7z|csv|midi|tif|zip|avi|doc|gz|mkv|ppt|tiff|zst|avif|docx|ico|mp3|apk|dmg|webm|xls|xlsx|xml|txt)$"
redis_addr = "127.0.0.1"
redis_port = 6379
redis_password = "Aa123456"
connect_timeout = 2000
send_timeout = 2000
read_timeout = 2000
ip_black_seconds = "86400"
CCDeny="on"
CCrate="200/60"

-- waf.lua
local client_ip = getClientIp()
local redis = require "resty.redis"
local red = redis:new()
local ok, err = red:connect(redis_addr, redis_port)
red:auth(redis_password)
red:set_timeout(connect_timeout, send_timeout, read_timeout)

if not ok then
    ngx.log(ngx.ERR, "failed to connect to Redis: ", err)
    return
end

if statics() then
elseif uabot() then
elseif whiteip(red, client_ip) then
elseif blockip(red, client_ip) then
elseif denycc(red, client_ip) then
else
        return
end

local ok, err = red:set_keepalive(10000, 100)
if not ok then
        return
end

-- init.lua
require 'config'

local ngx_re = require "ngx.re"
local ngxmatch=ngx.re.find

local optionIsOn = function (options) return options == "on" and true or false end
local CCDeny = optionIsOn(CCDeny)

------------------------读取函数规则--------------------------
function ltrim(input)
    return (string.gsub(input, "^[ \t\n\r]+", ""))
end

function getClientIp()
    -- 如果waf面向client,则优先使用ngx.var.remote_addr,防止被伪造ip,注释掉X_Forwarded_For标头的获取
    local IP = ngx.req.get_headers()["X_Forwarded_For"]
    if IP then
        local IPS, err = ngx_re.split(IP, ",")
        local ips_len = table.getn(IPS)
        IP, err = ngx_re.split(IP, ",")[ips_len]
        IP = ltrim(IP)
    end
    --ngx.log(ngx.ERR, "X_Forwarded_For_IP: ", IP)
    --local IP = ngx.var.remote_addr
    if IP == nil then
        IP  = ngx.var.remote_addr
    end
    if IP == nil then
        IP  = "unknown"
    end
    --ngx.log(ngx.ERR, "remote_addr: ", IP)
    return IP
end

------------------------------------------ 防护函数 ---------------------------------------------------------
function denycc(red, client_ip)
    --local uri=ngx.var.request_uri
    local uri=ngx.var.uri
    local CCcount=tonumber(string.match(CCrate,'(.*)/'))
    local CCseconds=tonumber(string.match(CCrate,'/(.*)'))
    local host, err = string.lower(ngx.req.get_headers()["Host"])
    local token = client_ip..uri
    local black_ip = 'black_'..host..client_ip
    local limit = ngx.shared.limit
    local req,_=limit:get(token)
    if req then
        if req >= CCcount then
            local req_count, err = red:incr(black_ip)
            if req_count == 1 then
                red:expire(black_ip, ip_black_seconds) -- Set expiration time to 3600 seconds
            end
            return true
        else
            limit:incr(token,1)
        end
    else
        limit:set(token,1,CCseconds)
    end

    return true
end

function uabot()
    local ua = ngx.var.http_user_agent
    if ua ~= nil then
        for _,bot in pairs(bot_agent) do
            if bot ~="" and ngxmatch(ua,bot,"isjo") then
                return true
            end
        end
    end
    return false
end

function statics()
    local uri=ngx.var.uri
    if ngxmatch(uri,static_file,"isjo") then
        return true
    end
    return false
end

function whiteip(red, client_ip)
    if CCDeny then
        -- 如果预先设置白名单ip示例:white_192.168.1.58
        local white_ip = 'white_'..client_ip
        local whited, err = red:get(white_ip)
        if whited ~= ngx.null and whited then
            return true
        end
        return false
    end
    return true
end

function blockip(red, client_ip)
    local host, err = string.lower(ngx.req.get_headers()["Host"])
    local black_ip = 'black_'..host..client_ip
    local blocked, err = red:get(black_ip)

    if blocked ~= ngx.null and blocked then
        ngx.exit(403)
        return true
    end

    return false
end


最后修改日期: 2025年2月3日

作者

留言

撰写回覆或留言

发布留言必须填写的电子邮件地址不会公开。