前年我们在Synology 的NAS 中发现了Pre-auth RCE 的漏洞(CVE-2021-31439),并在Pwn2Own Tokyo 中取得了Synology DS418 play 的控制权,而成功获得Pwn2Own 的点数,后续也发现这个漏洞不只存在Synology 的NAS,也同时存在多数厂牌的NAS 中,这篇研究将讲述这漏洞的细节及我们的利用方式。
此份研究亦发表于HITCON 2021,你可以从这里取得投影片!
Network Attached Storage
早期NAS 一般用途为让伺服器本身与资料分开也为了做异地备援而使用的设备,功能上主要单纯让使用者可以直接在网路上存取资料及分享档案,现今的NAS 更是提供多种服务,不止档案分享更加方便,也与IoT 的环境更加密切,例如SMB/AFP 等服务,可轻易的让不同系统的电脑分享档案,普及率也远比以前高很多。
现今的NAS,也可装上许多套件,更是有不少人拿来架设Server,在这智慧家庭的年代中,更是会有不少人与home assistant 结合,使得生活更加便利。

Motivation
为何我们要去研究NAS 呢 ?
红队需求
过去在我们团队在执行红队过程中,NAS 普遍会出现在企业的内网中,有时更会暴露在外网,有时更会存放不少企业的机密资料在NAS 上,因此NAS 渐渐被我们关注,战略价值也比以往高很多。
勒索病毒
近年来因为NAS 日益普及,常被拿来放个人的重要资料,使NAS 成为了勒索病毒的目标,通常骇客组织都会利用漏洞入侵NAS 后,将存放在NAS 中的档案都加密后勒索,而今年年初才又爆发一波locker 系列的事件,我们希望可以减少类似的事情再次发生,因而提高NAS 研究的优先程度,来增加NAS 安全性。也为了我们实现让世界更安全的理想。
Pwn2Own Mobile 2020
最后一点是NAS 从2020 开始,成为了Pwn2Own Mobile 的主要目标之一,又刚好前年我们也想尝试挑战看看Pwn2Own 的舞台,所以决定以NAS 作为当时研究的首要目标,前年Pwn2Own 的目标为Synology 及WD ,由于Synology 为台湾企业常见设备,所以我们最后选择了Synology 开始研究。
Recon
Environment
我们的测试环境是DS918+ 与Pwn2own 目标极为类似的型号,我们为了更佳符合平常会遇到的环境以及Pwn2Own 中要求,会是全部default setting 的状态。
Attack surface

首先可先用netstat 看tcp 和udp 中有哪些port 是对外开放,可以看到tcp 及udp 中在default 环境下,就开了不少服务,像是tcp 的smb/nginx/afpd 等

而udp 中则有minissdpd/findhost/snmpd 等,多数都是一些用来帮助寻找设备的协定。
我们这边挑了几个Service 做初步的分析
DSM Web interface
首先是DSM Web 介面,最直觉也最直接的一部分,这部分大概也会是最多人去分析的一块,有明显的入口点,在古老时期常有command injection 漏洞,但后来Synology 有严格规范后彻底改善这问题,程式也采用相对保守的方式开发,相对安全不少。
SMB
Synology 中的SMB 协定,使用的是Open Source 的Samba,因使用的人众多,进行code review 及漏洞挖掘的人也不少,使得每年会有不少小洞,近期最严重的就是SambaCry,但由于较多人在review 安全性相对也比其他服务安全。
iSCSI Manager
主要协助使用者管理与监控iSCSI 服务,由Synology 自行开发,近期算比较常出现漏洞的地方,但需要花不少时间Reverse ,不过是个不错的目标,如果没有其他攻击面,可能会优先分析。
Netatalk
最后一个要提的是Netatalk 也就是afp 协定,基本上没什么改,大部分沿用open source 的Netatalk,近期最严重的漏洞为2018 的Pre-auth RCE (CVE-2018-1160),关于这漏洞可参考Exploiting an 18 Year Old Bug,Netatalk 相对其他Service 过去的漏洞少非常多,是比较少被注意到的一块,并且已经长时间没在更新维护。
我们经过整体分析后, 认为Netatalk 也会是Synology 中最软的一块,且有Source code可以看,所以我们最后决定先分析他。当然也还有其他service 跟攻击面,不过这边由于篇幅因素及并没有花太多时间去研究就不一一分析介绍了。我们这次的重点就在于Netatalk。
Netatalk
Apple Filing Protocol (AFP) 是个类似SMB 的档案传输协定,提供Mac 来传输及分享档案,因Apple 本身并没有开源,为了让Unlx like 的系统也可以使用,于是诞生了Netatalk,Netatalk 是个实作Mac 的AFP 协定的OpenSource 专案,为了让Mac 可以更方便的用NAS 来分享档案,几乎每一厂牌的NAS 都会使用。
Netatalk in Synology
Synology 中的netatalk 是预设开启,版本是改自3.1.8 的netatalk,并且有在定期追踪安全性更新,只要刚装好就可以用afp 协定来与Synology NAS 分享档案,而binary 本身保护有ASLR/NX/StackGuard。

