$ cat writeup.md…
$ cat writeup.md…
kitctf
Task: SUID-root Rust reimplementation of cat that re-runs its own permission checks in userland; std::fs::metadata (check) and fs::read_to_string (use) both follow symlinks on the same path with no fd pinning. Solution: TOCTOU symlink-swap race — flip a symlink between a decoy file we own and /flag so metadata sees the decoy (check passes) while the root read resolves to /flag, leaking the flag.
$ cat /etc/rate-limit
Rate limit reached (20 reads/hour per IP). Showing preview only — full content returns at the next hour roll-over.
SuperCat. DO NOT EAT. The better, newer, more tasteful version of cat. Obv. highly opinionated
A SUID-root binary /usr/local/bin/supercat is a Rust reimplementation of cat.
Provided files (supercat.tar.gz): main.rs, Dockerfile, Cargo.toml,
Cargo.lock. We connect as user ctf (uid 1000) over ncat --ssl <host> 443,
which (via socat ... EXEC:bash) drops us into a bash shell as ctf. The goal is
to read /flag (owned root:root, mode 0400).
The word "highly opinionated" is the semantic clue: the program does NOT trust the kernel for access control — it re-implements the checks itself in userland. That re-implementation is the bug.
/flag — root:root, mode 0400 (only root may read)./usr/local/bin/supercat — SUID root (chmod 4755, chown root:root).ctf (uid 1000, gid 1000, groups=1000), Debian bullseye image, kernel 6.12.perl was present — no gcc/cc, no python/python3.
This dictated the exploit language (Perl racer rather than C).Baseline behaviour:
$ supercat /flag
this super cat wont be tricked by your pesky bribery attempts...
$ supercat /tmp/m # a file we own, mode 0644
<contents printed 4 times> # once per passing permission check
main.rs does, in pseudocode:
let file_meta = std::fs::metadata(file)?; // (1) CHECK: stats the path let (uid, gid, groups) = get_permissions(); // (2) caller's REAL uid/gid/groups // parsed from /proc/self/status (ctf=1000) let mode = file_meta.mode(); ...
$ grep --similar