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.