宝塔面板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