Voltar as noticias
Meu agente de pentest de IA relatou 23 shells root. Na verdade, não conseguiu nenhum.
Casos de UsoMediaEN

Meu agente de pentest de IA relatou 23 shells root. Na verdade, não conseguiu nenhum.

Dev.to - MCP·23 de julho de 2026

Eu estive construindo um agente de teste de penetração autônomo — um LLM dirigindo ferramentas reais (nmap, masscan, hydra, Metasploit, searchsploit) em um loop: reconhecer um alvo, escolher um exploit, dispará-lo, decidir se funcionou, seguir em frente. Tudo abaixo roda contra uma VM Metasploitable deliberadamente vulnerável em um laboratório isolado. Nada aqui é uma técnica para atacar sistemas que você não possui; é uma história sobre fazer um agente de IA dizer a verdade sobre o que fez.

Porque a primeira lição difícil foi esta: um agente LLM irá felizmente relatar sucesso que nunca alcançou. Uma das minhas primeiras execuções produziu um relatório bonito — "23 de 23 portas violadas, root em todas." O número real era zero. Não uma única shell. O agente havia alucinado uma total comprometimento e escreveu isso com confiança.

Este post é o arco daquela mentira até um motor que agora gera três shells root reais, os relata honestamente e se recusa a reivindicar qualquer coisa que não possa provar.

Por que mentiu: "sucesso" era uma correspondência de string

O validador — o componente que decide se um exploit funcionou — estava fazendo algo que parece razoável e é catastrófico:


python
# a verificação original "funcionou?"
if "login:" in output or "shellcodes" in output.lower():
    return "confirmed"
Duas modos de falha independentes alimentaram isso:

output do searchsploit. Quando você pesquisa no Exploit-DB, a ferramenta imprime um cabeçalho: Exploits: ... / Shellcodes: .... Cada busca — pura inteligência, sem exploração alguma — continha a palavra "Shellcodes". Então cada porta que o agente olhou foi marcada como violada.
Texto do banner. Qualquer serviço que ecoasse login: contava como uma shell.
O resultado foi um relatório que era 100% adjetivos e 0% evidências. "Confirmado." "Alta confiança." "Violado." Todas correspondências de string, nenhuma delas uma shell.

A correção: evidências, não vibrações
Eu substituí a heurística por breach_confirmed() — uma função que só retorna verdadeiro quando a saída contém prova real de execução de código:

_SHELL_EVIDENCE = (
    re.compile(r"uid=\d+\([a-z]+\).*gid=\d+"),   # saída do id(1)
    re.compile(r"root@[\w.-]+:[~/]"),            # um prompt root
    # ...marcadores que uma shell real produz, não aqueles que um banner pode falsificar
)

def breach_confirmed(output: str) -> bool:
    return any(p.search(output) for p in _SHELL_EVIDENCE)
Um exploit curado que gera o backdoor do vsftpd 2.3.4 e executa id produz
uid=0(root) gid=0(root). Isso é uma violação. Uma tabela do searchsploit não é. A mesma execução que costumava afirmar 23/23 agora diz: 3 confirmados, 20 corretamente não confirmados. Os 3 são reais. Essa honestidade — estar disposto a dizer "Eu tentei e não funcionou" — é a propriedade mais importante de todo o sistema.

Por que não conseguiu gerar nada: os bugs de encanamento entediantes
Uma vez que o agente parou de mentir, ele expôs o quão pouco estava realmente funcionando. Os culpados não eram inteligentes — eram do tipo sem glamour, silencioso, que nunca lança uma pilha de rastreamento onde você está olhando:

1. A ferramenta que o modelo nunca conseguiu chamar. O modelo continuava chamando
searchsploit com {"query": "vsftpd"}. O esquema da ferramenta exigia {"keyword": "..."}. A camada MCP rejeitou rigidamente cada chamada como malformada
antes de ser executada — 20 vezes por execução, cada uma um evento silencioso de 0 segundos. Correção: aceitar
keyword | query | search e descartar a exigência rígida.

