commix-testbed is a collection of deliberately vulnerable web pages, written by Anastasios Stasinopoulos (@ancst), used to exercise commix's detection and exploitation features - and to learn what command (and code) injection actually looks like in code.
Every page contains the same underlying bug: a request value reaches a shell, or a string the application evaluates as code. What changes from page to page is everything around it - where the value lands, what the application does to it first, and how much of the result comes back. That is what separates an injection point you find in seconds from one that is nearly invisible, and it is why a scanner needs more than one technique.
Each scenario carries an explanation of what the code gets wrong and what the mistake teaches, so the collection reads as a tour of the bug class rather than a pile of targets.
Warning
These pages execute whatever they are given. Run the testbed locally and keep it off any network you do not control.
It is a target, not a demo you leave running.
- Regular parameters -
GET,POSTandPUTbodies, JSON, GraphQL and SOAP/XML documents, Base64 and hex encodings, single and double quoting, HTTP Basic and Digest authentication, and a page where only two of three parameters are validated. - Code evaluation sinks -
eval(),assert(),preg_replace()with an attacker-controlled pattern,create_function(), andstr_replace()sanitising into an evaluated string. - Weak filters - denylisted metacharacters, whitespace rejection, anchored patterns that constrain only one end, lax domain validation, a command-name blocklist, a function-name blocklist that reads the query string and only the query string, and a check that inspects the first of two same-named parameters while the application reads the last. Each one is wrong in a different way.
- HTTP headers -
User-Agent,Cookie,RefererandX-Forwarded-Host, plus a customX-Forwarded-Forheader read for the access log. - Stateful and multi-step - pages that ask for something carried between requests rather than something clever inside one: a login and the session it hands out, anti-CSRF tokens and single-use nonces, a second-order sink that answers on another page, account lockout, a connection held open, an endpoint that refuses at random, a request body sent chunked, non-standard parameter and cookie delimiters, an out-of-band-only sink, a document root that is not the default, output held back until the next request, a branch only a phone reaches, decoy parameters beside the real one, a body that is never percent-decoded, a sink that answers
500while running the command, and aSet-Cookiethat breaks the session if it is stored. - Discovery - pages whose injectable surface is not written where a scan looks for it: a parameter the markup never names, routes that exist only inside the script a page loads, and an API that publishes an OpenAPI description of endpoints nothing links to.
- CTF challenges - five real, named challenges recreated faithfully, filter and language included: YesWeHack Dojo #30 (Terminal Isolation), #36 (Shell Escape) and #41 (Ruby Treasure, the one page here served as a genuine Ruby CGI script rather than PHP standing in for it), a Root-Me filter bypass, and YesWeHack Dojo #46 (Ghost Whisper).
- Python scenarios - the same classic, blind and code-injection bugs written in Python and served as CGI, so a run is not only exercising PHP.
- Ruby scenarios - the same
eval()-on-the-value code injection, in Ruby. - JavaScript scenario - the same
eval()-on-the-value code injection, in Node - run as its own standalone HTTP server rather than CGI under the main site, since the base image's glibc predates any Node build that would still run there. Start it withdocker run -d -p 8087:3000 -v "$PWD/scenarios/javascript:/app:ro" -w /app -e TESTBED_URL=http://localhost:8081 node:20-alpine node server.js, alongside the main container. - Every reported type - classic and blind, command and code injection, so all four combinations commix can report are covered.
Scenarios are labelled with the type commix reports them as, and each page says which option reaches it - several exist to exercise one option and cannot be reached without it. See the techniques wiki page for what those mean and which technique reaches each one.
You can download commix-testbed on any platform by cloning the official Git repository :
$ git clone https://github.com/commixproject/commix-testbed.git commix-testbed
Alternatively, you can use the dockerized version :
$ docker run --rm -p 8080:80 commixproject/commix-testbed
Note
PHP is required for running the testbed. There are no dependencies and no build step. The pages parse on everything from 5.5 to 8.x.
Serve it with PHP's built-in server :
$ php -S 127.0.0.1:8080
Note
Most scenarios shell out to ping, so the host serving them needs it installed - a bare
php:apache image does not have it. The file-based technique needs the web root to be
writable, and the out-of-band technique needs the host to be able to reach the internet.
Four pages need an interpreter older than PHP 8 to be exploitable, though they load on any
version: create_function() was removed in PHP 8, the e regex modifier in PHP 7, and assert()
stopped evaluating a string argument in PHP 8. The published image ships PHP 5.5, so every
scenario works there.
Open http://127.0.0.1:8080 for the index, which lists every scenario alongside an explanation of the flaw behind it.
Point commix at any scenario :
$ python3 commix.py --url="http://127.0.0.1:8080/scenarios/regular/GET/classic.php?addr=127.0.0.1"
Header-based scenarios are only reached at the right --level - 2 covers cookies, 3 covers the
other headers :
$ python3 commix.py --url="http://127.0.0.1:8080/scenarios/user-agent/ua(classic).php" --level=3
Where the value is evaluated as code rather than run by a shell, --eval names the language. In
PHP :
$ python3 commix.py --url="http://127.0.0.1:8080/scenarios/regular/GET/eval.php?user=ancst" --eval=php
... and in Python :
$ python3 commix.py --url="http://127.0.0.1:8080/scenarios/python/eval.py?user=ancst" --eval=python
Note
A shell separator means nothing at an evaluated sink - the payload has to be valid code in the
language doing the evaluating, which is what --eval settles. The Python page concatenates the
value inside a quoted literal, and commix works that boundary out for itself.
Prove execution where the page gives nothing back, by timing :
$ python3 commix.py --url="http://127.0.0.1:8080/scenarios/regular/POST/blind.php" --data="addr=127.0.0.1" --technique=t
... or out-of-band, where the proof arrives at a server of your own rather than in the response :
$ python3 commix.py --url="http://127.0.0.1:8080/scenarios/regular/POST/blind.php" --data="addr=127.0.0.1" --oob
Note
--oob uses the public oast.fun interactsh server by default, so interaction metadata for your
target leaves your network. Point --oob-server at a self-hosted instance to keep it in-house,
and --oob-transport=dns where the target can resolve a name but not reach the internet over
HTTP.
To get an overview of commix available options, switches and/or basic ideas on how to use commix, check usage, usage examples and filters bypasses wiki pages.
For bug reports or enhancements regarding commix-testbed, please open an issue. A good new scenario demonstrates something the existing pages do not: a different sink, a different place the value arrives from, or a filter that fails for a reason none of the others already show.
- Testbeds guide: https://github.com/commixproject/commix/wiki/Command-injection-testbeds
- Issues tracker: https://github.com/commixproject/commix-testbed/issues
- Project site: https://commixproject.com

