Western Digital PR4100 NAS 上的远程代码执行 (CVE-2022-23121)

原文链接:https://research.nccgroup.com/2022/03/24/remote-code-execution-on-western-digital-pr4100-nas-cve-2022-23121/

月饼漏洞

概括

这篇博文描述了一个未经检查的返回值漏洞,该漏洞由 NCC 集团利用开发小组 (EDG) 的Alex PlaskettCedric HalbronnAaron Adams于 2021 年 9 月发现并利用。我们在 2021 年 11 月的 Pwn2Own 2021 竞赛中针对 Western Digital PR4100 成功利用了它。Western Digital 发布了固件更新(5.19.117),完全删除了对开源第三方易受攻击服务“Depreciated Netatalk Service”的支持。由于此漏洞已在上游 Netatalk 代码中得到解决,因此分配了 CVE-2022-23121,并与新的 Netatalk 版本3.1.13一起发布了 ZDI 公告分布式,它与许多其他漏洞一起修复了此漏洞。

介绍

该漏洞位于Netatalk项目中,该项目是Apple 归档协议 (AFP)的开源实现。Netatalk 代码在/usr/sbin/afpd服务和/lib64/libatalk.so库中实现。该afpd服务默认在Western Digital My Cloud Pro 系列 PR4100 NAS 上运行。

该漏洞可远程利用,无需身份验证。它允许攻击者以nobodyNAS 上的用户身份远程执行代码。此用户可以访问通常需要身份验证的私有共享。

我们已经分析并利用了 5.17.107 版本上的漏洞,我们将在下面详细介绍,但旧版本也可能存在漏洞。

注意:西部数据 My Cloud Pro 系列 PR4100 NAS 基于 x86_64 架构。

我们将我们的漏洞利用命名为“Mooncake”。这是因为我们在 2021 年 9 月 21 日完成了编写我们的漏洞利用程序,这恰好是 2021 年中秋节又名月饼节的日子。

漏洞详情

背景

DSI / AFP 协议

Apple 归档协议 (AFP)是众所周知的服务器消息块 (SMB)协议的替代方案,用于通过网络共享文件。可以在此处找到 AFP 规范。

AFP 通过数据流接口 (DSI)协议传输,该协议本身通过 TCP/IP 在 TCP 端口 548 上传输。

然而,SMB 似乎赢得了文件共享网络协议之战,而 AFP 鲜为人知,即使仍然支持 NAS 等设备。AFP 协议在 OS X 10.9 中被弃用,AFP 服务器在 OS X 11 中被移除。

网聊

Netatalk项目是用于UNIX 平台的 AFP/DSI 实现,于 2000 年移至 SourceForge。其最初目的是允许类 UNIX 操作系统作为许多 Macintosh/OS X 客户端的 AFP 服务器。

如前所述,法新社的兴趣越来越少。这也反映在 Netatalk 项目中。最新的 Netatalk稳定版本(3.1.12) 于 2018 年 12 月发布,这使其成为一个相当弃用且不受支持的项目。

Netatalk 项目容易受到CVE-2018-1160dsi_opensession()漏洞的攻击,该漏洞是Netatalk < 3.1.12 中DSIOpensession 命令 ( ) 中的越界写入。由于没有 ASLR,这在Seagate NAS上被成功利用,后来在带有 ASLR 的环境中作为Hitcon 2019 CTF 挑战的一部分。

AppleDouble 文件格式

AppleSingle 和 AppleDouble文件格式旨在存储常规文件的元数据,并允许在不同文件系统之间共享该信息,而不必担心互操作性。

主要思想是基于任何文件系统都允许将文件存储为一系列字节的事实。因此,如果另一端的文件系统支持它们,则可以将常规文件的元数据(又名属性)保存到常规文件中的附加文件中,并将这些属性反映回另一端(或至少其中一些)。否则,可以丢弃附加属性。

AppleSingle 和 AppleDouble 规范可在 此处找到。AppleDouble 文件格式也在samba 源代码中用这张图解释:

afpd二进制文件和库libatalk.so没有符号。但是,Western Digital 发布了他们使用的基于 Netatalk 开源的实现,以及由于GNU 通用公共许可证 (GPL)而在此处实现的补丁。西部数据发布的最新源代码存档是 5.16.105 版本,与分析的最新版本 (5.17.107) 不匹配。但是,我们已经确认了这一点,并且迄今为止从未在所有 OS 5 版本中进行过修改。因此,下面显示的代码通常是指 Netatalk 源代码。afpdnetatalk.so