2. Códigos ANSI envenenando nomes de módulos. msfconsole destaca seu termo de busca com códigos de escape de cor: exploit/unix/\x1b[45mftp\x1b[0m/vsftpd_234. Meu parser capturou esses bytes como parte do caminho do módulo, então cada módulo selecionado falhou ao carregar. O Metasploit bloqueado estava com uma taxa de sucesso de 0% por uma razão que era invisível nos logs em texto simples. Correção: strip_ansi() antes de analisar.

3. Strings de versão disfarçando-se como produtos. nmap relata serviços RPC versão-primeiro: 2 (RPC #100000). Meu parser de impressão digital pegou 2 como o nome do produto e alimentou "2" ao Metasploit como um termo de busca. Correção: uma proteção de dígito inicial para que uma versão nunca possa ser confundida com um produto.

4. Um atraso de 5 minutos devido a uma constante de tempo limite. call_model() reutilizou o tempo limite da ferramenta de 300 segundos, então uma única solicitação de LLM local travada atrasou todo o engajamento por cinco minutos. Correção: um tempo limite de modelo separado de 90 segundos.

5. Um sandbox que consumia sua própria saída. O sandbox de execução de código levantou TimeoutExpired sem imprimir o contrato EXIT que o parser esperava, então um exploit lento apareceu como um "Sem marcador EXIT" opaco após queimar todo o relógio. Correção: capturar o tempo limite, emitir um EXIT limpo 124 com saída parcial, descartar o relógio padrão para 60s.

Nenhum desses é interessante. Todos eles eram a diferença entre "funciona" e "silenciosamente não faz nada." Coletivamente, eles estavam prejudicando o agente enquanto os logs pareciam bons.

Por que disparou lixo: relevância vs. relevância
Com o encanamento corrigido, o agente começou a alcançar o Metasploit — e imediatamente fez algo absurdo. Veja o que ele escolheu:

Porta    Serviço (impressão digital)   Módulo que escolheu Classificação
23  Linux telnetd   exploit/linux/http/asuswrt_lan_rce  excelente
513 rlogin (login)  exploit/windows/misc/ais_esel_server_rce    excelente
5900    VNC exploit/linux/misc/igel_command_injection   excelente
Um RCE de roteador contra telnet. Um exploit do Windows contra um serviço rlogin Linux. Cada um "excelente" classificado, cada um uma bobagem.

A razão: msfconsole search <termo> corresponde seu termo contra descrições de módulos, não apenas seus alvos. Uma impressão digital difusa de uma palavra como "login" puxa qualquer módulo cuja descrição mencione login — e o Metasploit classifica pela confiabilidade do exploit, não pela relevância para seu alvo. Portanto, a melhor correspondência classificada para uma má consulta é confiantemente, precisamente errada.

O filtro de relevância
A correção é um filtro que roda antes da classificação: mantenha um candidato apenas se um token de serviço real da impressão digital aparecer no caminho do módulo — depois de descartar as palavras genéricas da árvore que correspondem a tudo.

_GENERIC = {"linux", "windows", "unix", "multi", "http", "misc",
            "scanner", "auxiliary", "exploit", ...}

def _relevance_tokens(terms: str) -> set:
    return {w for w in terms.lower().split()
            if len(w) >= 3 and not w[0].isdigit() and w not in _GENERIC}

def _is_relevant(module: str, tokens: set) -> bool:
    return any(tok in module for tok in tokens)   # o token deve estar no CAMINHO
asuswrt, ais_esel, igel — todos foram embora. x11_keyboard_exec para X11 e
vnc_keyboard_exec para VNC sobreviveram. Isso troca um pouco de recall por muita precisão, que é a troca certa quando o modo de falha é disparar exploits no alvo errado.

Um bug mais silencioso que o filtro revelou
Enquanto eu estava lá: módulos de serviços r (rsh/rlogin/rexec) vivem sob auxiliary/scanner/rservices/, não exploit/. Meu seletor tinha uma flag exploits_only=True que descartava silenciosamente toda a árvore auxiliar — então o módulo certo para rlogin nunca poderia ser escolhido, em qualquer alvo. Eu removi a flag e mudei para uma classificação em camadas: módulos exploit/ primeiro (eles geram shells), logins auxiliares como uma classificação de fallback. Sem codificação rígida por porta — isso se generaliza para qualquer serviço que o Metasploit tenha cobertura.

Indo para múltiplos agentes — sem reimportar a mentira
A próxima fase foi transformar um loop de agente único em um pipeline adequado: orquestrador → reconhecimento → atacante → validador → repórter. A armadilha: aqueles seis agentes já existiam como demonstrações desconectadas, e o antigo validador usava a mesma heurística de string que produziu "23/23." Conectá-los como estavam teria reimportado o pecado original.

Então a regra se tornou: um motor honesto, compartilhado por todos. Eu extraí a lógica comprovada — AgentMemory, plan_exploit_step, breach_confirmed — em um único exploitation_core.py. O validador de múltiplos agentes agora delega para breach_confirmed em vez de corresponder strings. O atacante executa apenas através de uma execute_fn injetada, controlada por humanos — nunca o cliente MCP sem controle que contorna o portão de aprovação do operador. Essa invariância é fixada por
Contexto Triplo Up

O artigo ilustra os desafios e soluções na implementação de agentes de IA para testes de penetração, destacando a importância da precisão nos relatórios. Empresas brasileiras podem se beneficiar ao entender como evitar 'alucinações' em relatórios de IA, garantindo resultados confiáveis em segurança cibernética.

Noticias relacionadas

Gostou do conteudo?

Receba toda semana as principais novidades sobre WebMCP.