一、背景
目前国内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
留言