
Da correção ao RCE: exploit construído à mão para CVE-2010-4221 (estouro de pilha TELNET IAC no ProFTPD), com toda a jornada orientada por falhas documentada
Um laboratório totalmente reproduzível e um exploit escrito à mão, usando raw sockets, para
CVE-2010-4221 — o estouro de buffer na pilha pré-autenticação em
pr_netio_telnet_gets() do ProFTPD — construído como exercício de aprendizado em
pesquisa de vulnerabilidades e desenvolvimento de exploits.
Cada falha está documentada. O caminho feliz é uma mentira; os desvios são a lição.
Este repositório é um artefato educacional. Ele existe para que pessoas que não podem pagar por um mentor ou treinamento possam aprender como um exploit de corrupção de memória realmente nasce: de um patch, através de falhas, até uma prova de conceito funcional dentro de um laboratório que você possui.
Trabalho de Red Team — segurança ofensiva real e profissional — é definido por uma palavra: autorização. Tudo o que um profissional faz acontece dentro de um acordo escrito: um documento de Regras de Engajamento assinado que nomeia o escopo, os alvos, as técnicas permitidas, a janela de tempo e as pessoas que o aprovaram. Sem esse papel, exatamente as mesmas teclas digitadas não são uma profissão — são um crime em praticamente todas as jurisdições da Terra.
Então aqui está o contrato para este repositório, inegociável:
O ofício vale a pena ser aprendido. O ofício só vale algo com a disciplina que o acompanha.
O ProFTPD fala sequências de escape TELNET no canal de controle FTP. Em
TELNET, 0xFF (IAC, "Interpret As Command") é o byte de escape; um
0xFF literal é enviado como 0xFF 0xFF.
pr_netio_telnet_gets() copia bytes do cliente para um buffer na pilha
(char buf[PR_DEFAULT_CMD_BUFSZ+1] do pr_cmd_read, 4104 bytes com
MAXPATHLEN=4096 do glibc), rastreando o espaço restante em buflen — um
size_t, sem sinal.
O caminho vulnerável (1.3.3a, netio.c):
case TELNET_IAC:
switch (cp) {
...
default:
*bp++ = TELNET_IAC; // escrita #1
buflen--; // decremento #1
telnet_mode = 0;
break;
}
break;
...
*bp++ = cp; // escrita #2
buflen--; // decremento #2 <-- nenhuma verificação entre eles
Duas escritas, dois decrementos, nenhuma verificação de zero entre eles. Quando buflen
é exatamente 1, o par o decrementa para 0, então sofre underflow para
SIZE_MAX (18 quintilhões). O loop agora acredita que o buffer é infinito
e continua escrevendo bytes controlados pelo atacante pilha acima — sobre registradores
salvos, RBP salvo e o endereço de retorno.
Pré-autenticação. A função é executada antes de USER/PASS serem sequer processados.
A correção (commit
3cc69b8388,
"Bug#3521 - Telnet IAC processing stack overflow", lançado em 1.3.3c)
tem doze linhas. Toda a fronteira de segurança é:
if (buflen == 0) {
break;
}
Veja patch.diff. Ler o patch lhe diz onde estava a
ferida — essa é a habilidade.
O Dockerfile compila o ProFTPD 1.3.3a a partir do código-fonte do snapshot histórico do Debian, deliberadamente inseguro (é assim que 2010 parecia):
-fno-stack-protector — sem canário-z execstack — pilha executável (sem NX)-no-pie — endereços binários fixosgdb, que desativa ASLR por padrão → pilha determinísticadocker build -t proftpd-133a .
docker rm -f lab133 2>/dev/null
docker run -d --name lab133 --cap-add SYS_PTRACE \
--security-opt seccomp=unconfined -p 127.0.0.1:2122:21 \
proftpd-133a sh -c 'gdb -batch -ex "set follow-fork-mode child" \
-ex "run" -ex "continue" --args /usr/local/sbin/proftpd -n -d1 \
> /tmp/gdb.txt 2>&1; sleep 600'
python3 exploit.py
Saída esperada:
[S] 220 ProFTPD 1.3.3a Server (lab-iac) ...
[S] THE SERVER SAID: b'PWNED!!PWNED!!'
(O shellcode escreve nos fds 0, 1 e 2 porque não queríamos depender de saber qual deles carrega o canal de controle — dois deles respondem.)
"SITE " + trenó de NOPs + shellcode + [inundação \xff\xff] + preenchimento + [ret] + "\n"
^^^^^^^^^^^^^^^^^^^^ ^^^^
o shellcode vive DENTRO do buffer a cauda do estouro apenas
do comando — a região que ninguém toca entrega UM endereço
buf[4102] é tocado
(NUL de truncamento). Tudo abaixo das variáveis locais vivas do frame está calmo.buflen ao underflow (veja "A jornada"
para o problema de paridade).pr_cmd_read atinge return 0 após o parsing, a
CPU aterrissa no trenó e desliza para dentro do shellcode.Esta arquitetura invertida — payload primeiro, inundação em segundo, endereço por último —
foi validada contra o módulo canônico do Metasploit
(proftp_telnet_iac), que usa o mesmo layout. Os alvos deles tinham NX,
então precisavam de uma cadeia ROP com um "quádruplo deref" do ponteiro res;
nosso laboratório tem pilha executável, então um único retorno direto é suficiente.
O exploit final tem 60 linhas. O que custou:
Inundação \xff cega → nada. O servidor educadamente fechou a
sessão. Causa raiz: buflen começa em 4102 (PAR) e cada par IAC
decrementa em 2 — ele aterrissa em 0 limpo, nunca em 1. O underflow
precisa de paridade quebrada. Lição: ler a máquina de estados vence pulverizar.
Tamanho de buffer errado. A primeira tentativa calibrada assumiu um buffer de 1024 bytes.
O real é MAXPATHLEN+8 = 4104 no Linux/glibc. A inundação
parou 3KB antes do alvo. Lição: meça o alvo, não
assuma o alvo.
Primeiro SIGSEGV. Padrão cíclico de Bruijn (Aa0Aa1...) colocou o
slot de retorno em buf+4152, validado de forma cruzada duas vezes (matemática do frame + deslocamento do padrão). Lição: o padrão cíclico é uma fita métrica, não um
exploit.
Controle de RIP. Definir o slot para 0x4141414141414141 travou a
própria instrução ret — x86-64 recusa endereços não canônicos, e
a falha aterrissa em ret, com nosso valor esperando no backtrace.
Lição: um crash em ret com seu valor no frame = controle.
Shellcode acima do slot de ret → corrompido. 8 bytes sobrescritos por um
ponteiro de heap (0x4d7838 — posteriormente identificado como a alocação do pool
cmd_rec). Os frames de pilha acima do slot pertencem a funções ainda
trabalhando entre o pouso e o sequestro. Lição: o estouro não é a
última escrita; o programa continua vivendo na pilha que você acabou de vandalizar.
"Zona morta" abaixo do slot → também rabiscada. As próprias variáveis locais
de pr_cmd_read (cmd, buflen, cp) vivem bem ali e continuam sendo
armazenadas durante o parsing.
Investigação forense com watchpoint de hardware. watch *(long*)ADDR no gdb transformou
o mistério em uma câmera: cada escrita no endereço corrompido, com
backtrace, em ordem. Lição: quando a pergunta é "quem escreveu esta
memória?", a resposta está a um comando do gdb de distância.
Leia a referência, depois entenda-a. O módulo canônico confirmou a arquitetura invertida. Ler outro exploit depois de construir seu próprio modelo mental é estudo; antes, é cópia.
size_t nunca fica negativo — ele fica gigantesco. Underflow inteiro
em um contador de espaço restante é um estouro de pilha com passos extras.\x0a
(termina a leitura) e sobrevive a \xff (escape TELNET) por design.argv[0] (binário da árvore de build vs binário instalado) deslocou toda a
pilha em 0x40 e invalidou silenciosamente um exploit perfeito.modules/exploits/linux/ftp/proftp_telnet_iac.rb
(rapid7/metasploit-framework)Construído por diegslva, aprendendo em público — de "nunca escrevi um exploit" a RCE pré-autenticação com shellcode feito à mão, em um dia documentado. Se este repositório lhe ensinou algo, retribua.