caas picoMini by redpwn Solution

Published: April 2, 2026

Description

Now presenting cowsay as a service. Find the flag at caas.mars.picoctf.net.

Remote

The challenge runs at https://caas.mars.picoctf.net - no download needed.

Try visiting /cowsay/hello in your browser to see the cowsay output.

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1
    Identify the command injection
    Observation
    I noticed that the challenge description says input is passed directly to cowsay at the URL path level, which suggested the server concatenates that path into a shell command without sanitization and that injecting a semicolon would allow appending a second command.
    The server runs /usr/games/cowsay followed by the URL path parameter directly in a shell. Because the input is unsanitized, a semicolon appends a second shell command after the cowsay call.
    Learn more

    OS command injection occurs when a web application passes user-controlled data to a shell interpreter without sanitization. The server-side code is likely something like exec('cowsay ' + req.params.message) in Node.js, which hands the entire string to /bin/sh -c. The shell interprets special characters in the user's input as control syntax rather than data.

    The semicolon (;) is a shell command separator - it tells sh to execute the next command regardless of whether the previous one succeeded. Other injection characters include && (run if previous succeeded), || (run if previous failed), | (pipe output), and backticks or $() for command substitution.

    This is classified as a CWE-78 (Improper Neutralization of Special Elements used in an OS Command) and appears in the OWASP Top 10 under "Injection." The correct fix is to never pass user input to a shell at all - use language-native APIs (in Node.js: child_process.execFile() with an argument array, which bypasses the shell entirely).

  2. Step 2
    List files on the server
    Observation
    I noticed that once command injection was confirmed via the semicolon separator, the filename of the flag was unknown; running ls through the injected path would reveal the exact filename before attempting to read it.
    Append ;ls to the URL path to run ls after cowsay. This reveals a file called falg.txt - note the deliberate typo.
    bash
    curl "https://caas.mars.picoctf.net/cowsay/hello;ls"

    Expected output

    falg.txt
    What didn't work first

    Tried: Immediately try to read flag.txt without listing first

    The file is actually named falg.txt, not flag.txt, so cat flag.txt returns an error like 'No such file or directory' and nothing appears in the cowsay response. Enumerating the directory with ls first reveals the correct filename before attempting to read it.

    Tried: Use the browser address bar directly instead of curl to send the semicolon injection

    Most browsers percent-encode the semicolon as %3B before sending the request, so the server receives the literal string 'hello%3Bls' and cowsay just moos that text rather than running ls. curl passes the semicolon through unencoded by default, which is why it triggers the injection.

    Learn more

    Enumeration is the process of discovering what resources exist on a target system before trying to access them. Running ls via command injection shows the current working directory's contents - file names, which point toward where interesting data lives. This is the first step in post-injection reconnaissance.

    The deliberate typo falg.txt (instead of flag.txt) is a common CTF trick: it prevents people from guessing the filename without actually exploiting the vulnerability. It also tests whether solvers actually read the directory listing or just blindly try common filenames.

    In real penetration testing, command injection of this severity is a critical finding. From an ls, a real attacker would escalate to reading /etc/passwd, exfiltrating credentials, establishing persistence, or pivoting to other internal systems. The cowsay wrapper is irrelevant - it's just the vector through which the shell receives the injection.

  3. Step 3
    Read the flag file
    Observation
    I noticed the ls output revealed the file is named falg.txt and that the URL path cannot contain a literal space, which suggested using ;cat%20falg.txt so the percent-encoded space reaches the shell correctly as the argument separator for cat.
    Use ;cat with %20 for the space character (URL encoding) to read the flag file.
    bash
    curl "https://caas.mars.picoctf.net/cowsay/hello;cat%20falg.txt"
    What didn't work first

    Tried: Use a raw space in the curl URL instead of %20

    A literal space in the URL path causes curl to split the argument at the space, treating 'falg.txt' as a separate shell word rather than part of the URL. curl exits with a 'URLs can only contain 255 characters' or malformed URL error and never sends the request. Percent-encoding the space as %20 keeps the entire path as a single URL token that the server then decodes back to a space before passing it to sh.

    Tried: Use cat%20flag.txt (correct filename spelling) rather than cat%20falg.txt

    The file on the server is deliberately misspelled as falg.txt, not flag.txt. cat flag.txt returns 'cat: flag.txt: No such file or directory' embedded in the cowsay response, with no flag content. Running the ls injection first (previous step) reveals the correct typo'd filename.

    Learn more

    URL encoding (percent-encoding) represents characters that have special meaning in URLs by replacing them with a % followed by two hexadecimal digits. A space is %20. This is necessary because raw spaces in a URL path are invalid - browsers and HTTP clients either reject them or encode them automatically. When the server decodes the URL before passing it to the shell, %20 becomes a literal space.

    cat(concatenate) reads and prints file contents to standard output. In a web context, standard output from the injected command is captured and included in the HTTP response - the server's cowsay output and the flag file contents both appear in the same response body.

    This challenge is named "CaaS" - Cowsay as a Service - a playful reference to "Software as a Service (SaaS)." It illustrates how wrapping any command-line tool as a web service without input sanitization is dangerous regardless of how harmless the tool itself is. The same vulnerability would exist if the server ran fortune, figlet, or banner with user input.

Interactive tools
  • 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{moooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo0o}

Node.js exec() with unsanitized user input in a shell context allows arbitrary command injection - semicolons separate commands in bash, so ;cat falg.txt runs as a second shell command.

Key takeaway

OS command injection arises whenever an application concatenates user input directly into a string passed to a shell interpreter, letting the attacker supply shell metacharacters (semicolons, pipes, backticks, dollar-sign substitution) that the interpreter treats as syntax rather than data. The correct fix is to never invoke a shell at all: use execFile or equivalent APIs that accept an argument array, bypassing the shell entirely, rather than trying to sanitize or escape input after the fact. This vulnerability class appears in web apps, IoT firmware, network appliances, and CI/CD pipelines wherever a convenience command-line wrapper is exposed to untrusted input.

Related reading

Want more picoMini by redpwn writeups?

Tools used in this challenge

What to try next