OS Command Injection: Detection, Blind Injection, and Prevention

Tested on Kali GNU/Linux Rolling 2026.2 (kali-rolling)
Package dvwa 2.5-0kali1 on http://127.0.0.1:42001
Lab environment Pentest lab
Cert prep CompTIA PenTest+ PT0-003: injection testing against OS command processors
Privilege Normal user for browser and curl; sudo for dvwa-start if the service is stopped
Scope What OS command injection is, finding shell-backed features, safe confirmation on DVWA, blind/timing detection, Linux vs Windows command context, Burp Repeater checks, impact tied to process identity, and prevention. Does not teach payload catalogs or post-exploitation after a shell.
Related guides Install DVWA on Kali Linux
Short on time?Beta Get the gist of this 8-minute read in seconds, then ask follow-up questions

OS command injection, also called shell injection, is a web vulnerability where attacker-controlled input changes an operating-system command executed by the server.

In practice that usually means:

  • The app pastes your input into a shell command string
  • The shell treats part of that input as syntax, not only as data
  • You can start a second command or change how the first one runs

This lesson uses DVWA Command Injection at Low on http://127.0.0.1:42001. For modules and levels, use the DVWA learning map. Template injection is a different flaw; see server-side template injection.

IMPORTANT
Run these steps only on systems you own or are authorized to test. Keep DVWA on localhost or host-only per rules of engagement. Prefer benign proofs such as id or a short sleep. Do not use destructive commands.

What OS command injection is

Vulnerable apps often wrap a system utility and glue user input onto the command line:

text
user input → string built for shell/system → OS runs attacker-controlled command syntax

Flow from user input into a shell-built system command and OS execution

On DVWA Low, the module builds a ping line like this (simplified from the package source):

php
$cmd = shell_exec( 'ping  -c 4 ' . $target );

$target comes from the ip request parameter. If you can end the ping argument and start another command, you have OS command injection.

Related idea: argument injection. Even without a second shell command, unsafe input can still change options on the intended program. Skipping the shell helps against chaining, but you still need to allowlist allowed arguments.

How this differs from nearby flaws:

  • XSS: the browser runs markup or script
  • SSTI: a template engine on the server evaluates directives
  • Command injection: the OS shell (or similar) runs the command

Lab setup

Start DVWA if needed:

bash
sudo dvwa-start

When this finishes (or if DVWA was already running), the local site should answer on port 42001.

Then in the browser:

  1. Open http://127.0.0.1:42001
  2. Log in with admin / password
  3. Left menu → DVWA Security → Low → Submit
  4. Left menu → Command Injection

For Terminal checks, copy PHPSESSID from DevTools and set:

bash
export DVWA_COOKIE='PHPSESSID=your_session_here; security=low'

export prints nothing on success. Confirm the variable in the same Terminal:

bash
echo "${DVWA_COOKIE}"

You should see your real PHPSESSID=...; security=low string, not the placeholder text. Use the curl command for -X POST and --data-urlencode if you need flag reminders.


Find command-execution inputs

Look for features that already call OS tools:

  • Host lookup / ping / traceroute
  • Format conversion or image processing wrappers
  • Archive extractors and "system" utilities in admin panels
  • Backup, mail, or print helpers that shell out

On DVWA, that feature is the ping form on Command Injection.

Before you try separators:

  • Send a unique harmless marker
  • Watch command output, errors, and response time
  • Use Burp HTTP history to find the exact parameter name

Confirm the vulnerability safely

Submit a normal IP first so you know what healthy output looks like:

bash
curl -s -H "Cookie: ${DVWA_COOKIE}" -X POST \
  -d 'ip=127.0.0.1&Submit=Submit' \
  "http://127.0.0.1:42001/vulnerabilities/exec/" \
  | python3 -c 'import sys,re; m=re.search(r"<pre>(.*?)</pre>", sys.stdin.read(), re.S); print(m.group(1)[:280] if m else "no pre")'
output
PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.051 ms
64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.078 ms
64 bytes from 127.0.0.1: icmp_seq=3 ttl=64 time=0.062 ms
64 bytes from 127.0.0.1: icmp_seq=4 ttl=64 time=0.071 ms

What this shows:

  • DVWA ran ping -c 4 against 127.0.0.1
  • Four replies means the baseline path works
  • Keep this output as your "normal" reference

Next, append a second command with ;. On this Linux lab, ; id is a clear, low-impact proof:

bash
curl -s -H "Cookie: ${DVWA_COOKIE}" -X POST \
  --data-urlencode 'ip=127.0.0.1; id' \
  -d 'Submit=Submit' \
  "http://127.0.0.1:42001/vulnerabilities/exec/" \
  | python3 -c 'import sys,re; m=re.search(r"<pre>(.*?)</pre>", sys.stdin.read(), re.S); print(m.group(1) if m else "no pre")'
