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 1Probe the sanitizer
ObservationThe shell autocorrects and blocks bare commands like ls and cat, so the filter is matching known command names at the start of the input. A construct that begins with a different character, such as a parameter expansion, gets past 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 defeats a naive string match, but this rewriter still recognizes the command token after unquoting, so the quoted form is blocked all the same. The assign-default expansion works for a different reason: it changes which token comes first, not where the verb is hidden.
Tried: Using ${ls} or ${cat} without an operator, expecting the expansion to run the command.
Without an operator, the brace form just expands a variable named ls or cat. Those are unset, so it expands to nothing and nothing runs. The assign-default form makes Bash set the variable to the word and substitute that value, which the shell then executes.
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 2Chain the exploit
ObservationOnce the expansion runs real commands, list blargh to find flag.txt and then read it. Be precise about what the shell does with the payload: a < written inside the expansion is not a redirection, because redirections are recognised at parse time and this one only appears after the expansion has already happened.Use ${parameter=ls blargh} to enumerate the directory, then ${parameter=cat blargh/flag.txt} to read the flag. The ${parameter=cat < blargh/flag.txt} form also prints it, but the < is a plain argument rather than a redirection, so cat complains about it first.What didn't work first
Tried: Reading the < in ${parameter=cat < blargh/flag.txt} as a genuine stdin redirection.
It is not one. Bash decides which tokens are redirection operators while parsing the line, and at that moment the whole thing is still a single ${...} word. After the expansion, word splitting hands cat two arguments, a literal < and blargh/flag.txt, so cat prints "cat: '<': No such file or directory" and then dumps the flag anyway. The exit status is 1 even though the flag appeared.
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 command.
Learn more
The second half is just argument passing, and it pays to be exact about it. Bash identifies redirection operators while parsing the line, and at parse time
${parameter=cat < blargh/flag.txt}is one single word. Only afterwards does the expansion producecat < blargh/flag.txt, and word splitting then handscattwo ordinary arguments: a literal<andblargh/flag.txt. You can reproduce that locally: the shell printscat: '<': No such file or directoryand then the file contents, exiting 1. The flag appears, but not because anything was redirected. Dropping the<entirely (${parameter=cat blargh/flag.txt}) is the clean form and exits 0.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.
Interactive tools
- Regex TesterTest regular expressions against a string with live match highlighting, flag toggles, and common CTF pattern shortcuts.
- Strings ExtractorPull printable text from any binary, library, or image. ASCII and UTF-16 detection, configurable minimum length, flag-like highlight, no command line needed.
- Reverse Shell GeneratorGenerate reverse shell payloads (bash, nc, python, perl, ruby, php, node, powershell) and matching listeners. Set host and port once, copy any variant.
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.