注意:Western Digital PR4100 使用最新的 3.1.12 netatalk 源代码作为基础。

让我们分析一下 netatalk 代码在处理以 AppleDouble 文件格式存储的打开分叉文件时如何接受客户端连接、解析 AFP 请求以到达易受攻击的代码。

入口点函数初始化内存中的main()大量对象,加载 AFP 配置,并开始监听 AFP 端口 (TCP 548)。

dsi_start()函数基本上调用了 2 个函数:dsi_getsession()afp_over_dsi().

对函数指针的dsi_getsession()调用是:dsi->proto_opendsi_tcp_open()

dsi_tcp_open()函数接受客户端连接,创建一个子进程fork()并开始初始化与客户端的 DSI 会话。

Teaser:这对剥削很有用。

回到dsi_getsession()afpd集,而处理客户端连接集*childp != NULL的分叉子集afpd*childp == NULL

我们现在回到dsi_start(). 对于父级,没有任何反应,main()永远循环继续等待其他客户端连接。对于处理连接的孩子,afp_over_dsi()被调用。该函数读取 AFP 数据包(即 DSI 有效负载),确定 AFP 命令并调用afp_switch[]全局数组内的函数指针来处理该 AFP 命令。

afp_switch[]全局数组初始化为初始值,该值preauth_switch仅包含几个可用的预身份验证处理程序。postauth_switch一旦客户端通过身份验证,我们可以猜测它被设置为该值。这可以访问许多其他 AFP 功能。

有趣的是,西部数据 PR4100Public默认具有 AFP 共享,无需用户身份验证即可使用。这意味着只要我们以Public共享为目标,我们就可以访问所有这些身份验证后处理程序。还值得一提的是,相同的Public共享可通过服务器消息块 (SMB) 协议提供给来宾用户,无需任何密码。这意味着我们可以通过 AFP 或 SMB 读取/创建/修改任何文件,只要它们存储在Public共享中。

我们感兴趣的AFP命令是“FPOpenFork”,由afp_openfork()handler处理。如前所述,fork 文件是一种特殊类型的文件,用于存储与常规文件关联的元数据。fork 文件以 AppleDouble 文件格式存储。afp_openfork()处理程序找到要打开和调用的卷和分叉文件路径(ad_open()“ad”代表 AppleDouble)。

ad_open()函数非常通用,因为它可以打开不同的分叉文件:数据分叉文件、元数据分叉文件或资源分叉文件。由于我们在这里处理资源分叉,我们最终调用ad_open_rf()(“rf”代表资源分叉)。

注意:ad_open()libatalk/文件夹中,而不是etc/afpd前面讨论的代码。因此,我们从现在开始分析的代码在libatalk.so.

ad_open_rf()然后调用ad_open_rf_ea()

ad_open_rf_ea()函数打开资源分叉文件。假设文件已经存在,它最终调用ad_header_read_osx()来读取实际内容,它是 AppleDouble 格式的:

我们终于达到了我们的易受攻击的功能:ad_header_read_osx().

了解漏洞

读取资源分支的ad_header_read_osx()内容,即以 AppleDouble 文件格式解释它。Netatalk 将 AppleDouble 文件格式的元素存储在其自己的adouble结构中,我们将很快详细说明。ad_header_read_osx()首先读取 AppleDouble 标头以确定有多少条目。

然后我们看到它调用parse_entries()来解析不同的 AppleDouble 条目。下面有趣的是,如果parse_entries()失败,它会记录一个错误但不会退出。

如果我们仔细观察parse_entries(),我们会发现如果发生以下情况之一,它会设置错误:

  • AppleDouble“eid”为零
  • AppleDouble“offset”超出范围
  • 如果添加到数据长度的 AppleDouble“offset”超出界限,则“eid”不指资源分叉时

我们知道我们正在处理资源分叉,所以第二个条件很有趣。简而言之,这意味着我们可以提供一个超出范围的 AppleDouble “偏移量”并parse_entries()返回一个错误,但ad_header_read_osx()忽略该错误并继续处理。

从这里开始,了解哪些缓解措施是有用的,以了解我们需要绕过什么,并在parse_entries()返回后分析代码以了解我们可以构建什么样的利用原语。

开发

缓解措施到位

检查内核的 ASLR 设置:

