Skip to content
العربية

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.

Estimated time
25 min
Level
Intermediate
Your progress in this track0%

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:

ping.js
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:

what the shell sees
ping -c 1 8.8.8.8; cat /flag

The ; 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:

all run a second command
| cmd     $(cmd)     `cmd`     <newline>cmd     & cmd     ; cmd

Blacklisting 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

safe.js
// 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.

Review questionA ping tool strips ';' and '&&' from the host before running exec('ping ' + host). Is it safe?

Labs

  • A command inside a command

    EasyBrowser lab

    Run an extra command that reads the flag file and shows its content.

  • It strips one separator and forgets the rest

    MediumBrowser lab

    Read the flag file again, bypassing the stripping of ; and &&.

  • Injection with no output

    HardBrowser lab

    Read the flag secret even though the tool shows no output (blind injection).