DSI
讲漏洞之前,先带大家来看一下netatalk 中,部分重要结构,首先是DSI,Netatalk 在连线时是使用的DSI (Data Stream interface) 来传递资讯,Server 跟Client 都是通过DSI 这个协定来沟通,每个connection 的packet 都会有DSI 的header 在packet 前面
DSI Packet Header :

DSI 封包中内容大致上会如上图所示,会有Flag/Command 等等metadata 以及payload 通常就会是一个DSI Header + payload 的结构
AFP over DSI :

afp 协定的通讯过程大概如上图所示,使用AFP 时,client 会先去拿server 资讯,来确定有哪些认证的方式还有使用的版本等等资讯,这个部分可以不做,然后会去Open Session 来,开启新的Session,接着就可以执行AFP 的command ,但在未认证之前,只可以做登入跟登出等相关操作,我们必须用login 去验证使用者身份,只要权限没问题接下来就可像SMB 一样做档案操作
在Netatalk 实作中,会用dsi_block 作为封包的结构
dsi_block :

- dsi_flag 就是指该packet 是request or reply
- dsi_command 表示我们的request 要做的事情
- DSICloseSession
- DSICommand
- DSIGetStatus
- DSIOpenSession
- dsi_code
- dsi_doff
- DSI data offset
- Using in DSIWrite
- dsi_len
DSI : A descriptor of dsi stream

在netatalk 中,除了原始封包结构外,也会将封包及设定档parse 完后,将大部分的资讯,存放到另外一个名为DSI 结构中,例如server_quantum 及payload 内容等,以便后续的操作。

而封包中的Payload 会存放在DSI 中command 的buffer 中,该buffer 大小,取自于server_quantum,该数值则是取自于afp 的设定档afp.conf 中。

如果没特别设定,则会取用default 大小0x100000。
有了初步了解后,我们可以讲讲漏洞。
Vulnerability

我们发现的漏洞就发生在,执行dsi command 时,读取payload 内容发生了overflow,此时并不需登入就可以触发。问题函式是在dsi_stream_receive

这是一个将接收到封包的资讯parse 后放到DSI 结构的function,这个function 接收封包资料时,会先根据header 中的 dsi_len
来决定要读多少资料到command buffer 中,而一开始有验证dsi_cmdlen
不可超过server quantum 也就是command buffer 大小。

然而如上图黄匡处,如果有给dsi_doff
,则会将 dsi_doff
作为cmdlen 大小,但这边却没去检查是否有超过command buffer。

使得 dsi_strem_read
以这个大小来读取paylaod 到command buffer 中,此时command buffer 大小为0x100000,如果 dsi_doff
大小超过0x100000 就会发生heap overflow。
Exploitation
由于是heap overflow,所以我们这边必须先理解heap 上有什么东西可以利用,在DSM 中的Netatalk 所使用的Memory Allocator 是glibc 2.20,而在glibc 中,当malloc 大小超过0x20000 时,就会使用mmap 来分配记忆体空间,而我们在netatalk 所使用的大小则是0x100000 超过0x20000 因此会用mmap 来分配我们的command buffer。

因为是以mmap 分配的关系,最后分配出来的空间则会在Thread Local Storage 区段上面,而不是在正常的heap segment 上,如上图的红框处。

