SansAlpha picoCTF 2024 Solution

Published: April 3, 2024

Description

The Multiverse is within your grasp! Unfortunately, the server that contains the secrets of the multiverse is in a universe where keyboards only have numbers and (most) symbols.

Keyboard-restricted shell

SSH to mimas.picoctf.net on port <PORT_FROM_INSTANCE> with password <PASSWORD_FROM_INSTANCE>.

Experiment with the restricted shell to learn which characters it accepts and how it responds to your input.

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
The Linux CLI for CTF guide covers the bash parameter-expansion and globbing tricks this challenge forces you to lean on, plus the standard kit for harvesting characters out of error messages.
  1. Step 1
    Capture an error message into a variable
    Observation
    I noticed the shell blocks all alphabetic input, which suggested the only source of letters available was the shell's own error output, making it necessary to trap a predictable error like 'bash: $: command not found' into a variable for later indexing.
    Pick an invalid token like $ or ? and trap its error output in a variable. _1=$ 2>&1 runs the bare $ as a command, redirects stderr into stdout, and stores "bash: $: command not found" in $_1. Verify with echo "$_1" - if you see that single line printed back, you have your character source.
    bash
    _1=`$ 2>&1`
    bash
    echo "$_1"

    Expected output

    bash: $: command not found
    What didn't work first

    Tried: Using $() instead of backticks to capture the error, without the 2>&1 redirect

    $(_1=$($ )) captures only stdout, so _1 ends up empty because bash error messages go to stderr. echo "$_1" prints nothing, leaving you with no character source. The fix is to add 2>&1 inside the capture so stderr is merged into stdout before backticks or $() grabs it.

    Tried: Running a random word like foo 2>&1 instead of $ to trigger the error

    Letters are blocked by the restricted shell, so typing foo results in an immediate rejection before any error message is generated. The trick requires using a non-letter token such as $ or ? that the shell actually tries to evaluate and then fails on, producing the 'command not found' string containing the letters you need.

    Learn more

    The keyboard restriction blocks letters, but parameter expansion (${_1:N:1}) does not require typing letters - it indexes into a string by position. By trapping bash's own error output we get a long string full of letters we can index into.

    $ as a standalone command is the cleanest trigger because it parses, fails immediately, and produces exactly one line: bash: $: command not found. ? works similarly. Wrapping in backticks runs the command in a subshell and captures its output; the 2>&1 redirection sends stderr (where the error actually lives) onto stdout so it can be captured. Without that redirection $_1 would silently end up empty, and the next step would fail with no useful diagnostic.

  2. Step 2
    Extract characters with parameter expansion
    Observation
    I noticed the captured error string 'bash: $: command not found' contains common lowercase letters at fixed, predictable positions, which suggested using bash's ${var:offset:length} substring syntax to pluck individual characters without typing any letters.
    Bash $&#123;var:offset:length&#125; slices a substring out of a variable. Test locally first: echo "$&#123;_1:9:1&#125;" should print c, and echo "$&#123;_1:10:1&#125;" should print o. From the captured string "bash: $: command not found", you can pluck b/a/s/h/c/o/m/n/d/t/f/u as needed.
    Learn more

    This is the heart of the trick. The captured error string contains the lower-case alphabet's commonly-used letters in fixed positions, so you can spell short command names like echo, cat, sh, or bash by chaining slices.

    String:    bash: $: command not found
    Position:  01234567890123456789012345
                        1111111111222222
    
    Useful indices (verify with: echo "${_1:N:1}"):
      ${_1:0:1}  = b
      ${_1:1:1}  = a
      ${_1:2:1}  = s
      ${_1:3:1}  = h
      ${_1:9:1}  = c   <- needed for /bin/echo
      ${_1:10:1} = o   <- needed for /bin/echo
      ${_1:13:1} = a
      ${_1:15:1} = d
      ${_1:17:1} = n
      ${_1:19:1} = t
      ${_1:23:1} = u
      ${_1:24:1} = n
  3. Step 3
    Locate the flag file with a glob
    Observation
    I noticed that shell wildcards like * and ? require no letters to type, which suggested using a glob such as ./*/* to enumerate files in the current directory even without access to ls, and that bash would reveal the path in an error message when it tried to execute the matched file.
    Run ./*/* and bash expands the glob to whatever paths exist; the listing here shows the flag at ./blargh/flag.txt, matching pattern ./*/????.???.
    bash
    ./*/*

    Expected output

    bash: ./blargh/flag.txt: Permission denied
    What didn't work first

    Tried: Running /* or /* /* to enumerate files instead of ./*/*

    /* expands from the filesystem root and produces a long list of system paths, not the challenge directory contents. The error output becomes noisy and does not reliably reveal ./blargh/flag.txt. Starting with ./ anchors the glob to the current working directory where the flag actually lives.

    Tried: Trying to read the flag directly after seeing the path in the error message

    The shell printed 'Permission denied' when it tried to execute ./blargh/flag.txt as a command, which means direct execution is blocked. The path is now known, but you still need to read it with a constructed cat or process substitution - the next steps cover how to do that without typing any letters.

    Learn more

    ./*/* expands to the list of files exactly one directory deep below the current directory. Bash tries to execute the first match as a command, which (helpfully) fails and prints the file path in the error message - giving you the path even when you can't type letters to ls. The pattern ./*/????.??? later picks out the same path by structure (4-letter name, 3-letter extension).

  4. Step 4
    Build /bin/echo from globs and slices
    Observation
    I noticed that /bin is exactly 3 characters and echo is 4 characters, which suggested that combining a ? wildcard path like /???/?c?o with parameter-expansion slices for the letters 'c' and 'o' from $_1 would reliably resolve to /bin/echo without typing any forbidden letters.
    Glob /???/?$&#123;_1:9:1&#125;?$&#123;_1:10:1&#125; into the path /bin/echo. The /??? matches /bin (any 3-char directory under /), and the inner ?c?o pattern matches "echo" via positions 9 and 10 of $_1. The earlier attempt at /usr/bin/cat (/?$&#123;_1:2:1&#125;?/???/??$&#123;_1:19:1&#125;) failed because the ?s? prefix didn't expand to /usr on the box (the glob found no matches and bash printed "bash: /?s?/???/??t: No such file or directory").
    bash
    /???/?${_1:9:1}?${_1:10:1}
    What didn't work first

    Tried: Trying to reach /usr/bin/cat with /?${_1:2:1}?/???/??${_1:19:1} because cat is more familiar than echo

    The glob /?s?/???/??t requires /usr to exist as a 3-character directory under /, but on this machine the layout does not match: no subdirectory of / fits ?s? with a middle s. Bash leaves the literal unmatched pattern in place and the shell rejects it as 'No such file or directory'. /bin/echo is reachable with the simpler /???/?c?o pattern because /bin is exactly 3 characters and echo is 4.

    Tried: Constructing the full path as a string in a variable, then running it with ${_cmd}

    Assigning _cmd='/bin/echo' is blocked because the assignment itself contains letters. Even if you built the string through concatenation of parameter-expansion slices, invoking it via ${_cmd} does not work the same way as a bare glob - bash does not perform word-splitting and glob-expansion on the result of a variable reference used as a command name in the same pass. Running the glob directly as the command token is the reliable approach.

    Learn more

    Verify the glob expands the way you expect before using it as the command: echo /???/?${_1:9:1}?${_1:10:1} should print /bin/echo and nothing else. If it prints multiple matches or the literal pattern, the glob is wrong and the command will not execute echo.

    When a glob has no match, bash on this image leaves the literal pattern in place, so the failure surface is "/?s?/???/??t: No such file or directory" - the unmatched pattern itself appears in the error, which doubles as a diagnostic. That's the failure mode you should expect, and how you knew the /usr/bin/cat path's middle ?s? wedge wasn't hitting /usr.

  5. Step 5
    Read the flag
    Observation
    I noticed that bash's $(<file) process substitution reads a file's contents as a builtin without spawning any external program like cat, which suggested pairing it with the glob ./*/????.??? and the reconstructed /bin/echo command to print the flag without typing any disallowed letters.
    Combine the constructed echo with bash process substitution: /???/?$&#123;_1:9:1&#125;?$&#123;_1:10:1&#125; "$(<./*/????.???)" prints the contents of ./blargh/flag.txt without ever typing the literal letters cat, less, or grep.
    bash
    /???/?${_1:9:1}?${_1:10:1} "$(<./*/????.???)"

    Expected output

    picoCTF{7h15_mu171v3r53_15_m4dn355_145...}
    What didn't work first

    Tried: Using $(cat ./blargh/flag.txt) instead of $(<./blargh/flag.txt) to read the file

    The word cat contains letters, which the restricted shell blocks before the command can even run. $(<file) is a bash builtin read-file form that requires no external program and no letters, making it the only viable option in this environment.

    Tried: Passing the glob ./*/????.??? directly as a shell argument without the $(<...) wrapper

    Without the $(<...) wrapper, the glob expands to the file path ./blargh/flag.txt as a string, not its contents. echo would just print the path, not the flag. The $(<...) layer is what causes bash to open and read the file before passing the result to echo.

    Learn more

    $(<file) is a bash builtin that reads the contents of file without spawning cat; it's the bash idiom for "file slurp." Combined with the globbed-up echo, the final command never types any disallowed character but still prints the flag contents.

Flag

Reveal flag

picoCTF{7h15_mu171v3r53_15_m4dn355_145...}

There are multiple paths through this challenge - the key insight from the hint 'Where can you get some letters?' is to harvest characters from error messages and use bash parameter expansion to build valid commands.

Key takeaway

Bash restricted shells and character-filter defenses are almost always escapable because the shell itself leaks the forbidden characters through error messages, and its own metacharacters (parameter expansion, globbing, process substitution) are powerful enough to reconstruct arbitrary commands from those harvested characters. Real escape techniques follow the same pattern: find a channel that emits the restricted content (error output, environment variables, file names), then use the shell's own evaluation mechanics to consume it. This class of bypass is why defense-in-depth mandates dropping to a minimal interpreter or a purpose-built safe execution environment rather than a blocklist.

Related reading

Want more picoCTF 2024 writeups?

Useful tools for General Skills

Do these first

What to try next