Command Injection happens when attacker-controlled input becomes part of an operating system command.
The primitive is shell interpretation. The application intends to run one command, but the shell receives user-controlled characters that change where that command ends and what runs next.
This is different from SQL injection and XSS. The dangerous parser is not the database and not the browser. The dangerous parser is the operating system shell.
Common command execution sinks include:
os.systemsubprocess.run(..., shell=True)subprocess.Popen(..., shell=True)popen- Backtick execution in scripting languages
- Template helpers that call shell commands
- Admin panels that run diagnostics
- Backup, archive, conversion, upload, image, and PDF processing features
Common shell metacharacters include:
;
&
&&
||
|
`
$()
>
<
newline
Common abuse scenarios include:
- Running local commands as the web server user
- Reading environment details
- Discovering the current user and working directory
- Proving code execution with
id,whoami, oruname - Chaining into file reads when the process has access
- Chaining into persistence or lateral movement in real incidents
When enumerating command injection opportunities, identify:
- Features that run diagnostics such as ping, lookup, trace, or status checks
- File conversion features
- Image processing features
- PDF generation features
- Archive extraction features
- Backup and restore features
- Import/export features
- Admin-only maintenance tools
- Parameters named
host,ip,domain,path,file,url,command,cmd,format, ortemplate - Whether the application returns command output, timing differences, or only success/failure
Types
Direct command injection returns command output in the response.
target=localhost; id
Blind command injection does not return output. Impact is proven through timing, side effects, logs, or controlled callbacks inside an authorized lab.
target=localhost; sleep 3
Argument injection happens when the application avoids the shell but still lets the attacker control dangerous arguments to a trusted program.
program --safe-option attacker-controlled-option
Argument injection is not always command execution. It still matters because many programs have powerful options.
Enumeration
Start with a normal value:
localhost
127.0.0.1
example.local
Then test one separator at a time:
localhost; id
localhost && id
localhost | id
localhost || id
localhost
id
Use harmless proof commands:
id
whoami
pwd
uname -a
Compare:
- Status code
- Response length
- Command output
- Error output
- Response time
- Audit logs
Interesting findings include:
uid=in the response- The web server user's name in the response
- Different output after adding a shell separator
- Error messages from
/bin/sh - Delay after injecting
sleep - Application logs showing the full command string
Once execution is confirmed, keep the proof small. The goal is to prove that the shell is interpreting user input.
Exploit
- Find a feature that runs a system command.
POST /command-injection/basic
target=localhost
- Confirm the normal command works.
127.0.0.1 localhost
- Add a shell separator and a harmless proof command.
POST /command-injection/basic
target=localhost; id
- Confirm command execution.
uid=1000(kali) gid=1000(kali)
- Test alternate separators when a blacklist appears.
localhost | id
localhost && whoami
- Stop once impact is proven.
The exploit is not the command itself. The exploit is proving that user input crossed into shell syntax.
Remediation
Patch command injection by removing the shell boundary:
- Do not use
shell=Truewith user-controlled data. - Use argument arrays such as
["getent", "hosts", target]. - Validate input with strict allowlists.
- Treat every parameter used near a command as untrusted.
- Keep user-controlled labels, filenames, paths, and options out of shell strings.
- Use built-in language libraries instead of shelling out when possible.
- Run the web application with least privilege.
- Add timeouts to subprocess calls.
- Log denied command-like input.
Safe subprocess pattern:
subprocess.run(
["getent", "hosts", target],
shell=False,
capture_output=True,
text=True,
timeout=3,
)
Quoting helps only when a shell is still required. It is not the preferred fix when the command can be expressed as an argument list.