Lion Cipher Lab
Exploit and fix OS command injection (in your own lab)
- AppSec / DevSecOps
- Intermediate
- ~55 min
- Free · no account required
Why it matters
OS command injection is a flagship case of OWASP Top 10 A05:2025 (Injection) and its own weakness, CWE-78, and it is one of the highest-impact web bugs because the payoff is direct: run commands on the server as the account the app runs as. The bug appears whenever an application builds a command line out of user input and hands the whole string to a SHELL to run. A shell is the program (sh/bash on macOS/Linux, cmd on Windows) that interprets command lines — and it treats certain characters specially: ';', '|', '&', '&&', '||' and newlines chain or separate commands; '$(...)' and backticks…
What you'll learn
- explaining OS command injection: user input is concatenated into a command string that is handed to a SHELL to interpret, so attacker-controlled input becomes an attacker-controlled command
- recognizing the shell metacharacters that make it possible — ; | & && || newline (command separators/chaining), $(...) and `...` (command substitution), > (redirection) — and why they only matter when a shell parses the string
- building a tiny vulnerable Flask 'ping tool' on your own machine that runs os.system / subprocess.run(..., shell=True) on a string built from a user-supplied host
- exploiting it locally by sending host="127.0.0.1; whoami > PWNED" (';' on macOS/Linux, '&' on Windows, or $(whoami)) and watching a marker file appear — proof a second command you injected actually executed
- fixing it the right way: stop using the shell — call subprocess.run(["ping", "-c", "1", host], shell=False) so host is a single ARGUMENT that can never be parsed as a command, and allowlist the input to a hostname/IP shape
- re-sending the identical payload against the fixed app and confirming it becomes a harmless literal argument (ping just rejects it as an invalid host) with no marker file created
- explaining why a blocklist of 'bad characters' is a fragile, losing fix (newlines, $IFS, encodings, and other metacharacters keep bypassing it) and why avoiding the shell — or not shelling out at all — is the durable one
- recognizing that this is a language-wide bug class: Node child_process exec vs execFile/spawn, PHP system/exec/shell_exec, Java Runtime.exec/ProcessBuilder, Ruby backticks and system, and where it hides — anywhere user input reaches a command line
Steps in this lab
- What OS command injection is — the vocabulary and the ground rules
- Set up: confirm Python, make a folder, install Flask
- Build the VULNERABLE app (a 'ping tool' that trusts the shell)
- Prove the injection (a host value that runs a second command)
- Why blocklisting 'bad characters' is the wrong fix
- Fix it: stop using the shell — pass an argument array, then allowlist
- What you did — and the reflex to keep