Aprimoramento do Firewall Lua-Waf para Nginx com Banimento Dinâmico de IP

O firewall Nginx integrado ao painel BaoTa apresenta limitações contra ataques contínuos, pois apenas bloqueia solicitações sem realizar o banimento de IPs. As modificações a seguir permitem uma proteção mais eficaz, encluindo banimento dinâmico baseado em comportamento malicioso.

  • Ataques CC (Distributed Denial of Service) recorrentes resultam em banimento imediato do IP.
  • Varreduras de vulnerabilidade persistentes também acionam o banimento.
  • Se um intervalo de IPs for responsável por ataques, todo o segmento de rede é banido.
  • Detecção de IP real mesmo quando CDN está ativo, garantindo banimento correto.
  • Banimento temporário com remoção automática após uma hora.

Para aplicar essas alterações, modifique os arquivos no diretório /www/server/nginx/waf/. Caso o painel BaoTa não esteja instalado, é necessário que o Nginx tenha suporte a Lua, e as regras devem ser adaptadas, incluindo a definição de listas negras ou a importação de regras de firewall existentes.

Arquivo de Configuração: config.lua

Este arquivo define parâmetros globais como caminhos para regras e logs, além de ativar funcionalidades específicas do firewall.

caminhoRegras = "/www/server/panel/vhost/wafconf/"    -- Diretório das regras
logAtivado = "sim"
diretorioLogs = "/www/wwwlogs/waf/"    -- Diretório de logs
bloqueioUrlAtivado = "sim"
redirecionamentoAtivado = "sim"
verificacaoCookieAtivada = "sim"
verificacaoPostAtivada = "sim"
moduloBrancoAtivado = "sim" 
extensoesArquivoNegado = {"php"}
listaIpPermitido = {}
listaIpBloqueado = {}
negacaoCCAtivada = "sim"
taxaAtaqueCC = "500/100"    -- Limite de requisições por período (por exemplo, 500 requisições em 100 segundos)

Arquivo de Inicialização: init.lua

Este módulo carrega as regras e implementa a lógica central do firewall, incluindo funções para detecção de ataques e gerenciamento de banimentos.

require 'config'
local correspondencia = string.match
local encontrarNginx = ngx.re.find
local decodificarUrl = ngx.unescape_uri
local obterCabecalhos = ngx.req.get_headers
local estaAtivado = function (opcoes) return opcoes == "sim" and true or false end
caminhoLogs = diretorioLogs 
caminhoRegras = caminhoRegras
bloqueioUrlAtivado = estaAtivado(bloqueioUrlAtivado)
verificacaoPostAtivada = estaAtivado(verificacaoPostAtivada)
verificacaoCookieAtivada = estaAtivado(verificacaoCookieAtivada)
moduloBrancoAtivado = estaAtivado(moduloBrancoAtivado)
correcaoPathInfo = estaAtivado(correcaoPathInfo)
logAtivado = estaAtivado(logAtivado)
negacaoCCAtivada = estaAtivado(negacaoCCAtivada)
redirecionamentoAtivado = estaAtivado(redirecionamentoAtivado)

function extrairSubstring(str, delim)
    local reverso = string.reverse(str)
    local _, pos = string.find(reverso, delim)
    local comprimento = string.len(reverso) - pos + 1
    return string.sub(str, 1, comprimento)
end

function obterIpCliente()
    local enderecoIp = ngx.var.remote_addr 
    if ngx.var.HTTP_X_FORWARDED_FOR then
      enderecoIp = ngx.var.HTTP_X_FORWARDED_FOR
    end
    if enderecoIp == nil then
        enderecoIp = "desconhecido"
    end
    enderecoIp = extrairSubstring(enderecoIp, "[.]") .. "*"
    return enderecoIp
end

function obterIpReal()
    local enderecoIp = ngx.var.remote_addr 
    if ngx.var.HTTP_X_FORWARDED_FOR then    -- Verificação de IP real com CDN
      enderecoIp = ngx.var.HTTP_X_FORWARDED_FOR
    end
    if enderecoIp == nil then
        enderecoIp = "desconhecido"
    end
    return enderecoIp
