miscfreehard

rustjail

b01lersc

Task: a remote Rust jail compiles attacker code under a harsh substring blacklist, hiding the flag as a randomized file in the working directory. Solution: use test::TestOpts.logfile to reach safe PathBuf procfs primitives, find the parent shell's open runner.sh FD, then overwrite the script so dash resumes into `cat flag_*.txt` at the old EOF offset.

$ ls tags/ techniques/
blacklist_bypasstestopts_pathbuf_indirectionprocfs_reconfile_descriptor_script_overwriteoffset_aligned_script_injection

$ cat /etc/rate-limit

Rate limit reached (20 reads/hour per IP). Showing preview only — full content returns at the next hour roll-over.

rustjail — b01lers CTF 2026

Description

The original organizer prompt was not preserved in the provided files; the challenge was solved through the remote Rust jail service and local notes.

The service accepted a Rust payload over ncat --ssl, wrote it into src/main.rs, and executed cargo run with nightly 2025-09-25 and edition 2024. The goal was to escape the restrictions, discover the randomized flag_<32hex>.txt in the temporary working directory, and print its contents.

Overview / Challenge Setup

At runtime the working directory looked like /tmp/tmp_x/src and contained:

  • Cargo.toml
  • runner.sh
  • wrapper.py
  • get_payload.py
  • cargo_home/
  • src/main.rs
  • flag_<32hex>.txt

The filter was a simple Python substring blacklist over the Rust source. These raw substrings were banned:

unsafe, path, flag, std, alloc, core, include, impl, concat, macro, <, >, '

This is much weaker than an AST-based filter. It blocks exact text, not semantics, so string escapes like "\x66lag_*.txt" still produce the forbidden string at runtime.

Restriction Analysis

The obvious read-file routes were cut off:

  • std::fs::* was impossible because std was blacklisted.
  • FFI through libc::open was blocked because edition 2024 requires unsafe extern, and unsafe was blacklisted.
  • include_* and #[path] were also blocked directly.

The important positive discovery was that extern crate test; and #![feature(test)] still worked. That gave access to test::TestOpts, test::TestDescAndFn, and test::test_main.

Two key primitives came out of that:

  1. Blacklist bypass inside strings. Raw source checks can be dodged with escapes such as "\x66lag_*.txt".
  2. Indirect PathBuf construction. test::TestOpts.logfile has type Option<PathBuf>, so writing
let p = test::TestOpts { logfile: Some("/proc/self/exe".into()), ..opts }.logfile.unwrap();

produced a safe path object without ever writing banned tokens like std or path in source. From there, safe methods such as read_link, read_dir, join, and metadata became available.

...

$ grep --similar

Similar writeups