
De l'exploit à l'exécution de code : exploit artisanal pour CVE-2010-4221 (débordement de pile TELNET IAC dans ProFTPD), avec le parcours complet guidé par les échecs documenté
Un laboratoire entièrement reproductible et un exploit écrit à la main, basé sur des sockets brutes, pour
CVE-2010-4221 — le débordement de pile pré-authentification dans
pr_netio_telnet_gets() de ProFTPD — conçu comme exercice d'apprentissage
en recherche de vulnérabilités et développement d'exploits.
Chaque échec est documenté. Le chemin idéal est un mensonge ; les détours sont la leçon.
Ce dépôt est un artefact pédagogique. Il existe pour que les personnes qui ne peuvent pas se permettre un mentor ou une formation puissent apprendre comment un exploit de corruption mémoire naît réellement : d'un patch, à travers les échecs, jusqu'à une preuve de concept fonctionnelle dans un laboratoire qui vous appartient.
Le travail Red Team — la sécurité offensive réelle et professionnelle — se définit par un seul mot : autorisation. Tout ce qu'un professionnel fait se déroule dans le cadre d'un accord écrit : un document signé de règles d'engagement qui nomme le périmètre, les cibles, les techniques autorisées, la fenêtre temporelle et les personnes qui l'ont approuvé. Sans ce papier, les mêmes frappes clavier ne sont pas une profession — ce sont un crime dans pratiquement toutes les juridictions de la planète.
Voici donc le contrat pour ce dépôt, non négociable :
Le métier vaut la peine d'être appris. Le métier ne vaut quelque chose qu'avec la discipline qui l'accompagne.
ProFTPD parle les séquences d'échappement TELNET sur le canal de contrôle FTP. En
TELNET, 0xFF (IAC, « Interpret As Command ») est l'octet d'échappement ; un
0xFF littéral est envoyé comme 0xFF 0xFF.
pr_netio_telnet_gets() copie les octets du client dans un buffer de pile
(char buf[PR_DEFAULT_CMD_BUFSZ+1] de pr_cmd_read, 4104 octets avec
MAXPATHLEN=4096 de glibc), en suivant l'espace restant dans buflen — un
size_t, non signé.
Le chemin vulnérable (1.3.3a, netio.c) :
case TELNET_IAC:
switch (cp) {
...
default:
*bp++ = TELNET_IAC; // écriture #1
buflen--; // décrément #1
telnet_mode = 0;
break;
}
break;
...
*bp++ = cp; // écriture #2
buflen--; // décrément #2 <-- aucun contrôle entre les deux
Deux écritures, deux décréments, aucun contrôle de zéro entre eux. Lorsque buflen
vaut exactement 1, la paire le décrémente à 0, puis le fait passer sous zéro jusqu'à
SIZE_MAX (18 quintillions). La boucle croit désormais que le buffer est infini
et continue d'écrire des octets contrôlés par l'attaquant vers le haut de la pile — par-dessus
les registres sauvegardés, le RBP sauvegardé et l'adresse de retour.
Pré-authentification. La fonction s'exécute avant que USER/PASS ne soient jamais traités.
Le correctif (commit
3cc69b8388,
« Bug#3521 - Telnet IAC processing stack overflow », publié dans la 1.3.3c)
fait douze lignes. Toute la frontière de sécurité est :
if (buflen == 0) {
break;
}
Voir patch.diff. Lire le patch vous indique où se trouvait
la blessure — c'est cela, la compétence.
Le Dockerfile compile ProFTPD 1.3.3a à partir des sources historiques du snapshot Debian, délibérément non sécurisé (voilà à quoi ressemblait 2010) :
-fno-stack-protector — pas de canari-z execstack — pile exécutable (pas de NX)-no-pie — adresses binaires fixesgdb, qui désactive l'ASLR par défaut → pile déterministedocker 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
Sortie attendue :
[S] 220 ProFTPD 1.3.3a Server (lab-iac) ...
[S] THE SERVER SAID: b'PWNED!!PWNED!!'
(Le shellcode écrit sur les descripteurs 0, 1 et 2 car nous ne voulions pas dépendre de la connaissance de celui qui porte le canal de contrôle — deux d'entre eux répondent.)
"SITE " + NOP sled + shellcode + [inondation \xff\xff] + padding + [ret] + "\n"
^^^^^^^^^^^^^^^^^^^^ ^^^^
le shellcode vit DANS le buffer la queue du débordement ne
de commande — la région que personne délivre QU'UNE adresse
ne touche
buf[4102] est touché
(NUL de troncature). Tout ce qui se trouve sous les variables locales vivantes du cadre est calme.buflen au passage sous zéro (voir « Le voyage »
pour le problème de parité).pr_cmd_read atteint return 0 après l'analyse, le
CPU atterrit dans le sled et glisse dans le shellcode.Cette architecture inversée — payload d'abord, inondation ensuite, adresse en dernier —
a été validée contre le module Metasploit canonique
(proftp_telnet_iac), qui utilise la même disposition. Leurs cibles avaient NX,
ils avaient donc besoin d'une chaîne ROP avec un « quadruple déréférencement » du pointeur res ;
notre laboratoire a une pile exécutable, donc un simple retour direct suffit.
L'exploit final fait 60 lignes. Ce qu'il a coûté :
Inondation \xff aveugle → rien. Le serveur a poliment fermé la
session. Cause racine : buflen commence à 4102 (PAIR) et chaque paire IAC
décrémente de 2 — il atterrit proprement sur 0, jamais sur 1. Le passage
sous zéro nécessite une parité brisée. Leçon : lire la machine à états vaut mieux que d'arroser.
Mauvaise taille de buffer. La première tentative calibrée supposait un buffer de 1024 octets.
Le vrai fait MAXPATHLEN+8 = 4104 sous Linux/glibc. L'inondation
s'est arrêtée 3 Ko avant la cible. Leçon : mesurez la cible, ne supposez pas la cible.
Premier SIGSEGV. Le motif cyclique de Bruijn (Aa0Aa1...) a placé l'
emplacement de retour à buf+4152, validé deux fois (calcul du cadre + décalage du motif).
Leçon : le motif cyclique est un mètre ruban, pas un exploit.
Contrôle de RIP. Régler l'emplacement à 0x4141414141414141 a fait planter
l'instruction ret elle-même — x86-64 refuse les adresses non canoniques, et
le défaut atterrit sur ret, avec notre valeur en attente dans la backtrace.
Leçon : un crash sur ret avec votre valeur dans le cadre = contrôle.
Shellcode au-dessus de l'emplacement ret → écrasé. 8 octets écrasés par un
pointeur de tas (0x4d7838 — identifié plus tard comme l'allocation du pool cmd_rec).
Les cadres de pile au-dessus de l'emplacement appartiennent à des fonctions qui continuent
de travailler entre l'atterrissage et le détournement. Leçon : le débordement n'est pas la
dernière écriture ; le programme continue de vivre sur la pile que vous venez de vandaliser.
« Zone morte » sous l'emplacement → également griffonnée. Les propres variables
locales de pr_cmd_read (cmd, buflen, cp) vivent juste là et continuent d'être
stockées pendant l'analyse.
Analyse médico-légale par point d'arrêt matériel. watch *(long*)ADDR dans gdb a transformé
le mystère en caméra : chaque écriture à l'adresse écrasée, avec
backtrace, dans l'ordre. Leçon : quand la question est « qui a écrit cette
mémoire ? », la réponse est à une commande gdb.
Lisez la référence, puis comprenez-la. Le module canonique a confirmé l'architecture inversée. Lire un autre exploit après avoir construit votre propre modèle mental est une étude ; avant, c'est de la copie.
size_t ne devient jamais négatif — il devient gigantesque. Le passage sous zéro
d'un compteur d'espace restant est un débordement de pile avec des étapes supplémentaires.\x0a
(termine la lecture) et survit à \xff (échappement TELNET) par conception.argv[0] (binaire de l'arborescence de compilation vs binaire installé) a décalé toute la
pile de 0x40 et invalidé silencieusement un exploit parfait.modules/exploits/linux/ftp/proftp_telnet_iac.rb
(rapid7/metasploit-framework)Construit par diegslva, apprenant en public — de « jamais écrit d'exploit » à RCE pré-authentification avec shellcode fait main, en une journée documentée. Si ce dépôt vous a appris quelque chose, transmettez-le.