end

function escreverLog(arquivoLog, mensagem)
    local descritor = io.open(arquivoLog, "ab")
    if descritor == nil then return end
    descritor:write(mensagem)
    descritor:flush()
    descritor:close()
end

function registrarEvento(metodo, url, dados, etiquetaRegra)
    if logAtivado then
        local ipReal = obterIpReal()
        local agenteUsuario = ngx.var.http_user_agent
        local nomeServidor = ngx.var.server_name
        local horaAtual = ngx.localtime()
        if agenteUsuario then
            linha = ipReal.." ["..horaAtual.."] \""..metodo.." "..nomeServidor..url.."\" \""..dados.."\"  \""..agenteUsuario.."\" \""..etiquetaRegra.."\"\n"
        else
            linha = ipReal.." ["..horaAtual.."] \""..metodo.." "..nomeServidor..url.."\" \""..dados.."\" - \""..etiquetaRegra.."\"\n"
        end
        local nomeArquivo = caminhoLogs..'/'..nomeServidor.."_"..ngx.today().."_seguranca.log"
        escreverLog(nomeArquivo, linha)
    end
end

function carregarRegras(nomeArquivo)
    local arquivo = io.open(caminhoRegras..'/'..nomeArquivo, "r")
    if arquivo == nil then
        return
    end
    local tabela = {}
    for linha in arquivo:lines() do
        table.insert(tabela, linha)
    end
    arquivo:close()
    return tabela
end

function acumularPenalidade(pontuacao)
    local chaveToken = obterIpCliente() .. "_firewall"
    local cacheCompartilhado = ngx.shared.limit
    local contagem, _ = cacheCompartilhado:get(chaveToken)
    if contagem then
        cacheCompartilhado:set(chaveToken, contagem + pontuacao, 3600)  -- Aumentar pontuação com validade de 1 hora
    else
        cacheCompartilhado:set(chaveToken, pontuacao, 3600)
    end
end

function obterContagemPenalidades()
    local chaveToken = obterIpCliente() .. "_firewall"
    local cacheCompartilhado = ngx.shared.limit
    local contagem, _ = cacheCompartilhado:get(chaveToken)
    if contagem then
        return contagem
    else 
        return 0
    end
end

function verificarBanimento()
    local penalidades = obterContagemPenalidades()
    if penalidades >= 100 then        -- Limite de pontuação para banimento
        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

regrasUrl = carregarRegras('url')
regrasArgumentos = carregarRegras('args')
regrasAgenteUsuario = carregarRegras('user-agent')
regrasUrlPermitida = carregarRegras('whiteurl')
regrasPost = carregarRegras('post')
regrasCookie = carregarRegras('cookie')
htmlResposta = carregarRegras('returnhtml')

function enviarHtmlProibido()
    acumularPenalidade(15)      -- Penalidade por ataque malicioso
    if redirecionamentoAtivado then
        ngx.header.content_type = "text/html;charset=UTF-8"
        ngx.status = ngx.HTTP_FORBIDDEN
        ngx.say(htmlResposta)
        ngx.exit(ngx.status)
    end
end

function ehUrlPermitida()
    if moduloBrancoAtivado then
        if regrasUrlPermitida ~= nil then
            for _, regra in pairs(regrasUrlPermitida) do
                if encontrarNginx(ngx.var.uri, regra, "isjo") then
                    return true 
                end
            end
        end
    end
    return false
end

function verificarExtensaoArquivo(extensao)
    local conjuntoExtensoes = criarConjunto(extensoesArquivoNegado)
    extensao = string.lower(extensao)
    if extensao then
        for regra in pairs(conjuntoExtensoes) do
            if ngx.re.match(extensao, regra, "isjo") then
                registrarEvento('POST', ngx.var.request_uri, "-", "ataque de arquivo com extensão "..extensao)
                enviarHtmlProibido()
            end
        end
    end
    return false
