← Back to Writeups
HTBN/AMisc

blazinglyfast

XESXOR8/23/20268 min read
#misc#htb#n/a

blazinglyfast

Platform: B01Lersc | Category: Misc | Type: Challenge | Difficulty: Hard | OS: NA | Author: D3v0o0Nu11 | Date: 2026-04-18 | Status: Solved Techniques: permission_recovery, pic_constant_recovery, self_binary_introspection, symbol_resolution, token_exfiltration

Summary

Task: a Rust jail lets us control the body of pub fn jail(input: In) -> Out, while the host embeds a random expected token and prints the flag only if program stdout matches it. Solution: read the generated ELF via argv[0], recover the reveal_token return value from its machine code, print that token, and bypass Out construction entirely.

Recon

Port scan

nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
PortServiceVersionNotes
<PORT><SVC><VER><notes>

Enumeration highlights

  • Event: b01lersc | ID: 20260418_b01lersc_blazinglyfast
  • Tags: elf, rust, rust_jail, safe_rust, sandbox, self_introspection, pic, execute_only
  • Indicators: user only controls the body of a safe Rust function, goal appears to require constructing a private wrapper type, wrapper prints the flag only when stdout matches a hidden random token, binary is execute-only but owned by the running process, argv[0] reveals the path to the generated ELF
  • Source: 20260418_b01lersc_blazinglyfast.md

Foothold

Vulnerability / Misconfiguration

  1. Permission_recovery
  2. Pic_constant_recovery
  3. Self_binary_introspection
  4. Symbol_resolution
  5. Token_exfiltration
<command>

Exploitation

  • See original writeup content for detailed exploitation.

Privilege Escalation

Enumeration

sudo -l
find / -perm -4000 2>/dev/null
getcap -r / 2>/dev/null
cat /etc/crontab
ps aux

Exploitation

  1. N/A for challenge-type writeup; see exploitation above.
  2. Flag obtained via challenge solve.
<command>

Flags

FlagLocationValue
flagREDACTED

Key Takeaways / Lessons

  • permission_recovery
  • pic_constant_recovery
  • self_binary_introspection
  • symbol_resolution
  • token_exfiltration
  • Tags: elf, rust, rust_jail, safe_rust, sandbox, self_introspection, pic, execute_only

Original Writeup

<details><summary>Click to expand original content</summary>

Description

'Safe Rust is the true Rust programming language. If all you do is write Safe Rust, you will never have to worry about type-safety or memory-safety. You will never endure a dangling pointer, a use-after-free, or any other kind of Undefined Behavior (a.k.a. UB)' - Rustonomicon

We are given a remote service that compiles the body of pub fn jail(input: In) -> Out into a 32-bit Rust binary and runs it. At first glance the challenge looks like a “safe transmute” puzzle where we must somehow fabricate Out despite private wrapper types.

The real win condition is simpler: the Python wrapper prints the actual flag only when the program's stdout is exactly a random hidden token generated for that run. So the task is not “construct Out at all costs”; it is “make stdout equal the expected token by any safe-Rust-only route.”

Summary

The provided source in chall.py seeds an In, calls our jail(input), then passes the returned value into host::check(out). host::check compares a private-layout Out against internal expectations and prints the embedded token only if all fields match.

However, the outer Python script does one extra check after the binary exits: if the binary's stdout equals the per-run token, it prints the flag. That means we can ignore the nominal type puzzle and instead recover the token directly from the generated executable.

Recon

The important logic from tasks/b01lersc/blazinglyfast/tmpdist/chall.py is:

  1. The service inserts a fresh random string into reveal_token() -> &'static str.
  2. Our code only controls the body of pub fn jail(input: In) -> Out.
  3. In and Out wrap private inner structs, so direct construction of Out is intentionally blocked.
  4. After compilation, the wrapper deletes sources and artifacts, leaves only the binary, marks it execute-only with mode 0o111, and runs it.
  5. If the program's stdout is exactly the hidden token, the wrapper prints the real flag.

That last point completely changes the problem. We do not actually need a valid Out; we only need the token.

Live-service behavior mismatch

The local source claims the validator rejects unsafe, extern, trait, impl, std, #, and !. In practice, the live service really did reject unsafe, extern, trait, impl, and !, but std usage and # attributes/comments were accepted during exploitation.

I treat this as an observed deployment discrepancy, not a guaranteed property of the source tree. The solve used the live behavior.

Dead ends

The first idea was the classic “totally safe transmute” direction: if a soundness bug or weird layout trick lets us reinterpret In as Out, host::check would print the token for us.

I also tried a /proc/self/mem style route to inspect the running image and private data without unsafe code. That failed because /proc was not mounted inside the jail.

Other useful probes established the environment:

  • / contained /app,/bin,/dev,/etc,/lib,/lib32,/lib64,/tmp,/usr
  • /dev only exposed null, zero, and urandom
  • /tmp was writable but not persistent between connections
  • the process ran as uid/gid 1000
  • env::current_exe() failed because /proc/self/exe was unavailable
  • a ptrace helper failed with Operation not permitted

So the usual Linux self-inspection shortcuts were gone.

