Description
Special autocorrects every shell command, so you must abuse parameter expansion and braces to execute raw commands and leak the flag.
Setup
SSH to saturn.picoctf.net on port 56058 with the supplied password.
Experiment with bash parameter expansion to bypass the forced capitalization/rewriting and run arbitrary commands under the hood.
ssh -p 56058 ctf-player@saturn.picoctf.netd8819d45${parameter=ls blargh}${parameter=cat < blargh/flag.txt}Solution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1
Probe the sanitizerObservationI noticed the shell autocorrected and blocked bare commands like ls and cat, which suggested the filter was scanning for known command-name tokens at the start of input and that a Bash construct starting with a different character, such as parameter expansion, could bypass it.Bare ls or cat are rewritten, but expressions starting with ${ slip through. Test ${parameter=ls} as a smoke check before chaining anything else.What didn't work first
Tried: Trying to escape or quote the command name, like 'l''s' or \ls, to sneak past the filter.
Quoting tricks sometimes bypass simple string matches, but this shell's rewriter still catches recognized command tokens after unquoting. The sanitizer gets a second pass or normalizes the input before checking, so the quoted form gets blocked just the same. The assign-default parameter expansion works because it changes which token appears first, not because it hides the verb behind quotes.
Tried: Using ${ls} or ${cat} without an operator, expecting the expansion to run the command.
Without an operator like = the brace form just expands the variable named 'ls' or 'cat', which is unset and expands to an empty string - nothing gets executed. You need the assign-default form ${parameter=word} so Bash sets the variable to the word and then substitutes that value, which the shell subsequently runs as a command.
Learn more
The Special shell wraps every line in a regex-style rewriter that hunts for known command names at the start of input. The regex sees a literal token; it does not understand the rest of Bash's grammar. The form
${parameter=word}is the assign-default parameter expansion: ifparameteris unset, Bash sets it towordand substitutes that value, which then runs as a command. From the sanitizer's point of view the line begins with a brace, not a verb, so it leaves it alone.A worked trace makes the asymmetry concrete. Input
${parameter=ls}arrives. The sanitizer scans forls,cat,cdas the first identifier; finding${instead, it passes the line through. Bash then parses parameter expansion, assignsparameter=ls, substitutesls, and only at that point does the shell try to run the command. The rewriter never gets a second look.Empirical edge cases worth confirming on the box:
${ ls}with a leading space is a syntax error (Bash expects an identifier immediately after the brace), and${ls}with no operator simply expands an unset variable to the empty string. The sanitizer rejects neither, but only the assign-default form (=) actually runs anything. JSX renders the literal braces with{and}escapes; the shell line itself is plain.The technique generalises across restricted shells:
rbash,git-shell, and CTF jails all leak when the filter is built on string matching rather than a full Bash parser. The wider toolbox covers aliases, functions, command substitution$(cmd), process substitution<(cmd), brace expansion, and SUID detours throughvim :!orawk system(). See Linux CLI for CTFs for a broader survey of these patterns.Step 2
Chain the exploitObservationI noticed the sanitizer was still catching the filename when passed as a direct argument to cat inside the parameter expansion, which suggested using stdin redirection with < to feed the file to cat without it appearing as an argv argument the filter could inspect.Use ${parameter=ls blargh} to enumerate the directory and ${parameter=cat < blargh/flag.txt} to read the flag through stdin redirection.What didn't work first
Tried: Trying ${parameter=cat blargh/flag.txt} with the filename as a normal argument.
Passing the path as an argument to cat still gets caught by the sanitizer because it matches the argument pattern it is filtering for. The redirection operator approach - using cat < blargh/flag.txt - keeps the filename out of the argument list entirely and feeds the file through stdin instead, which the filter does not inspect.
Tried: Using ${parameter=ls} without a directory argument and then guessing the flag filename.
Without ls blargh you only see the top-level directory and may not realize the flag is nested inside the blargh subdirectory. Running ${parameter=ls blargh} first reveals flag.txt inside that folder, giving you the exact path needed for the cat redirect command.
Learn more
The redirection trick is the second half of the bypass. The sanitizer matches arguments to commands; it does not parse redirection operators.
cat < blargh/flag.txtopens the file ascat's stdin instead of passing it asargv[1], so a filter that blockscat foooften missescat < foo. Combine that with the parameter-expansion wrapper and the whole expression sails past the rewriter.The same lesson applies to real restricted-shell deployments: kiosk terminals,
git-shell, container init shells. Whenever the gate is "match these tokens" rather than "parse the language and enforce a whitelist on the AST," the gate leaks. The only durable fix is to swap the shell for a non-shell interface or to enforce the boundary at the syscall layer with seccomp or namespaces.
Flag
Reveal flag
picoCTF{5p311ch3ck_15_7h3_w0...35}
Any creative use of ${parameter=...} (or similar expansion) that runs cat on blargh/flag.txt yields the answer.