end

function criarConjunto(lista)
    local conjunto = {}
    for _, item in ipairs(lista) do conjunto[item] = true end
    return conjunto
end

function verificarArgumentos()
    for _, regra in pairs(regrasArgumentos) do
        local argumentos = ngx.req.get_uri_args()
        for chave, valor in pairs(argumentos) do
            if type(valor) == 'table' then
                local concatenado = {}
                for k, v in pairs(valor) do
                    if v == true then
                        v = ""
                    end
                    table.insert(concatenado, v)
                end
                dados = table.concat(concatenado, " ")
            else
                dados = valor
            end
            if dados and type(dados) ~= "boolean" and regra ~= "" and encontrarNginx(decodificarUrl(dados), regra, "isjo") then
                registrarEvento('GET', ngx.var.request_uri, "-", regra)
                enviarHtmlProibido()
                return true
            end
        end
    end
    return false
end

function verificarUrl()
    if bloqueioUrlAtivado then
        for _, regra in pairs(regrasUrl) do
            if regra ~= "" and encontrarNginx(ngx.var.request_uri, regra, "isjo") then
                registrarEvento('GET', ngx.var.request_uri, "-", regra)
                enviarHtmlProibido()
                return true
            end
        end
    end
    return false
end

function verificarAgenteUsuario()
    local agente = ngx.var.http_user_agent
    if agente ~= nil then
        for _, regra in pairs(regrasAgenteUsuario) do
            if regra ~= "" and encontrarNginx(agente, regra, "isjo") then
                registrarEvento('UA', ngx.var.request_uri, "-", regra)
                enviarHtmlProibido()
                return true
            end
        end
    end
    return false
end

function verificarCorpo(dados)
    for _, regra in pairs(regrasPost) do
        if regra ~= "" and dados ~= "" and encontrarNginx(decodificarUrl(dados), regra, "isjo") then
            registrarEvento('POST', ngx.var.request_uri, dados, regra)
            enviarHtmlProibido()
            return true
        end
    end
    return false
end

function verificarCookie()
    local cookie = ngx.var.http_cookie
    if verificacaoCookieAtivada and cookie then
        for _, regra in pairs(regrasCookie) do
            if regra ~= "" and encontrarNginx(cookie, regra, "isjo") then
                registrarEvento('Cookie', ngx.var.request_uri, "-", regra)
                enviarHtmlProibido()
                return true
            end
        end
    end
    return false
end

function negarAtaqueCC()
    if negacaoCCAtivada then
        limiteCC = tonumber(string.match(taxaAtaqueCC, '(.*)/'))
        periodoCC = tonumber(string.match(taxaAtaqueCC, '/(.*)'))
        local chaveToken = obterIpReal()
        local cacheCompartilhado = ngx.shared.limit
        local contagem, _ = cacheCompartilhado:get(chaveToken)
        if contagem then
            if contagem > limiteCC then
                cacheCompartilhado:incr(chaveToken, 1)
                acumularPenalidade(contagem - limiteCC)  -- Penalidade por ataque CC
                ngx.header.content_type = "text/html"
                ngx.status = ngx.HTTP_FORBIDDEN
                ngx.say("Limite de requisições excedido. Aguarde "..limiteCC.." segundos.")
                ngx.exit(ngx.status)
                return true
            else
                cacheCompartilhado:incr(chaveToken, 1)
            end
        else
            cacheCompartilhado:set(chaveToken, 1, periodoCC)
        end
    end
    return false
end

function obterDelimitadorMultipart()
    local cabecalho = obterCabecalhos()["content-type"]
    if not cabecalho then
        return nil
    end
    if type(cabecalho) == "table" then
        cabecalho = cabecalho[1]
    end
    local delimitador = correspondencia(cabecalho, ";%s*boundary=\"([^\"]+)\"")
    if delimitador then
        return delimitador
    end
    return correspondencia(cabecalho, ";%s*boundary=([^\",;]+)")