output
PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.053 ms
64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.053 ms
64 bytes from 127.0.0.1: icmp_seq=3 ttl=64 time=0.062 ms
64 bytes from 127.0.0.1: icmp_seq=4 ttl=64 time=0.047 ms

--- 127.0.0.1 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3057ms
rtt min/avg/max/mdev = 0.047/0.053/0.062/0.005 ms
uid=118(_dvwa) gid=120(_dvwa) groups=120(_dvwa)

What this shows:

  • Ping still ran
  • id also ran and printed uid=...(_dvwa)
  • Your input changed command syntax, not only the ping target

In the browser: Command Injection → enter 127.0.0.1; id → Submit. You should see the same uid= line in the page.


Blind command injection

Sometimes the OS still runs your command, but the app never returns stdout. That is blind command injection. You infer success from timing or another controlled side effect.

On DVWA Low, stdout is visible, so you can still practice timing. Measure a normal ping first:

bash
/usr/bin/time -f 'elapsed=%e' curl -s -o /dev/null -H "Cookie: ${DVWA_COOKIE}" -X POST \
  --data-urlencode 'ip=127.0.0.1' \
  -d 'Submit=Submit' \
  "http://127.0.0.1:42001/vulnerabilities/exec/"
output
elapsed=3.13

About three seconds is normal here for ping -c 4. Now add a short delay after the ping:

bash
/usr/bin/time -f 'elapsed=%e' curl -s -o /dev/null -H "Cookie: ${DVWA_COOKIE}" -X POST \
  --data-urlencode 'ip=127.0.0.1; sleep 2' \
  -d 'Submit=Submit' \
  "http://127.0.0.1:42001/vulnerabilities/exec/"
output
elapsed=5.14

The extra ~2 seconds matches sleep 2, so the second command ran even if you only looked at timing.

When stdout is missing, blind checks usually look like:

  • Timing: delay execution and compare response time to a baseline
  • Output redirection: write output somewhere the lab app can safely show
  • Out-of-band (OAST): trigger DNS/HTTP to an authorized monitor endpoint

Remember:

  • No printed output does not mean "safe"
  • Keep delays short in shared labs
  • Use only authorized callback endpoints for OAST

Windows vs Linux command context

Topic Typical Linux/macOS shell Typical Windows cmd
Common separators ;, &&, ||, | &, &&, ||, |
Delay proof sleep N timeout /T N (lab-dependent)
Identity proof id, whoami whoami
Quoting / escaping Shell-dependent Different metacharacters and quoting rules

Practical tips:

  • Confirm the OS first; separators differ
  • Do not memorize a giant payload list
  • Test only what your scope allows

DVWA picks ping vs ping -c 4 from php_uname.


Burp Repeater workflow

Use Burp Suite when you want to edit the POST body without the browser form:

  1. Proxy DVWA with intercept off
  2. Submit a normal IP such as 127.0.0.1 on Command Injection
  3. Find the request in Proxy → HTTP history
  4. Send to Repeater
  5. Change ip to a unique marker, then to 127.0.0.1; id

Compare:

  • Status code
  • Response length
  • Whether your marker or uid= text appears in the body
  • Whether filters strip characters from the parameter

Why impact varies

On this lab, the process identity was _dvwa, not root. Command injection gives you only what that OS user can do:

  • Read files it can read
  • Reach networks it can reach
  • Write where it has permission

That often leads into local privilege escalation next, not an automatic full compromise. See Linux privilege escalation after a low-privilege foothold.


Prevention

Best fix: do not build a shell command string from user input.

  • Prefer libraries/APIs over system, shell_exec, or concatenated Runtime.exec
  • If you must run a binary, call it without a shell, pass arguments separately, and allowlist each value
  • Skipping the shell blocks many chaining attacks, but not dangerous program options (argument injection)
  • Deny-lists of ; or && are not the main control; incomplete stripping still fails

Summary

OS command injection (shell injection) is untrusted input reaching a shell or system command as syntax. On DVWA Low, a normal ping is the baseline, 127.0.0.1; id proves a second command ran as _dvwa, and a short sleep shows how blind cases can still be timed.

Impact follows the process user. Skipping the shell stops many chaining attacks, but you still need to allowlist arguments. When the app evaluates template directives instead of OS commands, use the SSTI lesson instead of shell payloads.


References


Frequently Asked Questions

What is the difference between command injection and code injection?

Command injection targets an OS command processor (a shell or similar). Code injection targets an application language runtime or interpreter. Related, but different sinks and fixes.

Does missing command output mean the injection failed?

No. Blind command injection can still execute. Use timing or another authorized side channel before you conclude the parameter is safe.

Is blacklisting ; and && enough?

No. Separators and quoting differ by shell and OS, and incomplete filters are common. Prefer not invoking a shell with concatenated input.

Why did id show _dvwa instead of root?

DVWA's web stack runs the command as the application user. That identity bounds what the injection can do until a separate privilege-escalation path exists.

Finished reading?Beta Get the key takeaways in seconds, then ask follow-up questions
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with 15+ years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)