利用空字节写入漏洞攻破Synology DiskStation
在Synology DS1823xs+ NAS上实现root权限的远程代码执行
发布日期:2025年4月23日
去年10月,我们参加了Pwn2Own Ireland 2024并成功利用了Synology DiskStation DS1823xs+,获得了root权限的远程代码执行能力。该问题已被修复并分配了CVE-2024-10442。
DiskStation是Synology推出的一系列流行的NAS(网络附加存储)产品。过去在Pwn2Own比赛中它曾被成功利用几次,不过在去年的Pwn2Own Toronto 2023比赛中未被触及。
而Ireland 2024见证了三个成功的参赛作品,都使用了独特的漏洞。
本文将详细介绍我们研究Synology DiskStation并为比赛编写漏洞利用程序的经历。
准备攻击Synology DiskStation,Pwn2Own Ireland 2024
审查Synology软件包
如前所述,过去一两年的Pwn2Own没有针对Synology DiskStation的参赛作品。2024年,ZDI决定将一些非默认但由Synology开发的第一方软件包纳入比赛范围:
对于Synology DiskStation目标,将安装以下软件包并纳入比赛范围:
- MailPlus
- Drive
- Virtual Machine Manager
- Snapshot Replication
- Surveillance Station
- Photos
“软件包"是可选的附加应用程序/服务等,可以通过DiskStation Manager中的Synology Package Center轻松安装在设备上。
对我们来说,这意味着更多的攻击面,而且由于这是这些软件包首次被纳入范围,我们认为有很好的机会找到一些相对较浅的漏洞,因为这些软件包可能没有经过太多的安全审查。事实证明这非常正确。
我们查看的第一个软件包是Virtual Machine Manager,我们直接从物理DiskStation上的内置Package Center安装了它。
服务发现
然后我们可以通过测试设备上的SSH shell使用netstat枚举任何新的网络监听器。这揭示了一些仅限本地主机的服务,除了一个绑定到所有接口的服务,运行以下命令(以root身份):
|
|
这个监听器实际上是Replication Service的一部分,这是一个独立的软件包,是Virtual Machine Manager的依赖项(也是Snapshot Replication的依赖项)。考虑到高权限级别和与此服务通信的便利性,我们对此产生了兴趣。
分析二进制文件
下一步是检查二进制文件。由于我们在真实设备上安装了该服务,我们能够通过SSH提取文件。
或者,可以直接从Synology下载DSM(核心操作系统)和软件包的软件下载。然后可以使用提取工具解析自定义的Synology存档并输出软件包、固件映像或更新的内容。请注意,此特定工具是原生第一方Synology共享库的FFI包装器,可以从真实设备上提取,或使用单独的工具从DSM存档中提取。
发现漏洞
有了相关的二进制文件,我们可以开始查看在端口5566上监听的TCP服务的代码。
主要的二进制文件synobtrfsreplicad只是一个驱动程序的垫片,用于调用libsynobtrfsreplicacore.so.7中的功能,该库启动TCP监听器。
该服务是一个基于Linux的最小化分叉服务器,主进程不断调用accept()并为每个新的远程客户端分叉一个子进程。反过来,子进程运行一个基本的命令循环来解析发送到服务的传入消息。
每个命令都有一个简单的二进制格式,包含一个操作码,可选地后跟可变大小的数据负载:
|
|
定义了两个全局变量来促进解析这些命令消息。一个用于命令本身,另一个是一个类似环形缓冲区的结构,用于保存最多3个可变大小的命令负载。
|
|
读取消息的命令循环大致如下:
|
|
如果攻击者提供的长度太大,recvCmd会在不读取任何负载的情况下退出。然而,它的返回值为零,表示没有错误,考虑到头部长度无效,这有点奇怪……回到调用者那里,它不知道任何错误,事情正常进行,命令负载被空终止,使用任意大的头部长度。
这个漏洞足够简单,对于我们的初始POC,我们可以使用netcat发送一个仅由A组成的消息(至少12个),以经典的可利用方式:
除非你使用gdb附加到服务,否则设备上没有任何迹象表明出了问题。该故障似乎没有记录到syslog或任何其他DSM日志设施中,并且由于分叉服务器的性质,不会立即丧失功能。
此漏洞提供的原语将允许我们在共享库的BSS(数据段)的任意偏移处重复进行空字节写入。非常像CTF。尽管漏洞相当简单,但利用它会更有趣。
无论如何,由于所有缓解措施都已启用,我们首先必须以某种方式将其转换为信息泄漏。
分叉服务器
在我们继续之前,回想一下我们正在处理一个分叉服务器,这对于打破ASLR非常有用。
每个被分叉的子进程将与父进程具有完全相同的地址空间,崩溃它们没有任何后果:我们只需重新连接到服务并获得一个干净的状态,以新的子进程的形式。
有点像时间循环,每个连接都是一个以累积方式收集地址空间新信息的机会。
在高层,每次迭代具有以下结构:
- 猜测某个值(例如一个地址)
- 让二进制文件使用猜测的值,这样如果正确或不正确,它的行为会有所不同(例如错误的地址会导致崩溃)
- 观察二进制文件的行为以确定值是否正确
- 如果正确,我们找到了正确的值。否则,使用下一个猜测重复
随着我们继续,我们将看到如何将其应用于此特定二进制文件。
功能概述
由于相关漏洞发生在输入解析期间,我们尚未探索程序的许多功能,在构建漏洞利用程序时我们需要利用这些功能。
从网络读取命令后,命令循环对提供的操作码有一个switch-case。需要输入的操作码从可变长度的命令负载中解析它们。我们查看了所有可用的操作码以大致了解它们的功能:
- CMD_DSM_VER:无输入 - 返回DSM版本号
- CMD_SSL:为连接初始化SSL
- CMD_TEST_CONNECT
- CMD_NOP
- CMD_VERSION:输入整数 - 设置连接的"版本"以处理兼容性差异
- CMD_TOKEN:输入字符串"token”,必须作为键存在于磁盘上的JSON文件中 - 执行初始化并设置全局std::string g_token
- CMD_NAME:输入字符串"name" - 可能执行btrfs相关操作,和/或使用g_token修改JSON文件
- CMD_SEND:输入原始数据 - 将输入代理到文件描述符,似乎在其他地方设置为btrfs命令的管道
- CMD_UPDATE
- CMD_STOP:输入token字符串 - 从JSON中删除token
- CMD_COUNT
- CMD_CLR_BKP
- CMD_SYNCSIZE
- CMD_END
很快变得清楚的是,这些代码路径中的许多都依赖于提供有效的"token",该token应该已经存在于/usr/syno/etc/synobtrfsreplica/btrfs_snap_replica_recv_token的JSON文件中。
JSON被用作简单的键值属性存储,其中token是键:
|
|
据推测,某些外部服务分发这些token并写入文件,但这发生在哪里我们不清楚。
然而,有一个代码路径允许将token添加到JSON文件,可能以非预期的方式。CMD_NAME操作码使用当前的g_token,并将属性写入文件,有两个重要的细微差别:
- 它不检查g_token是否曾经被初始化(即使用CMD_TOKEN)
- 如果token尚未作为键存在于JSON对象中,设置属性会添加它
通常,未初始化的g_token将只是一个空字符串,但在内存损坏的情况下,一切都不确定,我们稍后将看到这如何证明是有用的。
ASLR Oracle #1:释放假的堆块
我们的原语是一个空字节写入,我们提供命令负载缓冲区的任意偏移量。偏移量是无符号的,因此我们只能将空值写入负载缓冲区之后的内存。
这就带来了一个问题:负载缓冲区之后是什么,这将是共享库BSS中g_recvbuf全局变量中的三个0x10000大小缓冲区之一。
除了少数std::string实例外,没有太多的全局变量,它们具有以下结构:
|
|
默认构造函数将长度设置为0,并将char*指向内联缓冲区。换句话说,我们将在BSS中有一堆std::string实例,指针设置为它们自己的BSS地址加上偏移量16。
现在,考虑如果我们使用空写入将其中一个指针的两个最低字节清零。前面的负载缓冲区是0x10000字节,足够大以保证部分置空的BSS指针指向此缓冲区内的某个位置,尽管我们不知道确切的偏移量。
由于ASLR具有页面粒度(12位),此偏移量中将有4位(一个半字节)的熵(即可以是0, 0x1000, 0x2000, … 0xf000)。
我们可以损坏的全局字符串之一是_gSnapRecvPath,可以通过CMD_NAME命令的操作之一重新分配。
当重新分配std::string时,如果char*没有指向内联缓冲区,将在分配新值之前对旧的(现在已损坏的)值调用delete。
这让我们可以在负载缓冲区内的假块上调用free。我们自然通过命令负载控制此缓冲区的内容。
当调用free时,如果假块的大小足够小,它将被放入glibc tcache。或者,如果大小无效(例如为零),free将调用abort,使进程崩溃。这创建了我们的第一个oracle,我们可以将其与分叉服务器行为结合,以确定假块位于16个可能偏移量(0, 0x1000, … 0xf000)中的哪一个。
对于16个可能的偏移量中的每一个:
- 用填充物填充负载缓冲区直到猜测的偏移量,后跟假块的元数据(这只是一个假的大小值)
- 触发漏洞两次以空出_gSnapRecvPath的char*的两个低字节
- 使用CMD_NAME释放损坏的char*,它可能指向也可能不指向放置在猜测偏移量处的假块
- 如果套接字保持连接并发送响应,猜测的偏移量正确
- 如果套接字关闭(即调用了abort),猜测不正确;尝试下一个偏移量
我们现在已经解析了一个半字节的ASLR熵,并且可以可靠地释放负载缓冲区中的假块,该块将被放入tcache。
ASLR Oracle #2:泄漏Token
tcache是空闲块的单链表,每个空闲块都有一个next指针。由于glibc中的一些硬化尝试,next指针的填充方式如下:
|
|
在我们的情况下,tcache列表之前将是空的(next = 0),因此写入的值将是&chunk->next » 12。换句话说,我们已将移位的BSS指针放入负载缓冲区。我们现在想找出某种方法来泄漏这个值。
一旦假块被释放并且移位的BSS指针被写入,我们将空出第二个全局std::string g_token的char*的两个低字节。此损坏将使g_token指向与_gSnapRecvPath完全相同的位置。即,指向移位的BSS指针。
回想我们之前关于CMD_NAME功能的讨论,它可以将未初始化的g_token添加到磁盘上的JSON文件。这就是该事实证明有用的地方,因为现在"未初始化"的g_token不是保存空字符串,而是指向移位的BSS指针。触发此代码路径,JSON文件现在包含我们想要泄漏的值。
还要注意,在将g_token写出到磁盘之前,我们可以额外触发一次空字节写入以截断移位的BSS指针。通过这种方式,我们可以写出指针的每个段。例如,如果移位的指针是0x766554433,我们可以写出每个段,从33, 3344, … 到完整的3344556607。
一旦JSON文件包含泄漏,我们可以按预期使用CMD_TOKEN,它期望一个字符串参数指示要使用的token。此token将在JSON文件中查找,并根据是否找到返回不同的错误代码。这创建了我们的第二个oracle,我们可以使用它来实现逐字节暴力破解:
- 循环b从0到4,对于移位BSS指针的5个字节中的每一个:
- 将指针截断为长度b+1,然后将截断的段写入JSON文件
- 循环可能的字节0 - 0xff:
- 发送CMD_TOKEN与猜测的字节(前面加上先前迭代中已知的字节,长度为b)
- 返回的错误代码将指示提供的字节是否正确
- 如果正确,我们找到了移位指针索引b处的字节
- 否则,继续尝试下一个可能的字节
一旦此逐字节暴力破解完成,我们将泄漏移位的BSS指针,这给了我们共享库的基地址。
由于mmap映射在虚拟内存中是连续的,这也给了我们所有共享库的地址,最值得注意的是libc。
劫持控制流
有了泄漏,我们准备制作最终负载以劫持控制流。
我们已经能够释放负载缓冲区中的假块,并且通过发送额外的命令,我们可以任意损坏此空闲块。
此时,我们可以以标准方式滥用tcache链表:
- 用任意地址损坏假块的next指针
- 分配与假块相同大小的东西 - malloc将返回假块,然后将tcache列表的新头设置为任意地址
- 再次分配相同大小以使malloc返回任意地址
我们只需要找到一些匹配两个连续分配模式的代码。
幸运的是,事实证明CMD_TOKEN处理程序符合此模式,并且在执行两个分配后,包含我们输入参数的std::string被析构,在我们的输入上调用delete。
这给我们带来了以下策略:
- 损坏假tcache块的next指针指向共享库的delete的GOT条目附近
- 发送CMD_TOKEN命令
- 处理程序将从损坏的tcache分配两次,用system覆盖delete的GOT条目
- 随后的析构函数调用delete,而是使用受控的输入字符串调用system
从这里开始游戏结束。我们可以简单地执行/bin/sh并将stdio重定向到已连接的客户端套接字(避免需要连接回)。
我们提交的完整漏洞利用代码已在此处提供。
修复
该漏洞被分配为CVE-2024-10442。Synology于2024年11月5日相对快速地发布了Replication Service的补丁(Pwn2Own Ireland于10月22日举行),你可以在此处找到公告。ZDI的公告可以在此处找到。
该补丁修改了recvCmd函数,在提供的头部长度太大时返回错误而不是零。
|
|
然后调用者检测到此错误并退出,而不是继续处理无效命令。
结论
尽管容易找到,但这个漏洞的利用很有趣,因为空字节写入作为一个原语相对较弱。它感觉像是你在CTF挑战中找到的那种错误,而tcache操作和暴力破解oracle也符合CTF的氛围。
更严肃地说,即使它在一个非默认软件包中,在远程可访问的服务(以root身份运行)中存在如此简单的漏洞有点令人担忧,特别是考虑到Synology是一个相当流行的消费和企业级NAS,并且这些设备暴露在互联网上并不罕见。