end

function ehIpPermitido()
    if next(listaIpPermitido) ~= nil then
        for _, ip in pairs(listaIpPermitido) do
            if obterIpCliente() == ip then
                return true
            end
        end
    end
    return false
end

function ehIpBloqueado()
    if next(listaIpBloqueado) ~= nil then
        for _, ip in pairs(listaIpBloqueado) do
            if obterIpCliente() == ip then
                ngx.exit(444)
                return true
            end
        end
    end
    return false
end

Arquivo de Execução: waf.lua

Este módulo orquestra as verificações do firewalll, processando requisições HTTP e aplicando as regras de segurança.

local comprimentoConteudo = tonumber(ngx.req.get_headers()['content-length'])
local metodoHttp = ngx.req.get_method()
local correspondenciaNginx = ngx.re.match
if ehIpPermitido() then
elseif ehIpBloqueado() then
elseif ehUrlPermitida() then
elseif verificarBanimento() then
elseif negarAtaqueCC() then
elseif ngx.var.http_Acunetix_Aspect then
    ngx.exit(444)
elseif ngx.var.http_X_Scan_Memo then
    ngx.exit(444)
elseif verificarAgenteUsuario() then
elseif verificarUrl() then
elseif verificarArgumentos() then
elseif verificarCookie() then
elseif verificacaoPostAtivada then
    if metodoHttp == "POST" then   
        local delimitador = obterDelimitadorMultipart()
        if delimitador then
            local comprimento = string.len
            local soquete, erro = ngx.req.socket()
            if not soquete then
                return
            end
            ngx.req.init_body(128 * 1024)
            soquete:settimeout(0)
            local tamanhoConteudo = tonumber(ngx.req.get_headers()['content-length'])
            local tamanhoBloco = 4096
            if tamanhoConteudo < tamanhoBloco then
                tamanhoBloco = tamanhoConteudo
            end
            local bytesLidos = 0
            while bytesLidos < tamanhoConteudo do
                local dados, erro, parcial = soquete:receive(tamanhoBloco)
                dados = dados or parcial
                if not dados then
                    return
                end
                ngx.req.append_body(dados)
                if verificarCorpo(dados) then
                    return true
                end
                bytesLidos = bytesLidos + comprimento(dados)
                local correspondenciaExtensao = correspondenciaNginx(dados, [[Content-Disposition: form-data;(.+)filename="(.+)\\.(.*)"]], 'ijo')
                if correspondenciaExtensao then
                    verificarExtensaoArquivo(correspondenciaExtensao[3])
                    transferenciaArquivo = true
                else
                    if correspondenciaNginx(dados, "Content-Disposition:", 'isjo') then
                        transferenciaArquivo = false
                    end
                    if transferenciaArquivo == false then
                        if verificarCorpo(dados) then
                            return true
                        end
                    end
                end
                local restante = tamanhoConteudo - bytesLidos
                if restante < tamanhoBloco then
                    tamanhoBloco = restante
                end
            end
            ngx.req.finish_body()
        else
            ngx.req.read_body()
            local argumentosPost = ngx.req.get_post_args()
            if not argumentosPost then
                return
            end
            for chave, valor in pairs(argumentosPost) do
                if type(valor) == "table" then
                    if type(valor[1]) == "boolean" then
                        return
                    end
                    dadosConcatenados = table.concat(valor, ", ")
                else
                    dadosConcatenados = valor
                end
                if dadosConcatenados and type(dadosConcatenados) ~= "boolean" and verificarCorpo(dadosConcatenados) then
                    verificarCorpo(chave)
                end
            end
        end
    end
else
    return
end

Essas modificações proporcionam uma abordagem mais robusta para o firewall, permitindo o banimento dinâmico com base em métricas de ataque. É crucial testar extensivamente entes de implementar em ambientes de produção para evitar falsos positivos e garantir a estabilidade do servidor.

Tags: nginx Lua WAF BaoTa Panel IP Banning

Publicado em 6-30 04:04