afpd 的memory layout 如上图所示,上述红框那块就是,红色+橘色这区段,在command buffer 下方的是Thread-local Storage。
Thread-local Storage
Thread-local Storage(TLS) 是用来存放thread 的区域变数,每个thread 都会有自己的TLS,在Thread 建立时就会分配,当Thread 结束的时候就会释放,而main thread 的TLS 则会在Process 建立时就会分配,如前面图片中的橘色区段,因此我们可利用heap overflow 的漏洞来覆盖掉大部分存放在TLS 上的变数。
Target in TLS
事实上来说TLS 可控制RIP 的变数有不少,这边提出几个比较常见的
- 第一个是main arena,主要是glibc 记忆体管理个结构,改main arena 可以让记忆体分配到任意记忆体位置,做任意写入,但构造上比较麻烦。
- 第二个是pointer guard 可藉由修改pointer guard 来改变原本呼叫的function pointer ,但这边需要先有leak 跟知道原本pointer guard 的值才能达成
- 第三个则是改
tls_dtor_list
,不须leak 比较符合我们现在的状况
Overwritetls_dtor_list
这技巧是由project zero 在2014 所提出的方法,覆盖TLS 上的tls_dtor_list 来做利用,藉由覆盖该变数可在程式结束时控制程式流程。
struct dtor_list
{
dtor_func func;
void *obj;
struct link_map *map;
struct dtor_list *next;
}
这边就稍微提一下这个方法,tls_dtor_list
是个dtor_list
object 的singly linked list 主要是存放thread local storage 的destructor,在thread 结束时会去看这个linked list 并去呼叫destructor function,我们可藉由覆盖 tls_dtor_list
指向我们所构造的dtor_list。

而当程式结束呼叫exit() 时,会去呼叫call_tls_dtors()
,该function 会去取 tls_dtor_list
中的object 并去呼叫每个destructor,此时如果我们可以控制 tls_dtor_list
就会去使用我们所构造的 dtor_list
来呼叫我们指定的函式。

但在新版本和synology 的libc 中,dtor_list 的function pointer 有被pointer guard 保护,导致正常情况下,我们并不好利用,一样需要先leak 出pointer guard 才能好好控制rip 到我们想要的位置上。
但有趣的是pointer guard 也会在TLS 上,他会存在TLS 中的tcbhead_t 结构中,如果我们overflow 够多,也可以在overflowtls_dtor_list
的同时,也将pointer guard 也一并清掉,这样就可以让我们不用处理pointer guard 问题。

先来讲讲tcbhead_t 这结构,这个结构主要是Thread Control Block (TCB),有点类似Windows 中的TEB 结构是thread 的descriptor,主要会用来存放thread 的各种资讯,而在x86_64 的Linux 架构的usermode 下,fs 暂存器会指向这位置,每当我们要存取thread local variable 时,都会透过fs 暂存器去存取,我们可以看到TCB 结构会有stack guard 及pointer guard 等资讯,也就是说当我们在拿pointer guard 时,也是用fs 暂存器从这个结构取出的。

我们回头看一下TLS 上的结构分布,可以看到 tls_dtor_list
后方就是这个,tcbhead_t
结构。只要我们overflow 够多就可以盖掉pointer guard,然而此时会出现另外一个问题。

因为stack guard 在pointer guard 前,当我们盖掉pointer guard 的同时,也会盖掉stack guard。那么盖掉stack guard 会有什么影响呢?

在我们呼叫 dsi_stream_receive()
时,因为有开启stack guard 保护的关系,会先从TLS 上,取得stack guard 放在stack 上,等到我们呼叫dsi_stream_read 去trigger overflow 且盖掉pointer guard 及stack guard 后,在 dsi_stream_receive()
返回时,会去检查stack guard 是否与TLS 中的相同,但因为这时候的TLS 的stack guard 已经被我们盖掉了,导致检查不通过而中止程式,就会造成我们无法利用这个技巧来达成RCE。
Bypass stack guard

在netatalk(afpd) 的架构中,事实上每次连线都会fork 一个新的process 来handle 使用者的request,而Linux 中的process 有个特性是fork 出来的process,memory address 及stack gurad 等都会与原先的parent process 相同,因此我们可以利用CTF 常见的招式,一个byte 一个bytes brute-force 的方式来获得stack guard 。
Brute-force stack guard
基本概念是在overflow 之后,我们可以只盖TLS 中的stack guard 最尾端一个byte ,每次连线都盖不同的byte,一旦与stack guard 不同,就会因为abort 而中断连线,我们可依据连线的中断与否,判断我们所覆盖的数值是否与stack guard 相同。

