此文转载自:斐斐のBlog
原文地址:宝塔面板Nginx的Lua-Waf防火墙终极改进 动态封禁IP
实在没有需要修改的地方,就连理由都一样,原文如下:
宝塔面板自带的Nginx防火墙有些鸡肋,对于大量的恶意攻击只能临时拦截,而不能封禁IP,下面的修改可以帮你做到:
CC攻击屡教不改,立即ban!
漏洞扫描屡教不改:立即ban!
同一个IP段轮流攻击,整个IP段都给你ban了!
使用了CDN?没关系,获取了真实IP再ban!
一小时后,unban……
宝塔面板的nginx修改/www/server/nginx/waf/目录下的三个文件即可,如果没有宝塔面板,nginx必须安装Lua,然后对下面的代码稍加修改,并且自己加上正则黑名单(或者下载个宝塔面板把规则文件拷出来)也可以正常使用。
代码:config.lua
RulePath = "/www/server/panel/vhost/wafconf/" --规则文件夹
attacklog="on"
logdir = "/www/wwwlogs/waf/" --日志文件夹
UrlDeny="on"
Redirect="on"
CookieMatch="on"
postMatch="on"
whiteModule="on"
black_fileExt={"php"}
ipWhitelist={}
ipBlocklist={}
CCDeny="on"
CCrate="500/100" --这个是CC攻击的几秒钟允许请求几次
代码:init.lua
require 'config'
local match = string.match
local ngxmatch=ngx.re.find
local unescape=ngx.unescape_uri
local get_headers = ngx.req.get_headers
local optionIsOn = function (options) return options == "on" and true or false end
logpath = logdir
rulepath = RulePath
UrlDeny = optionIsOn(UrlDeny)
PostCheck = optionIsOn(postMatch)
CookieCheck = optionIsOn(cookieMatch)
WhiteCheck = optionIsOn(whiteModule)
PathInfoFix = optionIsOn(PathInfoFix)
attacklog = optionIsOn(attacklog)
CCDeny = optionIsOn(CCDeny)
Redirect=optionIsOn(Redirect)
function subString(str, k) --截取字符串
ts = string.reverse(str)
_, i = string.find(ts, k)
m = string.len(ts) - i + 1
return string.sub(str, 1, m)
end
function getClientIp()
IP = ngx.var.remote_addr
if ngx.var.HTTP_X_FORWARDED_FOR then
IP = ngx.var.HTTP_X_FORWARDED_FOR
end
if IP == nil then
IP = "unknown"
end
IP = subString(IP, "[.]") .. "*"
return IP
end
function getRealIp()
IP = ngx.var.remote_addr
if ngx.var.HTTP_X_FORWARDED_FOR then --如果用了CDN,判断真实IP
IP = ngx.var.HTTP_X_FORWARDED_FOR
end
if IP == nil then
IP = "unknown"
end
return IP
end
function write(logfile,msg)
local fd = io.open(logfile,"ab")
if fd == nil then return end
fd:write(msg)
fd:flush()
fd:close()
end
function log(method,url,data,ruletag)
if attacklog then
local realIp = getRealIp()
local ua = ngx.var.http_user_agent
local servername=ngx.var.server_name
local time=ngx.localtime()
if ua then
line = realIp.." ["..time.."] \""..method.." "..servername..url.."\" \""..data.."\" \""..ua.."\" \""..ruletag.."\"\n"
else
line = realIp.." ["..time.."] \""..method.." "..servername..url.."\" \""..data.."\" - \""..ruletag.."\"\n"
end
local filename = logpath..'/'..servername.."_"..ngx.today().."_sec.log"
write(filename,line)
end
end
------------------------------------规则读取函数-------------------------------------------------------------------
function read_rule(var)
file = io.open(rulepath..'/'..var,"r")
if file==nil then
return
end
t = {}
for line in file:lines() do
table.insert(t,line)
end
file:close()
return(t)
end
-----------------------------------频繁扫描封禁ip-------------------------------------------------------------------
function ban_ip(point)
local token = getClientIp() .. "_WAF"
local limit = ngx.shared.limit
local req,_=limit:get(token)
if req then
limit:set(token,req+point,3600) --发现一次,增加积分,1小时内有效
else
limit:set(token,point,3600)
end
end
function get_ban_times()
local token = getClientIp() .. "_WAF"
local limit = ngx.shared.limit
local req,_=limit:get(token)
if req then
return req
else return 0
end
end
function is_ban()
local ban_times = get_ban_times()
if ban_times >= 100 then --超过100积分,ban
ngx.header.content_type = "text/html;charset=UTF-8"
ngx.status = ngx.HTTP_FORBIDDEN
ngx.exit(ngx.status)
return true
else
return false
end
return false
end
urlrules=read_rule('url')
argsrules=read_rule('args')
uarules=read_rule('user-agent')
wturlrules=read_rule('whiteurl')
postrules=read_rule('post')
ckrules=read_rule('cookie')
html=read_rule('returnhtml')
function say_html()
ban_ip(15) --恶意攻击,罚15分
if Redirect then
ngx.header.content_type = "text/html;charset=UTF-8"
ngx.status = ngx.HTTP_FORBIDDEN
ngx.say(html)
ngx.exit(ngx.status)
end
end
function whiteurl()
if WhiteCheck then
if wturlrules ~=nil then
for _,rule in pairs(wturlrules) do
if ngxmatch(ngx.var.uri,rule,"isjo") then
return true
end
end
end
end
return false
end
function fileExtCheck(ext)
local items = Set(black_fileExt)
ext=string.lower(ext)
if ext then
for rule in pairs(items) do
if ngx.re.match(ext,rule,"isjo") then
log('POST',ngx.var.request_uri,"-","file attack with ext "..ext)
say_html()
end
end
end
return false
end
function Set (list)
local set = {}
for _, l in ipairs(list) do set[l] = true end
return set
end
function args()
for _,rule in pairs(argsrules) do
local args = ngx.req.get_uri_args()
for key, val in pairs(args) do
if type(val)=='table' then
local t={}
for k,v in pairs(val) do
if v == true then
v=""
end
table.insert(t,v)
end
data=table.concat(t, " ")
else
data=val
end
if data and type(data) ~= "boolean" and rule ~="" and ngxmatch(unescape(data),rule,"isjo") then
log('GET',ngx.var.request_uri,"-",rule)
say_html()
return true
end
end
end
return false
end
function url()
if UrlDeny then
for _,rule in pairs(urlrules) do
if rule ~="" and ngxmatch(ngx.var.request_uri,rule,"isjo") then
log('GET',ngx.var.request_uri,"-",rule)
say_html()
return true
end
end
end
return false
end
function ua()
local ua = ngx.var.http_user_agent
if ua ~= nil then
for _,rule in pairs(uarules) do
if rule ~="" and ngxmatch(ua,rule,"isjo") then
log('UA',ngx.var.request_uri,"-",rule)
say_html()
return true
end
end
end
return false
end
function body(data)
for _,rule in pairs(postrules) do
if rule ~="" and data~="" and ngxmatch(unescape(data),rule,"isjo") then
log('POST',ngx.var.request_uri,data,rule)
say_html()
return true
end
end
return false
end
function cookie()
local ck = ngx.var.http_cookie
if CookieCheck and ck then
for _,rule in pairs(ckrules) do
if rule ~="" and ngxmatch(ck,rule,"isjo") then
log('Cookie',ngx.var.request_uri,"-",rule)
say_html()
return true
end
end
end
return false
end
function denycc()
if CCDeny then
CCcount=tonumber(string.match(CCrate,'(.*)/'))
CCseconds=tonumber(string.match(CCrate,'/(.*)'))
local token = getRealIp()
local limit = ngx.shared.limit
local req,_=limit:get(token)
if req then
if req > CCcount then
limit:incr(token,1)
ban_ip(req - CCcount) --CC攻击,罚分
ngx.header.content_type = "text/html"
ngx.status = ngx.HTTP_FORBIDDEN
ngx.say("老哥你手速也忒快了吧,要不休息"..CCcount.."秒?")
ngx.exit(ngx.status)
return true
else
limit:incr(token,1)
end
else
limit:set(token,1,CCseconds)
end
end
return false
end
function get_boundary()
local header = get_headers()["content-type"]
if not header then
return nil
end
if type(header) == "table" then
header = header[1]
end
local m = match(header, ";%s*boundary=\"([^\"]+)\"")
if m then
return m
end
return match(header, ";%s*boundary=([^\",;]+)")
end
function whiteip()
if next(ipWhitelist) ~= nil then
for _,ip in pairs(ipWhitelist) do
if getClientIp()==ip then
return true
end
end
end
return false
end
function blockip()
if next(ipBlocklist) ~= nil then
for _,ip in pairs(ipBlocklist) do
if getClientIp()==ip then
ngx.exit(444)
return true
end
end
end
return false
end
代码:waf.lua
local content_length=tonumber(ngx.req.get_headers()['content-length'])
local method=ngx.req.get_method()
local ngxmatch=ngx.re.match
if whiteip() then
elseif blockip() then
elseif whiteurl() then
elseif is_ban() then
elseif denycc() then
elseif ngx.var.http_Acunetix_Aspect then
ngx.exit(444)
elseif ngx.var.http_X_Scan_Memo then
ngx.exit(444)
elseif ua() then
elseif url() then
elseif args() then
elseif cookie() then
elseif PostCheck then
if method=="POST" then
local boundary = get_boundary()
if boundary then
local len = string.len
local sock, err = ngx.req.socket()
if not sock then
return
end
ngx.req.init_body(128 * 1024)
sock:settimeout(0)
local content_length = nil
content_length=tonumber(ngx.req.get_headers()['content-length'])
local chunk_size = 4096
if content_length < chunk_size then
chunk_size = content_length
end
local size = 0
while size < content_length do
local data, err, partial = sock:receive(chunk_size)
data = data or partial
if not data then
return
end
ngx.req.append_body(data)
if body(data) then
return true
end
size = size + len(data)
local m = ngxmatch(data,[[Content-Disposition: form-data;(.+)filename="(.+)\\.(.*)"]],'ijo')
if m then
fileExtCheck(m[3])
filetranslate = true
else
if ngxmatch(data,"Content-Disposition:",'isjo') then
filetranslate = false
end
if filetranslate==false then
if body(data) then
return true
end
end
end
local less = content_length - size
if less < chunk_size then
chunk_size = less
end
end
ngx.req.finish_body()
else
ngx.req.read_body()
local args = ngx.req.get_post_args()
if not args then
return
end
for key, val in pairs(args) do
if type(val) == "table" then
if type(val[1]) == "boolean" then
return
end
data=table.concat(val, ", ")
else
data=val
end
if data and type(data) ~= "boolean" and body(data) then
body(key)
end
end
end
end
else
return
end