
Dalla patch all'RCE: exploit costruito a mano per CVE-2010-4221 (stack overflow TELNET IAC in ProFTPD), con l'intero percorso guidato dagli errori documentato
Un laboratorio completamente riproducibile e un exploit scritto a mano con socket raw per
CVE-2010-4221 — l'overflow del buffer dello stack pre-autenticazione in
pr_netio_telnet_gets() di ProFTPD — creato come esercizio di apprendimento nella
ricerca di vulnerabilità e nello sviluppo di exploit.
Ogni fallimento è documentato. Il percorso felice è una bugia; le deviazioni sono la lezione.
Questo repository è un artefatto educativo. Esiste affinché le persone che non possono permettersi un mentore o una formazione possano imparare come nasce realmente un exploit di corruzione della memoria: da una patch, attraverso i fallimenti, fino a una prova di concetto funzionante all'interno di un laboratorio che possiedi.
Il lavoro Red Team — la sicurezza offensiva professionale e reale — è definito da una parola: autorizzazione. Tutto ciò che un professionista fa avviene all'interno di un accordo scritto: un documento firmato di Rules of Engagement che indica l'ambito, i target, le tecniche consentite, la finestra temporale e le persone che lo hanno approvato. Senza quel documento, gli stessi identici tasti non sono una professione — sono un reato in praticamente ogni giurisdizione sulla Terra.
Quindi ecco il contratto per questo repo, non negoziabile:
L'arte vale la pena di essere appresa. L'arte vale qualcosa solo con la disciplina che ne deriva.
ProFTPD parla le sequenze di escape TELNET sul canale di controllo FTP. In
TELNET, 0xFF (IAC, "Interpret As Command") è il byte di escape; un
0xFF letterale viene inviato come 0xFF 0xFF.
pr_netio_telnet_gets() copia i byte del client in un buffer dello stack
(char buf[PR_DEFAULT_CMD_BUFSZ+1] di pr_cmd_read, 4104 byte con
MAXPATHLEN=4096 di glibc), monitorando lo spazio rimanente in buflen — un
size_t, senza segno.
Il percorso vulnerabile (1.3.3a, netio.c):
case TELNET_IAC:
switch (cp) {
...
default:
*bp++ = TELNET_IAC; // scrittura #1
buflen--; // decremento #1
telnet_mode = 0;
break;
}
break;
...
*bp++ = cp; // scrittura #2
buflen--; // decremento #2 <-- nessun controllo in mezzo
Due scritture, due decrementi, nessun controllo dello zero tra di loro. Quando buflen
è esattamente 1, la coppia lo decrementa a 0, poi lo fa sottofluire a
SIZE_MAX (18 quintilioni). Il ciclo ora crede che il buffer sia infinito
e continua a scrivere byte controllati dall'attaccante lungo lo stack — sopra i registri
salvati, il RBP salvato e l'indirizzo di ritorno.
Pre-autenticazione. La funzione viene eseguita prima che USER/PASS vengano mai processati.
La correzione (commit
3cc69b8388,
"Bug#3521 - Telnet IAC processing stack overflow", rilasciata nella 1.3.3c)
è di dodici righe. L'intero confine di sicurezza è:
if (buflen == 0) {
break;
}
Vedi patch.diff. Leggere la patch ti dice dove si trovava
la ferita — questa è l'abilità.
Il Dockerfile compila ProFTPD 1.3.3a dal sorgente storico dello snapshot Debian, deliberatamente insicuro (è così che appariva il 2010):
-fno-stack-protector — nessun canary-z execstack — stack eseguibile (nessun NX)-no-pie — indirizzi binari fissigdb, che disabilita l'ASLR per impostazione predefinita → stack deterministicodocker 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
Output atteso:
[S] 220 ProFTPD 1.3.3a Server (lab-iac) ...
[S] THE SERVER SAID: b'PWNED!!PWNED!!'
(Il shellcode scrive sui fd 0, 1 e 2 perché non volevamo dipendere dal sapere quale trasporta il canale di controllo — due di loro rispondono.)
"SITE " + NOP sled + shellcode + [\xff\xff flood] + padding + [ret] + "\n"
^^^^^^^^^^^^^^^^^^^^ ^^^^
il shellcode vive DENTRO il comando la coda dell'overflow
buffer — la regione che nessuno tocca consegna UN SOLO indirizzo
buf[4102] viene toccato
(NUL di troncamento). Tutto sotto le variabili locali vive del frame è calmo.buflen al sottoflusso (vedi "Il viaggio"
per il problema della parità).pr_cmd_read raggiunge return 0 dopo il parsing, la
CPU atterra nello sled e scivola nel shellcode.Questa architettura invertita — payload prima, flood dopo, indirizzo per ultimo —
è stata validata contro il modulo Metasploit canonico
(proftp_telnet_iac), che usa lo stesso layout. I loro target avevano NX,
quindi avevano bisogno di una catena ROP con un "deref quadruplo" del puntatore res;
il nostro laboratorio ha uno stack eseguibile, quindi un singolo ritorno diretto è sufficiente.
L'exploit finale è di 60 righe. Cosa è costato:
Flood \xff cieco → niente. Il server ha chiuso educatamente la
sessione. Causa principale: buflen parte da 4102 (PARI) e ogni coppia IAC
decrementa di 2 — atterra su 0 pulitamente, mai su 1. Il sottoflusso
richiede che la parità venga rotta. Lezione: leggere la macchina a stati batte lo spraying.
Dimensione del buffer sbagliata. Il primo tentativo calibrato assumeva un buffer di 1024 byte.
Quello reale è MAXPATHLEN+8 = 4104 su Linux/glibc. Il flood
si è fermato 3KB prima del target. Lezione: misura il target, non
assumere il target.
Primo SIGSEGV. Il pattern ciclico de Bruijn (Aa0Aa1...) ha posizionato lo
slot di ritorno a buf+4152, convalidato due volte (calcolo del frame + offset del pattern).
Lezione: il pattern ciclico è un metro di misura, non un
exploit.
Controllo di RIP. Impostare lo slot a 0x4141414141414141 ha mandato in crash
l'istruzione ret stessa — x86-64 rifiuta indirizzi non canonici, e
il fault atterra su ret, con il nostro valore in attesa nel backtrace.
Lezione: un crash su ret con il tuo valore nel frame = controllo.
Shellcode sopra lo slot ret → sovrascritto. 8 byte sovrascritti da un
puntatore heap (0x4d7838 — successivamente identificato come l'allocazione del pool cmd_rec).
I frame dello stack sopra lo slot appartengono a funzioni che continuano
a lavorare tra l'atterraggio e il dirottamento. Lezione: l'overflow non è l'ultima
scrittura; il programma continua a vivere sullo stack che hai appena vandalizzato.
"Zona morta" sotto lo slot → anche scarabocchiata. Le variabili locali
di pr_cmd_read (cmd, buflen, cp) vivono proprio lì e continuano a essere
salvate durante il parsing.
Analisi forense con watchpoint hardware. watch *(long*)ADDR in gdb ha trasformato
il mistero in una telecamera: ogni scrittura all'indirizzo sovrascritto, con
backtrace, in ordine. Lezione: quando la domanda è "chi ha scritto questa
memoria?", la risposta è a un comando gdb di distanza.
Leggi il riferimento, poi capiscilo. Il modulo canonico ha confermato l'architettura invertita. Leggere un altro exploit dopo aver costruito il proprio modello mentale è studio; prima, è copiare.
size_t non diventa mai negativo — diventa gigantesco. Il sottoflusso intero
in un contatore di spazio rimanente è un overflow dello stack con passaggi extra.\x0a
(termina la lettura) e sopravvive a \xff (escape TELNET) per progettazione.argv[0] (binario dell'albero di build vs binario installato) ha spostato l'intero
stack di 0x40 e invalidato silenziosamente un exploit perfetto.modules/exploits/linux/ftp/proftp_telnet_iac.rb
(rapid7/metasploit-framework)Creato da diegslva, imparando in pubblico — da "non ho mai scritto un exploit" a RCE pre-autenticazione con shellcode fatto a mano, in un giorno documentato. Se questo repo ti ha insegnato qualcosa, paga in avanti.