Command Injection
How input pasted into a shell command runs as commands on the server, why stripping separators never works, and how passing arguments as an array fixes it.
- Track
- Web security
- Estimated time
- 25 min
- Level
- Intermediate
Before this lesson
Command injection happens when an application builds a shell command out of what you typed. The shell is a language too: ;, |, $() and backticks all mean “run a command”. So if your input lands on a shell line, you can stop being data and start being commands — running with the server’s privileges.
What is the vulnerability?
A diagnostics feature pings a host you give it:
exec('ping -c 1 ' + req.body.host);Type 8.8.8.8 and the server runs ping -c 1 8.8.8.8. Type 8.8.8.8; cat /flag and it runs:
ping -c 1 8.8.8.8; cat /flagThe ; ends the ping and starts a second command. The shell happily runs cat /flag and hands you the file. The server became your shell.
Why does it happen?
Functions like exec take one string and give it to a shell to interpret. The shell was designed to chain and expand commands, so every one of its operators is a weapon when your input reaches that line as text.
Why stripping separators fails
The tempting fix is to remove ; and &&. But the shell has many more ways to chain and substitute:
| cmd $(cmd) `cmd` <newline>cmd & cmd ; cmdBlacklisting a few leaves the rest, and you will bypass exactly that gap in the medium lab. Hiding the output does not help either: in the hard lab the command runs With no visible output, so the result is extracted through a side channel — a file you can read, a time delay, or an outbound request., and you exfiltrate the secret by writing it to a log you can read.
How to prevent it
// Pass arguments as an array: there is no shell line, so nothing to inject into.
if (!/^[a-z0-9.-]+$/i.test(req.body.host)) return res.status(400).end();
execFile('ping', ['-c', '1', req.body.host]);Do not go through a shell. Use an API that takes the program and its arguments as a separate array (execFile, spawn without shell: true) — the argument is passed to the program directly, never parsed for operators. Better still, avoid shelling out at all and use a library. If a shell is truly unavoidable, validate against a strict allowlist, and run the service with the least privilege so a slip does limited harm.
Labs
A command inside a command
SolvedEasyBrowser lab
Run an extra command that reads the flag file and shows its content.
Enter the labLockedIt strips one separator and forgets the rest
SolvedMediumBrowser lab
Read the flag file again, bypassing the stripping of ; and &&.
Enter the labLockedInjection with no output
SolvedHardBrowser lab
Read the flag secret even though the tool shows no output (blind injection).
Enter the labLocked