Exploit

1. Recover the executable path from argv[0]

env::args().next() revealed the actual generated binary path, for example /tmp/rust_jail_xxx/chall32.

Reading that file immediately failed with Permission denied, because the wrapper had already changed it to execute-only (0o111). But the running process owns the file, so safe Rust can simply do:

fs::set_permissions(&a0, fs::Permissions::from_mode(0o700)).ok();

After that, fs::read(&a0) succeeds. This is the key pivot: the challenge becomes pure self-introspection of our own ELF.

2. Locate reveal_token

From safe Rust, I invoked system tools on my own binary with std::process::Command:

  • nm -an to find the symbol address of reveal_token
  • readelf -SW to map virtual addresses to file offsets for .text and .rodata
  • objdump/raw byte parsing to understand how reveal_token returns its string

On i686, objdump showed a stub like:

a270: call next
a275: pop eax
a276: mov edx,0x20
a27b: add eax,0x58b73
a281: lea eax,[eax-0x14db8]
a287: ret

For a Rust &'static str return on 32-bit, eax holds the pointer and edx holds the length. So this function directly returns the embedded expected token from .rodata, and the token length here is 0x20 == 32 bytes.

3. Reconstruct the token from the ELF

Once the symbol VMA and section bases are known:

  1. Convert reveal_token VMA into a file offset inside .text.
  2. Read the first bytes of the function from the ELF.
  3. Parse:
  • mov edx, imm32 → returned string length
  • add eax, imm32 → PIC addend
  • lea eax, [eax+disp32] → final displacement
  1. Compute:
str_vma = sym_vma + 5 + add + disp

The + 5 comes from the call/pop PIC pattern: after call next, the popped value is the address of the next instruction.

  1. Convert str_vma into a file offset using .rodata VMA and file offset.
  2. Read len bytes from the ELF and write them to stdout.

At that point the Python wrapper sees stdout equal to the hidden token and prints the flag.

Final payload

Full working payload body from tasks/b01lersc/blazinglyfast/final_exploit.txt:

use std::{env,fs,process::Command,io::Write};
use std::os::unix::fs::PermissionsExt;

fn hx(s: &str) -> usize {
    usize::from_str_radix(s, 16).unwrap()
}
fn u32le(b: &[u8]) -> usize {
    u32::from_le_bytes([b[0], b[1], b[2], b[3]]) as usize
}
fn i32le(b: &[u8]) -> isize {
    i32::from_le_bytes([b[0], b[1], b[2], b[3]]) as isize
}

let a0 = env::args().next().unwrap_or_default();
fs::set_permissions(&a0, fs::Permissions::from_mode(0o700)).ok();
let data = fs::read(&a0).unwrap();

let nm = Command::new("/bin/nm").args(["-an", &a0]).output().unwrap();
let nm_s = String::from_utf8_lossy(&nm.stdout);
let mut sym_vma = 0usize;
for line in nm_s.lines() {
    if line.contains("reveal_token") {
        let p: Vec<&str> = line.split_whitespace().collect();
        if p.len() >= 3 {
            sym_vma = hx(p[0]);
            break;
        }
    }
}

let re = Command::new("/bin/readelf").args(["-SW", &a0]).output().unwrap();
let re_s = String::from_utf8_lossy(&re.stdout);
let mut text_vma = 0usize;
let mut text_off = 0usize;
let mut rod_vma = 0usize;
let mut rod_off = 0usize;
for line in re_s.lines() {
    let p: Vec<&str> = line.split_whitespace().collect();
    if p.len() >= 6 && p[1] == ".text" {
        text_vma = hx(p[3]);
        text_off = hx(p[4]);
    }
    if p.len() >= 6 && p[1] == ".rodata" {
        rod_vma = hx(p[3]);
        rod_off = hx(p[4]);
    }
}

let sym_off = text_off + sym_vma - text_vma;
let f = &data[sym_off..sym_off + 24];
let len = u32le(&f[7..11]);
let add = u32le(&f[13..17]) as isize;
let disp = i32le(&f[19..23]);
let str_vma = sym_vma as isize + 5 + add + disp;
let str_off = rod_off + (str_vma as usize - rod_vma);
let token = &data[str_off..str_off + len];

let mut out = std::io::stdout();
out.write_all(token).unwrap();
out.flush().ok();
std::process::exit(0);

Why it works

The challenge framing pushes us toward type construction: how can safe Rust create a valid Out when its inner representation is private? But the outer wrapper accidentally provides a stronger primitive than Out construction: it rewards any program whose stdout equals a secret embedded in the binary.

So the exploit completely sidesteps the nominal Rust type barrier. We never construct Out, never call host::check successfully, and never need a safe transmute bug. We just read our own executable, recover the constant returned by reveal_token, print it, and let the wrapper hand us the flag.

The execute-only chmod was also not a real barrier because file ownership remained with the running process. In safe Rust, set_permissions is enough to re-enable reads. Once the ELF is readable, the token is just data-flow through a tiny PIC function.

</details>

Auto-tracked: saved to WriteUps; run /xesor-revise to fold lessons into XESXor_Methodology.md.

signed by XESXOR