Command Injection Logs: What to Watch
Command injection detection from logs — the telemetry that exposes OS command injection, Sigma and Suricata rules, a CVE-2024-3400 case study, and hardening.
Command injection detection comes down to one unmistakable signal: a web or
application service process spawning a shell. Web servers serve pages; they do not
run bash, sh, cmd.exe, or powershell. When nginx, java, or php-fpm
launches a shell interpreter, that is command injection in progress. The web-tier
signatures catch the noisy attempts; the process-creation signal catches the one
that worked. This guide ships both.
OS command injection turns a web request into code execution with the privileges of the service. It maps to A03:2021 — Injection and remains a favorite of both nation-state and opportunistic attackers. The 2024 PAN-OS flaw CVE-2024-3400 — a CVSS 10.0 command injection in GlobalProtect, exploited in the wild as Operation MidnightEclipse — is the worked example here.
What is OS command injection?
OS command injection is an attack where untrusted input is passed into a shell the
application invokes, so the attacker’s text becomes additional operating-system
commands. The app meant to run ping <host>; the attacker supplies
8.8.8.8; cat /etc/passwd and the shell happily runs both. Unlike
SQL injection, which stays in
the database, command injection lands the attacker a shell on the host.
The payoff is code execution as the service account, which is why it maps to T1190 — Exploit Public-Facing Application for entry and T1059 — Command and Scripting Interpreter for the shell it spawns.
What are the types of command injection?
Three variants matter for detection, and each surfaces in a different place.
| Variant | What the attacker sees | Telemetry fingerprint | Best detection layer |
|---|---|---|---|
| In-band (results returned) | Command output in the HTTP response | Shell metacharacters in params + odd response | Web logs |
| Blind | No output; infers success | Time delays, or out-of-band DNS/HTTP callbacks | Egress + process telemetry |
| Out-of-band | Forces a callback to attacker host | DNS/HTTP to a freshly seen domain from the web host | DNS + egress logs |
The lesson: web parameters catch in-band attempts, but blind and out-of-band injection only show up in process-creation and egress telemetry. That is why the shell-spawn signal is the backbone of command injection detection.
How to detect command injection
Layer it from the web request to the shell it spawns.
The backbone: a service process spawning a shell
This is the highest-fidelity rule you can deploy, and it catches every variant regardless of how the payload arrived — the same process-creation logic behind SQL injection detection’s database-shell rule.
title: Web Service Process Spawning a Shell Interpreter
id: 3a7c2e91-darkpwn-illustrative
status: experimental
logsource:
category: process_creation
detection:
parent:
ParentImage|endswith: ['\nginx.exe','\httpd','\apache2','\java.exe','\php-fpm','\w3wp.exe']
child:
Image|endswith: ['\bash','\sh','\cmd.exe','\powershell.exe','\python','\perl']
condition: parent and child
falsepositives:
- CGI apps that legitimately shell out (allowlist the specific parent/child pair)
level: high The same backbone rule on Splunk and Sentinel
index=* (sourcetype=sysmon EventCode=1) OR (sourcetype=linux_audit type=EXECVE)
| eval parent=lower(ParentImage), child=lower(Image)
| where match(parent, "(nginx|httpd|apache2|java|php-fpm|w3wp|node|puma|gunicorn)")
AND match(child, "(\\\\|/)(bash|sh|dash|zsh|cmd\.exe|powershell\.exe|python[0-9.]*|perl)$")
| eval risk=case(
match(CommandLine, "(?i)(curl|wget|nc |ncat|base64\s+-d|/dev/tcp)"), "critical",
true(), "high")
| stats values(CommandLine) as cmds, min(_time) as first, count by Computer, parent, child, risk
| where count > 0 let webParents = dynamic(["nginx","httpd","apache2","java","php-fpm","w3wp","node"]);
let shells = dynamic(["bash","sh","dash","cmd.exe","powershell.exe","python","perl"]);
DeviceProcessEvents
| where InitiatingProcessFileName has_any (webParents)
| where FileName has_any (shells)
| extend Risk = iff(ProcessCommandLine has_any ("curl","wget","base64 -d","/dev/tcp","ncat"),
"Critical", "High")
| project Timestamp, DeviceName, InitiatingProcessFileName, FileName,
ProcessCommandLine, AccountName, Risk
| order by Timestamp desc Both encode the same escalation logic: the shell spawn is the alert, and what the shell reaches for immediately afterwards decides the severity.
Web tier: shell metacharacters in parameters
A triage signal for the in-band variant — pair it with rate thresholds so a single scanner request does not page anyone.
title: Suspicious Command Tokens in Web Request Parameters
id: 5b9d1f64-darkpwn-illustrative
status: experimental
logsource:
category: webserver
detection:
selection:
cs-uri-query|contains: [';cat ',';id',';whoami','|nc ','$(', '`id`','&&curl','%3Bcat']
condition: selection
falsepositives:
- Scanners and DAST runs; allowlist their source IPs
level: medium For perimeter IDS, a Suricata rule flags shell metacharacters in request bodies (reserve SID 1000004+):
alert http any any -> $HTTP_SERVERS any (
msg:"DARKPWN Shell metacharacters in HTTP request (possible command injection)";
flow:established,to_server; http.uri;
pcre:"/(\x3b|\x7c|%3b|%7c|\x60|\x24\x28)\s*(id|whoami|cat|curl|wget|nc)\b/i";
classtype:web-application-attack; sid:1000004; rev:1;) How do you detect command injection in containers?
Most command-injection guidance implicitly assumes a long-lived server with an endpoint agent on it. Modern web tiers frequently are not that, and the detection has to move accordingly — but containers also make this detection easier, not harder, if you set them up for it.
Where the telemetry comes from. There is no Sysmon on Linux, so process-creation events come
from one of three places: auditd with an execve rule, an eBPF-based sensor (Falco, Tetragon,
or your EDR’s Linux agent), or the container runtime’s own audit trail. eBPF is the usual
answer at scale because auditd under high process churn is expensive and drops events under
load — which is exactly when you need them.
Why containers improve the signal. The parent/child model gets much cleaner. A container image has a known entrypoint and a small, enumerable set of processes it should ever run. In a conventional server the baseline is muddy — cron, admin sessions, agents, maintenance scripts all spawn shells legitimately. In a container running one application, any shell execution is anomalous by construction.
Practical container detection rules, in order of value:
- Shell execution inside a container that has no interactive workload. Highest fidelity. Allowlist your healthcheck and init commands explicitly by full argument vector.
kubectl execinto a production pod. Legitimate but should be rare, ticketed, and attributable. Alert on every one and reconcile against change records.- Any process whose executable path is outside the image layers — a binary written at runtime is a dropped payload, and in an immutable-filesystem container it should be impossible.
- Outbound connections from a pod to a destination outside its service mesh policy. This is the blind and out-of-band variant surfacing, and it works even when process telemetry is missing.
Run the container filesystem read-only where you can. It converts step 3 from a detection into a prevention, and the failed write attempt is itself a loggable event.
What telemetry do you need to detect command injection?
| Platform | Process creation source | Egress source | Gotcha |
|---|---|---|---|
| Windows server | Sysmon EID 1 or Security 4688 | Sysmon EID 3, firewall | 4688 lacks parent command line |
| Linux VM | auditd execve, or eBPF sensor | Conntrack, firewall, DNS logs | auditd drops events under load |
| Container / Kubernetes | eBPF (Falco/Tetragon), runtime audit | CNI flow logs, service mesh | Node agent must see all namespaces |
| Managed PaaS | Platform application logs only | Platform egress logs | Often no process telemetry at all |
| Appliance (VPN, firewall) | Vendor logs only | Vendor logs, upstream flow | Frequently unparsed and unforwarded |
The bottom two rows are where most organizations are genuinely blind. On managed PaaS you often cannot get process telemetry at all, which makes the web-tier and egress signals your only coverage — plan the detection around that constraint rather than pretending the backbone rule applies. For appliances, the fix is usually just forwarding logs nobody ever ingested, which is cheap work with an outsized payoff given CVE-2024-3400.
Which false positives will you actually see?
| False positive | Which rule it hits | Why it happens | Resolution |
|---|---|---|---|
| CGI and legacy apps | Shell spawn | They legitimately shell out per request | Allowlist the exact parent/child pair, not the parent |
| Healthcheck scripts | Shell spawn (container) | Probe runs a shell command | Allowlist the full argument vector |
| Deployment and init containers | Shell spawn (container) | Setup genuinely runs shells | Scope the rule to the app container only |
| DAST and vulnerability scanners | Web metacharacters | They send injection payloads deliberately | Allowlist scanner source IPs, keep the rule enabled |
| Log-processing pipelines | Web metacharacters | Someone’s log line contains ;cat | Match on request parameters, not arbitrary body text |
Legitimate kubectl exec | Container exec | Engineers debug production | Alert anyway; reconcile against change tickets |
| Backup and cron jobs | Shell spawn | Scheduled maintenance spawns shells | Time-plus-account scoped filter, reviewed quarterly |
| Monitoring agents | Shell spawn | Some agents shell out to collect metrics | Allowlist by signed binary path |
Allowlisting the parent alone is the mistake that hollows this rule out. Excluding java
because one legacy application shells out disables the backbone detection for every Java service
you run. Allowlist the specific parent and child and, where you can, the command shape —
so the exception covers the known-good behaviour rather than the whole process class.
How do you triage a command injection alert?
- Capture the full command line and the process tree before anything else. Containers get rescheduled and short-lived processes vanish; if you isolate first you may lose the evidence entirely. This is the one detection class where a brief capture step precedes containment.
- Identify the entry request. Correlate the process timestamp against web access logs for that host or pod within a few seconds. This gives you the vulnerable endpoint and parameter.
- Read what the shell did next. Child processes and outbound connections separate a blocked
probe from an active compromise.
curl,wget,base64 -d, or/dev/tcpmean stage two. - Check for persistence. Look for new files in web-accessible directories (web shells), new cron entries or systemd units, and modified startup scripts.
- Establish the service account’s blast radius. What can that identity reach — databases, cloud roles, internal APIs? On a cloud host, check whether the instance metadata service was queried, which indicates credential theft. This is the same pivot covered in SSRF detection.
- Contain. Isolate the host or cordon and drain the node. For a container, capture the filesystem diff first, then kill the pod — the replacement is clean if the image is.
- Rotate every credential the process could read. Environment variables, mounted secrets, and any cloud role attached to the workload.
- Fix the endpoint. Until the underlying code stops building shell commands from input, the host is going to be re-exploited within hours of coming back.
Step 7 is the one most often under-scoped. Injected code runs as the service, which means it could read every environment variable and mounted secret in that workload — not just the ones the vulnerable endpoint used. Rotate all of them.
How to test your command injection detection
Validate in a lab you own, never against a third party:
- Stand up a deliberately vulnerable app (DVWA’s command-injection module, or a throwaway endpoint that shells out).
- Trigger the in-band variant and confirm the web-token and process-spawn rules fire.
- Trigger a blind/out-of-band variant (a callback to a host you control) and confirm the egress/DNS signal fires.
- Replay benign traffic and your DAST scan to confirm the rules stay quiet, then record the false-positive sources in each rule.
How to prevent command injection
- Run services at least privilege so a shell that does spawn is boxed in (NIST 800-53 AC-6).
- Patch internet-facing appliances fast — CVE-2024-3400 was added to CISA’s KEV catalog and exploited within days of disclosure.
- Default-deny egress from web hosts so blind/out-of-band callbacks become blocked, logged events.
The controls ranked by what they actually buy you
| Control | Stops the bug class? | Effort | Notes |
|---|---|---|---|
| Argument-array APIs (no shell) | Yes | Low per call site | The only true fix. execFile, subprocess.run([...]) |
| Remove the shell from the image | Yes, in practice | Medium | Distroless/scratch; also makes detection near-perfect |
| Read-only root filesystem | No, but blocks stage two | Low | Payload cannot be written to disk |
| Default-deny egress | No, but blocks stage two | Medium | Turns blind injection into a logged, failed callback |
| Least-privilege service account | No, limits blast radius | Medium | Decides how bad the incident is |
| Input allowlisting | Partially | Medium | Useful defence in depth; brittle alone |
| WAF signatures | No | Low | Catches noise; encoded and second-order variants pass |
Read that table top-down when deciding where to spend. The first two rows remove the vulnerability class; everything below them limits consequences. A team that has deployed only the bottom row has bought detection latency, not safety — and CVE-2024-3400 is the demonstration, because a WAF watching query strings had nothing to match against a payload hidden in a filename.
On the argument-array fix specifically: the distinction is whether a shell is involved at
all. subprocess.run("ping " + host, shell=True) hands the string to /bin/sh, which is where
metacharacters gain meaning. subprocess.run(["ping", host]) executes the binary directly with
host as one opaque argument — a semicolon in it is just a character in a hostname, and the
attack has nowhere to land. The same distinction exists in every language: Node’s execFile
versus exec, Java’s ProcessBuilder with an argument list versus a concatenated string, Go’s
exec.Command with separate arguments. This is a mechanical code-review check that scales
better than any signature.
Common command injection detection mistakes
- WAF-only coverage. Parameter signatures miss filename-smuggled and blind variants — exactly the CVE-2024-3400 pattern.
- No process-creation telemetry. Without it, the backbone signal is invisible.
- Ignoring egress. Blind and out-of-band injection live in DNS and outbound logs, not request logs.
- Untuned metacharacter rules that page on every scan and get muted.
- Allowlisting the parent process alone. Excluding
javabecause one legacy app shells out disables the backbone rule for every Java service you run. - Assuming the server model. On managed PaaS there is often no process telemetry at all, so a detection plan built around the shell-spawn rule silently covers nothing.
- Isolating before capturing. Containers get rescheduled and short-lived processes vanish; capture the process tree first or lose the evidence.
- Rotating only the credentials the endpoint used. Injected code reads every environment variable and mounted secret in the workload.
Command injection detection checklist
- Forward process-creation, web access, and DNS/egress logs to the SIEM.
- Deploy the service-process-spawns-a-shell rule — the highest-fidelity signal.
- Add the web-parameter metacharacter rule with a per-IP rate threshold.
- Add the Suricata metacharacter rule where you have TLS visibility.
- Chain shell-spawn alerts to child processes (
curl/wget/base64) and new egress. - Default-deny egress from web hosts; alert on new outbound destinations.
- Replace shelled-out commands with argument-array APIs in code review.
- Run services at least privilege; patch internet-facing appliances on a KEV cadence.
- Fire every rule in a lab against true-positive and benign traffic.
- Allowlist by parent and child pair — never by parent process alone.
- On containers, alert on any
execvethat is not the known entrypoint; allowlist healthchecks by full argument vector. - Ship distroless or shell-free runtime images wherever the workload allows.
- Set container root filesystems read-only so a dropped payload cannot be written.
- Alert on every
kubectl execinto production and reconcile against change records. - Confirm appliance and PaaS logs are actually forwarded and parsed — this is the most common blind spot and the cheapest to fix.
Item 15 deserves the last word. The CVE-2024-3400 intrusions were visible in appliance logs that most organizations had never forwarded to a SIEM. There was no detection engineering problem to solve — the evidence existed, unread, on the device. Ingesting logs you already generate is the highest return-on-effort work in this entire guide.
What does normal look like on this host?
Every signal above is only meaningful against a baseline, and the baseline is the part teams skip. A web server that never spawns a shell is a strong detection; a build server that spawns shells constantly makes the same rule useless noise.
Collect two weeks of process-creation telemetry per host role before tuning, and write down what the normal parent-child tree actually looks like. You will usually find that the interesting signal is not “a shell spawned” but “a shell spawned from a process that has never spawned one before” — and that distinction is only available if you know the prior.
Segment the baseline by role, not by fleet. Web tier, build tier, and admin jump hosts have wildly different legitimate behaviour, and a single fleet-wide threshold ends up tuned to the noisiest role, which is exactly the role you were least worried about.
The takeaway
Command injection detection is a behavior problem, not a string-matching one. Watch for a service process spawning a shell, chain it to what the shell does next, and remove the bug class by never building shell commands from untrusted input. Command execution also arrives straight from the keyboard — see USB Rubber Ducky detection patterns. Continue across the web-application attack surface with SQL injection detection and SSRF detection, or follow the data back out of the network with DNS tunneling detection. Or browse the full Detection Engineering pillar.
Training & tools referenced
Disclosure: Some links below are affiliate links. If you buy through them, darkpwn may earn a commission at no extra cost to you. We only recommend training and tools we actually use in our own lab, and affiliate links never influence editorial coverage.
- TryHackMeAuthorized labs to practice command-injection detection against real telemetrySecurity TrainingStart training
Frequently asked questions
How do you detect command injection in logs?
The highest-fidelity signal is a web or application service process (nginx, apache, java, php-fpm) spawning a shell interpreter such as bash, sh, cmd.exe, or powershell. At the web tier, watch request parameters for shell metacharacters and command tokens, and watch for unexpected outbound connections from the web host.
What is the difference between command injection and code injection?
Command injection runs operating-system commands through a shell the application invokes (for example, passing input to system() or a shell call). Code injection runs code in the application's own language or runtime. Command injection maps to MITRE ATT&CK T1059; the detection signal is a service process spawning a shell.
Which MITRE ATT&CK technique covers command injection?
Command injection against an internet-facing app maps to T1190 (Exploit Public-Facing Application) for the entry and T1059 (Command and Scripting Interpreter) for the shell execution it produces.
How do you detect command injection in containers?
Containers make this easier, not harder. A container image has a known entrypoint and a small set of processes it should ever run, so any shell execution inside an application container is anomalous by construction. Collect process events with an eBPF sensor such as Falco or Tetragon rather than auditd, which drops events under high process churn, and allowlist healthcheck and init commands by their full argument vector.
Does using distroless images stop command injection?
Largely, in practice. If the runtime image contains no shell, an injected command has no interpreter to invoke and the common exploitation path stops working. It also sharpens detection: the alert condition becomes any process execution that is not the known entrypoint, which has almost no false positives. It does not fix the underlying code defect, so pair it with argument-array APIs.
Can a WAF stop command injection?
A WAF blocks obvious shell-metacharacter payloads and is worth running, but encoding, blind, and second-order variants bypass tuned-down rules. The durable control is avoiding shell calls with untrusted input; the durable detection is process-creation telemetry showing a service spawning a shell.