VNE picoCTF 2023 Solution

Published: April 26, 2023

Description

We have a binary that can list directories as root. SSH into the provided server, run the binary named bin, and find a way to read the root flag through it.

SSH into the provided server as ctf-player using the supplied password and port.

Locate the SUID binary named bin in your home directory and run it once to see how it behaves.

bash
ssh ctf-player@saturn.picoctf.net -p <PORT_FROM_INSTANCE>

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1
    Observe default behavior
    Observation
    I noticed the binary was SUID-owned by root and the challenge said it could list directories as root, which suggested running it without any setup first to see what input it required before attempting any injection.
    Running ./bin with nothing set prints: Error: SECRET_DIR environment variable is not set. The binary uses the SECRET_DIR value as the directory it lists, and it does the listing as root because it is SUID.
    bash
    ./bin

    Expected output

    Error: SECRET_DIR environment variable is not set
    What didn't work first

    Tried: Run strings or ltrace on the binary to discover which environment variable it uses before running it

    strings may show fragments like SECRET_DIR buried among other output, but the binary itself prints the missing variable name in its error message when run directly. Running it is faster and more reliable than parsing strings output, which can produce hundreds of lines of noise from embedded library symbols.

    Tried: Check file permissions with chmod or sudo -l before investigating the binary's behavior

    The SUID bit (visible via ls -l as an 's' in the owner execute position) is what matters here, not writable permissions or sudo rules. sudo -l will show nothing useful for ctf-player, and chmod on a binary you do not own is denied. The privilege escalation path is through the binary's own SUID execution, not a sudo policy.

    Learn more

    A SUID binary runs with the privileges of its owner (here, root) regardless of who launches it. This binary lists the contents of whatever directory the SECRET_DIR environment variable points to, but performs that listing with root privileges. The intended use is harmless directory browsing; the bug is in how it constructs the listing command.

    Before crafting anything, run the binary normally to learn which environment variable it reads. The error message names it directly: SECRET_DIR. You can also confirm the SUID bit with ls -l bin (look for an s in the owner permission field).

  2. Step 2
    List directories as root with SECRET_DIR
    Observation
    I noticed the error message named SECRET_DIR as the required environment variable and the binary listed directories as root, which suggested pointing SECRET_DIR at /root to confirm whether the flag lived there.
    Set SECRET_DIR on the command line. Pointing it at /root shows that the flag file exists there, but a normal cat on it is denied because the file itself is only readable by root.
    bash
    SECRET_DIR=. ./bin
    bash
    SECRET_DIR=/root ./bin
    bash
    cat /root/flag.txt
    The listing of /root reveals flag.txt, but a direct cat /root/flag.txt returns Permission denied because only the binary runs as root, not your shell.
    What didn't work first

    Tried: Copy the binary to your home directory and run it from there, hoping the SUID privilege carries over to let you cat files afterward

    Copying a SUID binary strips the SUID bit because regular users cannot create SUID executables. The copy runs as ctf-player, not root, so SECRET_DIR=/root will still list the directory (if /root is world-executable) but cat /root/flag.txt from your shell is still denied. The privilege only exists while the original binary is executing.

    Tried: Use sudo cat /root/flag.txt after confirming the flag is at /root/flag.txt

    ctf-player has no sudo rights on this machine; sudo -l will confirm that. The only root-level execution available is through the SUID binary itself, so the flag must be read by making the binary perform the cat, not by escalating the shell independently.

    Learn more

    Supplying SECRET_DIR=/root confirms the flag location, but you still cannot read the file directly. The binary holds the root privilege, your interactive shell does not. The path forward is to make the privileged binary itself read the file rather than just listing the directory.

    This is the classic split that makes these challenges solvable: the privileged process accepts attacker-controlled input (the environment variable) and feeds it into a shell command without sanitizing it.

  3. Step 3
    Inject a command through SECRET_DIR
    Observation
    I noticed the binary accepted SECRET_DIR as an unvalidated string passed into a shell listing command as root, which suggested that appending a shell metacharacter like & or ; could chain a second privileged command such as cat /root/flag.txt.
    Because the binary passes SECRET_DIR into a shell, you can append your own command with a shell separator. Using & (or ;) chains a cat of the flag that runs as root, printing the flag.
    bash
    SECRET_DIR="/root&cat /root/flag.txt" ./bin
    What didn't work first

    Tried: Use a newline or null byte as the separator instead of & or ; to chain the second command

    Newlines and null bytes are stripped or terminate the variable value before it reaches the shell in most environments. The shell metacharacters & and ; are reliably preserved through environment variable assignment and are the correct separators here. A pipe (|) also works if the binary uses sh -c, because the left side produces output that the right side (cat) does not need.

    Tried: Wrap the injection in a subshell with $() - for example SECRET_DIR="/root $(cat /root/flag.txt)" - rather than using &

    Command substitution with $() is expanded by the shell that receives the variable, which is the unprivileged shell that runs the ./bin invocation, not the root-privileged shell inside the SUID binary. The substitution executes as ctf-player and gets Permission denied before the binary even starts. The injection must be a literal string that the SUID binary's internal shell evaluates.

    Learn more

    This is a command injection vulnerability. The binary takes the SECRET_DIR value and interpolates it into a shell command line (something like ls $SECRET_DIR) that runs with root privileges. Because the value is never escaped, a shell metacharacter such as &, ;, or | breaks out of the intended command and lets you run a second one.

    Setting SECRET_DIR="/root&cat /root/flag.txt" makes the binary first list /root, then run cat /root/flag.txt as root, which prints the flag inline with the listing output. The fix in real software is to never build shell commands from untrusted input: pass arguments to execve directly, or strictly validate and quote any value before it reaches a shell.

  4. Step 4
    Read the flag
    Observation
    I noticed the injected cat command ran with root privileges and printed the flag contents inline with the directory listing, which meant the output was immediately visible and ready to copy.
    The injected cat prints the flag value alongside the directory listing. Copy it and submit.
    Learn more

    Command injection through an unsanitized environment variable is a real and recurring class of privilege-escalation bug. Whenever a privileged program builds a shell command from data an unprivileged user controls (environment variables, file contents, network input), an attacker can chain arbitrary commands at the elevated privilege level.

Flag

Reveal flag

picoCTF{Power_t0_man!pul4t3_3nv_...}

Per-instance flag. The prefix picoCTF{Power_t0_man!pul4t3_3nv_ is consistent across multiple verified sources but the 8-character hex suffix varies per instance.

Key takeaway

SUID binaries grant a fixed set of elevated privileges to any user who runs them, but if the binary passes attacker-controlled input into a shell without sanitizing shell metacharacters, the attacker inherits those elevated privileges through the injected command. Environment variables are a commonly overlooked injection surface because developers focus on command-line arguments and network input; the safe fix is always to pass data as arguments to execve directly rather than constructing shell command strings. The same class of bug drives real CVEs in setuid helpers, sudo wrappers, and cron scripts.

Related reading

Want more picoCTF 2023 writeups?

Useful tools for Binary Exploitation

What to try next