这里

  • 0 – 禁用 ASLR。如果使用 norandmaps 引导参数引导内核,则应用此设置。
  • 1 – 随机化堆栈、虚拟动态共享对象 (VDSO) 页和共享内存区域的位置。数据段的基地址紧跟在可执行代码段的末尾。
  • 2 – 随机化堆栈、VDSO 页、共享内存区域和数据段的位置。这是默认设置。

使用checksec.pyafpd检查二进制文件的缓解措施:

所以总结一下:

  • afpd: 随机的
    • .text: 读/执行
    • .data:读/写
  • 图书馆:随机
  • 堆:随机
  • 堆栈:随机

由于一切都是随机的,我们将需要某种泄漏原语来绕过 ASLR。我们仍然需要调查是否可以触发越界偏移访问对利用有用的路径。

寻找好的利用原语

我们再来分析一下里面的代码ad_header_read_osx()。让我们假设前面讨论的parse_entries()函数解析一个 AppleDouble 文件,其中一些条目的偏移量可能超出范围。让我们看看parse_entries()回归后我们能做些什么。我们看到,假设if (ad_getentrylen(&adosx, ADEID_FINDERI) != ADEDLEN_FINDERI) {条件通过,它最终会调用 into ad_convert_osx()

正如下面的评论所述,该ad_convert_osx()函数负责将 Apple 的 AppleDouble 文件格式转换为 Netatalk 中实现的格式的简化版本。

我们看到该ad_convert_osx()函数首先将原始 fork 文件(AppleDouble 文件格式)映射到内存中。然后它调用memmove()丢弃 FinderInfo 部分并将其余部分移到它上面。

是时候看看adouble结构了。对我们来说重要的领域是ad_eid[]ad_data[]adouble读取 AppleDouble 文件时,该结构已被填充。所以我们控制所有这些领域。

用于访问 EID 偏移量或长度字段的函数/宏以及数据内容非常不言自明:

  • ad_getentryoff(): 获取一个 EID 偏移值
  • ad_getentrylen(): 获取一个 EID 长度值
  • ad_entry():获取与 EID 关联的数据(通过从上述偏移中检索)

所以我们控制 AppleDouble 文件格式中的所有字段。更准确地说,我们知道我们可以为我们需要的所有条目制作一个无效的 EID“偏移量”,因为前面讨论了parse_entries()未检查的返回值。此外,我们可以通过拥有更大的数据来制作所需大小的资源分支。这意味着我们可以有效地控制调用的源、目标和长度,memmove()以在内存映射之外控制我们控制的写入数据。

注意:我们要定位的条目是ADEID_FINDERIADEID_RFORK

想到的下一个问题是内存映射映射到哪里?

通过测试发现,如果fork文件小于0x1000字节,则映射文件分配在之前相当高的地址uams_pam.souams_guest.sold-2.28.somappings. 更准确地说,ld-2.28.so映射总是在映射文件开始之后的 0xC000 字节,即使 ASLR 到位:

这意味着我们可以使用memmove()覆盖上述库之一中的一些数据。但是要定位什么库?

在调试时,我们注意到当发生崩溃时,如果我们继续执行,Netatalk 中的一个特殊异常处理程序会捕获该异常来处理它。

更具体地说,我们覆盖了整个ld-2.28.so .data部分并最终导致以下崩溃:

我们可以看到它call在我们控制第一个参数和参数的指令上崩溃。

在 IDA 中检查ld-2.28.so,我们看到这是由于dl_open()调用了_dl_rtld_lock_recursive函数指针并将指针传递给了_dl_load_lock锁。

函数指针和 lock 参数都是该部分中_rtld_local全局的.data一部分。

这使得当我们能够覆盖该ld.so .data部分时,使用一个参数调用任意函数是一种非常通用的方法。

注意:这里有一个类似的技术(虽然有点不同)。

我们的目标是通过覆盖锁以包含要执行的 shell 命令并用地址覆盖函数指针来执行任意命令system()

幸运的是,我们已经知道我们已经将控制数据传递给system()函数,所以我们不需要知道它在内存中的位置。但是,由于 ASLR,我们不知道system()函数在哪里。所以我们需要某种泄漏原语来绕过 ASLR。

构建泄漏原语

如果我们再次查看之前的回溯,我们会发现它实际上是在ad_rebuild_adouble_header_osx(). 更具体地说,我们看到以下情况发生在ad_convert_osx()

  • 原始 AppleDouble 文件在内存中映射为mmap()
  • 前面讨论memmove()的被调用来丢弃 FinderInfo 部分
  • ad_rebuild_adouble_header_osx()叫做
  • 映射的文件未映射munmap()

ad_rebuild_adouble_header_osx()功能如下图。此函数负责将adouble结构的内容以 AppleDouble 格式写回映射的文件区域,以便将其保存到磁盘上的文件中。

但是如果我们查看memcpy()调试器中的参数,我们会注意到源参数实际上是从堆栈中引用的并且超出范围:

如果您查看ad_header_read_osx()前面提到的代码,您会注意到它已被确认,因为有一个struct adouble adosx;局部变量(因此存储在堆栈中)一直传递到ad_rebuild_adouble_header_osx().

那么这是什么意思呢?好吧,memcpy()从堆栈控制的偏移量将 32 个字节写入内存映射文件区域。这意味着我们可以让它将任意内存写回磁盘上的文件中。然后我们可以使用 SMB 读取 fork 文件(以 AppleDouble 文件格式存储),然后我们可以将该内容泄露给我们。

很好,但是libc.so堆栈中是否存储了任何地址,因为我们要调用system()驻留在其中的地址libc.so

事实证明,有一个这样的地址,因为main()它是从以下位置调用的__libc_start_main()

Wrapping up(包起来)

默认情况下,在西部数据 PR4100 上,我们可以在 AFP 和 SMB 中读取和写入文件,而无需身份验证,只要我们在Public共享上进行即可。

我们还知道,从父进程afpd派生出一个子进程来处理每个客户端连接。afpd这意味着每个子进程对所有已加载的库都有相同的随机化。

要触发该漏洞,我们需要mooncake存在一个常规文件,以及._mooncake在同一目录中精心制作的关联 fork 文件。然后我们可以在文件上通过 AFP 调用“FPOpenFork”命令mooncake并解析._mooncakefork 文件(以 AppleDouble 文件格式存储)。它最终调用了ad_convert_osx()负责将 Apple 的 AppleDouble 文件转换为 Netatalk 中实现的简化版本的函数。

所以我们首先从创建mooncake文件开始。我们使用 AFP 来完成,但我们认为我们也可以使用 SMB 来完成。然后我们要触发漏洞两次。

第一次,我们制作了._mooncakefork 文件来滥用memcpy()in ad_rebuild_adouble_header_osx(). 触发漏洞时:

  • 原始的._mooncakefork 文件在内存中映射为mmap()
  • memcpy()函数将返回地址写入__libc_start_main()映射区域
  • 调用该munmap()函数并将数据保存到._mooncake磁盘上的 fork 文件中。
  • 我们可以通过 SMB 读取 fork 文件将数据泄露给我们._mooncake(就好像它是一个普通文件一样)

这允许推断libc.so基地址并计算system()地址。

第二次,我们制作了._mooncakefork 文件来滥用memmove()in ad_convert_osx(). 触发漏洞时:

  • 原始的._mooncakefork 文件在内存中映射为mmap()
  • memmove()函数覆盖ld.so .data部分以破坏rtld_local._dl_rtld_lock_recursive函数指针与system()地址和rtld_local._dl_load_lock数据与shell命令执行
  • 由于memcpy()对未映射的堆栈地址的无效访问,函数崩溃
  • 在 Netatalk 中注册的异常处理程序调用dl_open()它使其调用system()我们的任意 shell 命令

我们选择初步删除netcat使用 SMB 静态编译的文件,并从以下路径执行它:/mnt/HD/HD_a2/Public/tools/netcat -nvlp 9999 -e /bin/sh.

下面是实际的漏洞利用:

Pwn2Own 备注

在比赛中使用漏洞利用时,漏洞利用在泄漏阶段的第一次尝试失败。我们猜测,与我们的测试环境相比,这可能是环境的时间问题。因此,我们修改了代码,在泄漏之前引入了一个“sleep()”,以确保 samba 将返回由易受攻击的 AFP 代码修改的数据。我们的第二次尝试使泄漏工作正常,但在尝试通过 telnet 连接时失败,因此我们在通过 telnet 连接之前添加了另一个“sleep()”以确保“system()”命令正确执行。幸运的是,这奏效了,这表明仅仅增加更多的睡眠就足以修复不可靠的漏洞,我们在第三次也是最后一次尝试中取得了成功🙂

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注