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.
Setup
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.
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.
Step 1
Observe default behaviorObservationI 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./binExpected 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_DIRenvironment 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 withls -l bin(look for ansin the owner permission field).Step 2
List directories as root with SECRET_DIRObservationI 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.bashSECRET_DIR=. ./binbashSECRET_DIR=/root ./binbashcat /root/flag.txtThe listing of/rootrevealsflag.txt, but a directcat /root/flag.txtreturnsPermission deniedbecause 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=/rootconfirms 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.
Step 3
Inject a command through SECRET_DIRObservationI 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.bashSECRET_DIR="/root&cat /root/flag.txt" ./binWhat 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_DIRvalue and interpolates it into a shell command line (something likels $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 runcat /root/flag.txtas 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 toexecvedirectly, or strictly validate and quote any value before it reaches a shell.Step 4
Read the flagObservationI 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.