以上图来说,我们假设stack guard 是0xdeadbeeffacebc00
,由于stack guard 特性,最低一个byte 一定会是0 ,这边从第二个byte 盖起,这边可以先盖00 试看看连线是否被中断,如果被中断代表盖的数值是错的,接下来我们就测其他数值看看有没有中断,依此类推,测到0xbc 发现没有中断,代表第二个byte 是0xbc,接下来就继续盖第三byte ,一样从0x00 盖到没中断,直到盖满8 bytes 的stack guard 都没中断连线后,我们就可以知道stack guard 的值是什么,接下来我们就可以解决stack guard 问题。
Construct the _dtor_list
to control RIP
在解决stack guard 问题后,netatalk 已可正常运作,接下来我们需要构造 _dtor_list
结构并结束程式来控制RIP,在当时的synology 的afpd 中并没有开启PIE,我们可以在afpd 的data 段中,构造_dtor_list
。

刚好在使用dhx2 method 的login 功能中,会将我们要登入的username 复制到global 的buffer 中,所以我们可以将这结构跟着username 一起写入固定的已知位置。

在一切都构造完成后,我们这边可以触发正常功能的 DSICloseSession
即可触发exit()
tls_dtor_list
in Synology

在reverse 后,发现synology 的glibc 中,会使用 __tls_get_addr()
来取得tls_dtor_list
,并非直接存取 tls_dtor_list
这个全域变数,而这函式的取得方式则会从前述 tcbhead_t
中先取div 栏位后,再取得其中的tls_dtor_list
,因此我们需要连同 tcb->div
一起构造在固定位置,另外一点是Synology 的afpd 中并没有system 可用,但事实上有execl 可以使用,只是参数稍微复杂一点而已。

最后我们构造的结构如上图所示,我们将tcb 及dtor_list 结构都构造在username buffer 中,触发exit() 后,就会去执行execl 并取得反连shell。
Remark
在一般的Netatalk 中,是会启用PIE ,不太容易在已知位置构造_dtor_list
,实际上也可以用类似方法leak 出libc 位置,依旧是exploitable,该漏洞不只影响Synology 也会影响到大部分有使用Netatalk 的设备。
Other vendor
我们测试了许多家有使用到Netatalk 的厂商,发现不少家有存在类似的问题,部分是unexploitable 但也有部分是exploitable。我们这边实测了QNAP 及Asustor,皆有成功获得shell。
QNAP
- We tested on TS451
- Not enable by default
- Protection
- 内建system
\

Asustor
- We tested on AS5202T
- Not enable by default
- Protection
- 内建system

QNAP 及Asustor 两家NAS 都没有开启Stack guard,不需brute-force 即可获得反连shell。
这个漏洞在Synology 尚未修补时,只要default 装好就可以利用,不需任何认证,而QNAP 及Asustor 虽然不是预设开启,但不少有使用Mac 的用户,还是会为了方便把它打开,基本上只要是NAS 几乎都会用到Netatalk,绝大多数的NAS 都有影响,只要有开启Netatalk,攻击者可以利用这个漏洞打下大部分的NAS。你的NAS 就再也不会是你的NAS。

我们后来也从shodan 上发现,其实也有非常多人将netatalk 开在外网,光在shodan 上就有13 万台机器,其中大部分是Synology。
Mitigation
Update
目前上述三台皆已修补,请尚未更新的用户更新到最新
该漏洞也在近期释出的Netatalk 3.1.13版本中修复,如有使用到Netatalk 3.1.13 以前版本,也请务必更新。
Disable AFP
- 没使用AFP 时,最好直接关掉或只限制在内网存取。该project 几乎已经很少维护,继续使用风险极高。
- 改用SMB相对安全
- 如果想要用类似功能,建议可使用SMB 相对安全不少,但只能说相对安全,不能说绝对没问题,建议还是将相关服务都开在内网就好,没用到的能关就关
Summary
我们已成功在NAS 中找到一个严重漏洞,并且成功写出概念证明程式,证实可以利用在Synology、QNAP 及Asustor 等主流NAS 上利用。我们也认为Netatalk 是在NAS 中新一代的后门!
未来希望有使用到第三方套件的NAS 厂商,可以多重新审视一下第三方套件所带来的安全性问题,强烈建议可以自行Review 一次,并且注意其他厂商是否也有修复同样套件上的漏洞,很有可能自己也会受到影响,也希望使用NAS 的用户,也能多多重视不要把NAS 开在外网,能关的服务就尽可能关闭,以减少攻击面,让攻击者有机可趁。
To be continue
事实上,我们并不只有找到一个漏洞,我们也发现还有不少问题,也运用在去年的Pwn2Own Austin 上,这部分我们在大部分厂商修复后会在公开其他的研究,就敬请期待Part II。