Detection
Find a parameter likely to feed a shell. Append a separator and a benign command (id, whoami, hostname). If the response changes - extra output appended, command output replacing the original, or a measurable delay - you have injection.
<PARAM>=<original>;id<PARAM>=<original>%0aid<PARAM>=<original>|id<PARAM>=<original>&&id<PARAM>=<original>`id`<PARAM>=<original>$(id)Success indicator: uid= in the response body, or a hostname/username that didn’t appear before.
Where to look
Section titled “Where to look”Any input that gets converted, pinged, archived, scanned, rendered, fetched, or otherwise touches the filesystem is a candidate. Common patterns:
- Network utilities - host pingers, traceroute, DNS lookup, port checkers
- File processors - PDF/image generators, archive extractors, document converters
- Diagnostic endpoints - admin-only “test connection” buttons, health checks
- DevOps and CI features - git operations, deploy hooks, build triggers
- Anything calling
ffmpeg,imagemagick,pandoc,wkhtmltopdf,git,tar,unzip,curl
Separator reference
Section titled “Separator reference”The separator determines whether the original command runs, whether yours runs, and whose output gets returned. Pick based on what the back-end is doing.
| Separator | URL-encoded | Original runs? | Injected runs? | Output | Notes |
|---|---|---|---|---|---|
; | %3b | Yes | Yes | Both, in order | Fails on cmd.exe; works in PowerShell |
\n | %0a | Yes | Yes | Both, in order | Universal; rarely blacklisted because legitimate input may need it |
& | %26 | Yes (background) | Yes | Both, injected often shown first | Background original on Linux; sequential on Windows |
| | %7c | Yes | Yes | Injected only (stdout piped) | Original’s output is consumed by your command’s stdin |
&& | %26%26 | Yes | Only if original succeeds | Both | Use when original is expected to succeed |
|| | %7c%7c | Yes | Only if original fails | Injected only | Useful when your injection breaks the original - pair with malformed input |
` ` | %60%60 | Substituted | Yes (first) | Original runs with your output as part of its argv | Linux/macOS only |
$() | %24%28%29 | Substituted | Yes (first) | Same as backticks | Linux/macOS only; nests cleanly |
Probing workflow
Section titled “Probing workflow”-
Establish baseline. Send the parameter’s expected input (
127.0.0.1,example.com, etc.) and record the exact response - body, status, length, time-to-first-byte. -
Send each separator with
id. One request per separator. Look foruid=,gid=in the body. URL-encode if the separator is interpreted by the URL parser.GET /check?<PARAM>=127.0.0.1;id HTTP/1.1GET /check?<PARAM>=127.0.0.1%0aid HTTP/1.1GET /check?<PARAM>=127.0.0.1%26%26id HTTP/1.1 -
No reflected output? The back-end may discard stdout, or output may be suppressed. Move to Blind & OOB - confirm with timing or callback.
-
Hostile errors? “Invalid input” or a 4xx without your output reaching the shell suggests filtering. Move to Filter bypass.
Front-end validation bypass
Section titled “Front-end validation bypass”A page that rejects payloads without making an HTTP request is validating in JavaScript. Bypass by sending the request directly.
-
Confirm front-end-only. Open DevTools → Network. Click submit with a malicious payload. If no request is sent on rejection, validation is client-side.
-
Capture a clean request. Submit valid input, intercept in Burp or ZAP, send to Repeater (
Ctrl+R). -
Inject into the captured request. Edit the parameter in Repeater. URL-encode the separator (
Ctrl+Uon the selection in Burp). -
Send and inspect. The back-end has no idea the front-end existed.
Reading the response
Section titled “Reading the response”What the response looks like depends on the separator and how the back-end captures stdout.
- Both outputs visible -
;,\n,&&typically return original output followed by yours. Theidline will appear after the ping output or whatever ran first. - Only your output -
|,||, sub-shells often replace the visible output entirely. Cleaner result, sometimes more reliable. - Output truncated - back-end may capture only the first line, or only stderr, or pipe through
grep. Try; id 2>&1or; echo START; id; echo ENDto delimit. - No output at all but status changed - back-end runs the command but discards output. You have RCE; you need blind techniques to read results.
Common failure modes
Section titled “Common failure modes”- Separator URL-decoded twice. Some frameworks decode the parameter before passing to the shell, so
%3bbecomes;correctly. Others decode at the WAF layer too, causing double-decode mishandling. If;works but%3bdoesn’t (or vice versa), try both encodings. - Spaces required, spaces blocked. Your separator works but
idruns without arguments - anything more elaborate fails on space filtering. Jump to filter bypass for${IFS},%09,{a,b}. - Original command’s exit code matters. With
&&your payload runs only if the original succeeded. If you malformed the input to inject, the original failed and&&won’t trigger. Use||or;instead. - Windows cmd.exe and
;. Semicolon is not a separator in cmd.exe - it’s literal. Use&or&&instead. PowerShell accepts;. - Output captured to a file you can’t read. Back-end writes stdout to
/tmp/result.logand shows you a parsed subset. Inject; cat /tmp/result.logor write your output where it gets displayed.
The detection methodology is uniform across PHP, Node, Python, Ruby, Java, and .NET back-ends - the separator semantics belong to the shell (bash, sh, dash, cmd.exe, PowerShell), not the calling language. The shell is determined by what the back-end invokes: system() and exec() in PHP go through /bin/sh; Node’s child_process.exec uses /bin/sh -c on Linux and cmd.exe /d /s /c on Windows; Python subprocess with shell=True uses /bin/sh -c or cmd.exe. Identify the shell from server fingerprinting (Server: header, error pages, file paths in stack traces) and pick separators accordingly.