Skip to content
stasinopoulos edited this page Oct 15, 2026 · 1 revision

commix can be driven over HTTP instead of a command line. commixapi.py serves a small REST API in which every scan is a task: it is created, given options by the names the command line knows them under, started, and then read while it runs and after it ends.

Each task is run as a process of its own. A run is settled in state that is written once and read everywhere, so two of them inside one process would be one run with two sets of answers; a process apiece is what keeps several scans from becoming one.

Starting the server

python commixapi.py
Option
-s, --server Run as a REST API server (the default).
-c, --client Run as a REST API client, against a server already serving.
-H, --host Address to listen on (Default: 127.0.0.1).
-p, --port Port to listen on (Default: 8775).
--token Token a request has to carry (Default: one made up per run).
--tasks-dir Directory to keep each task's options, output and report in.
[08:11:53] [info] Serving the commix API at 'http://127.0.0.1:8911'.
[08:11:53] [info] Tasks are kept under '/home/user/commix-api/wiki'.
[08:11:53] [info] Send the token 's3cr3t' as the 'X-Api-Token' header.

Warning

The server runs scans on behalf of whoever can reach it. It listens on 127.0.0.1 for that reason - a --host that answers the network hands command execution to it.

Authentication

Every request carries the token as the X-Api-Token header. /version is the one address that does not ask for it, so a client can find out what it is talking to before it authenticates.

curl -s -H "X-Api-Token: s3cr3t" http://127.0.0.1:8775/task/new

What an answer looks like

Every answer is JSON and says whether what was asked for happened.

{
  "success": true,
  "version": "v4.2.devXYZ",
  "api": "1.0"
}

Something the server understood but would not do is answered with 200 and "success": false, carrying the reason - a wrong token (401), an address that is not served (404) and one asked for with the wrong method (405) are the three that answer with a code of their own.

{
  "success": false,
  "message": "Unknown option 'nosuchthing'."
}

Note

api is the version of this contract, not of commix. It is counted on its own, so a client can tell whether it still knows how to talk to a newer commix.

Addresses

Method Address
GET /version commix's version, and this contract's.
GET /task/new Make a task, and hand back its id.
GET /task/<id>/delete Forget a task, ending its scan if one is running.
GET /option/<id>/list Every option of a task, with what it is set to.
POST /option/<id>/get One option, named in {"option": "url"}.
POST /option/<id>/set Set options, named as the command line names them.
POST /scan/<id>/start Start the scan - any options in the body are set first.
GET /scan/<id>/stop Ask the scan to stop, so that it writes down what it has.
GET /scan/<id>/kill End the scan's process outright.
GET /scan/<id>/status Where the run has got to.
GET /scan/<id>/data The run's results, as they stand.
GET /scan/<id>/log What the run has said, as {time, level, message}.
GET /download/<id> Every file the task has to hand.
GET /download/<id>/<path> One of them, as itself rather than as JSON.
GET /admin/list Every task this server holds.
GET /admin/flush Forget all of them.

A scan, from one end to the other

A task is made first, and answers with the id every other address is asked under.

curl -s -H "X-Api-Token: s3cr3t" http://127.0.0.1:8775/task/new
{
  "success": true,
  "taskid": "93c54d9323c4dcba"
}

Options are named the way the command line names them, with - read as _: url is --url, flush_session is --flush-session, time_limit is --time-limit. A handful are kept under a shorter name than they are spelled with - --user-agent as agent, --technique as tech - and either name is taken, so nothing has to be known about which of the two this one is. They are checked against commix's own parser, so a name it does not know is refused, and so is a value it would not have taken.

curl -s -H "X-Api-Token: s3cr3t" -H "Content-Type: application/json" \
     -d '{"url":"http://localhost:8081/scenarios/regular/GET/classic.php?addr=127.0.0.1","flush_session":true}' \
     http://127.0.0.1:8775/option/93c54d9323c4dcba/set

Starting the scan hands back the process it is running as. Options may be given here instead, which makes a scan two requests rather than three.

{
  "success": true,
  "engineid": 45671
}

While it runs, status says so, and log hands back what it has said so far. Both answer the same way after it ends, so a client polls one address throughout rather than two.

{
  "success": true,
  "status": "running",
  "returncode": null
}
curl -s -H "X-Api-Token: s3cr3t" "http://127.0.0.1:8775/scan/93c54d9323c4dcba/log?start=0&end=2"
{
  "success": true,
  "log": [
    {
      "time": "08:12:01",
      "level": "info",
      "message": "Testing connection to the target URL."
    },
    {
      "time": "08:12:05",
      "level": "info",
      "message": "Checking whether some kind of WAF/IPS is protecting the target."
    }
  ]
}

data is readable throughout as well - the report is written as the run goes, so findings appear as they are confirmed rather than all at once at the end.

One of the four findings of that run is shown below; each also carries a reproduce field holding the single request that brings it back.

{
  "success": true,
  "data": {
    "target": "http://localhost:8081/scenarios/regular/GET/classic.php?addr=127.0.0.1",
    "http_method": "GET",
    "command": "/home/user/commix/commix.py -c /home/user/commix-api/122c078d2162c7f4/options.ini",
    "started": "2026-10-08 08:12:08",
    "status": "completed",
    "findings": [
      {
        "parameter": "addr",
        "http_method": "GET",
        "technique": "Results-based",
        "type": "Classic command injection",
        "boundary": "';' separator, ' ' whitespace",
        "payload": "QUE;echo USNTCB$((81+65))$(echo USNTCB)USNTCB;"
      }
    ],
    "finished": "2026-10-08 08:12:27",
    "requests": 65,
    "target_os": "Unix",
    "waf_detected": false,
    "evasion_applied": ""
  },
  "error": []
}

A run over several targets - a bulk file, a proxy log, a sitemap or a crawl - carries one such object per target under targets, with started, status, finished and requests standing for the run as a whole. A run over one target keeps the flat shape above.

It is the same report --report-json writes, so what a scan run from a command line hands back and what the API hands back cannot drift apart. error carries what went wrong, which is what tells a run that could not reach its target from one that reached it and found nothing - both end with no findings.

How a run ended

status is what the run itself wrote, except where it was given no chance to write anything.

not started The task exists, but nothing has been started under it.
running The process is still going.
completed It reached the end of what it was asked to do.
time-limit --time-limit ended it, and it wrote down what it had.
terminated /scan/<id>/stop ended it, and it wrote down what it had.
failed It ended on an error - data carries which.
killed /scan/<id>/kill ended the process outright, so it wrote nothing further.

Stopping is worth preferring to killing: a stopped scan is asked to end and finishes its report first, while a killed one is gone before it can.

{
  "success": true,
  "status": "terminated",
  "returncode": 1
}

Asking by hand

-c talks to a server already serving, from a terminal. It is the same addresses asked for the same way, which is what makes a server worth having a look at before anything is written against it.

python commixapi.py -c --token=s3cr3t
[08:43:01] [info] Talking to commix v4.2.dev219 at 'http://127.0.0.1:8907'.
Available Commands
==================

    Name                   Description
    ----                   -----------
    new                    Make a task and use it from here on
    use <taskid>           Work on a task that already exists
    set <option> <value>   Set an option on the task in use
    options                Show every option of the task in use that is set
    start                  Start the task's scan
    status                 Say where the scan has got to
    data                   Show what the scan has found
    log                    Show what the scan has said
    stop                   Ask the scan to stop, keeping what it found
    kill                   End the scan's process outright
    list                   Show every task the server holds
    flush                  Forget all of them
    exit                   Leave

A task made with new is the one every command after it works on, until use takes another. Values are read as what they look like, so set flush_session true sets a switch and set time_limit 90 sets a number.

Note

The contract itself is written out in commixapi.yaml, an OpenAPI description of every address on this page - enough for a client to be generated from rather than written.

Taking what a run produced

/download/<id> says what the task has, and /download/<id>/<path> hands one of them back.

curl -s -H "X-Api-Token: s3cr3t" http://127.0.0.1:8775/download/93c54d9323c4dcba
{
  "success": true,
  "files": [
    { "path": "console.log", "size": 5016 },
    { "path": "options.ini", "size": 2986 },
    { "path": "output/localhost_8081/logs.txt", "size": 2127 },
    { "path": "output/localhost_8081/session.db", "size": 16384 },
    { "path": "report.json", "size": 2631 }
  ]
}
curl -s -OJ -H "X-Api-Token: s3cr3t" \
     "http://127.0.0.1:8775/download/93c54d9323c4dcba/output/localhost_8081/logs.txt"

This is the one address that answers with something other than JSON. The file arrives as itself, under its own name, so anything a run writes - a session file to resume from, the traffic log -t keeps, a file read off the target - can be taken off the server that ran it.

Note

A path that resolves outside the task's own directory is refused however it is spelled, encoded or not. A task hands back what it produced, and nothing else the host happens to hold.

Where a task keeps things

Every task has a directory of its own under --tasks-dir, holding the options it was started with, the report it is writing, what it has printed, and whatever the run itself produced.

<tasks-dir>/<taskid>/
  options.ini     the options the run was started with
  report.json     the report, written as the run goes
  console.log     what the run has printed
  output/         the run's own output directory

They outlive /task/<id>/delete, which forgets the task rather than erasing what it produced.

Note

  • An option that acts on the host serving the API rather than on the target is refused: --alert runs a command here, -c reads a file here, --preprocess and --postprocess run a script here, and --install, --update and --purge change this installation. A token for scanning somebody else's machine does not carry the right to run things on this one.
  • --report-json, --output-dir and --batch are settled by the server for every task, and setting them is refused rather than quietly overridden, so that what a task says it was given is what it was run with.
  • A scan is given its options once, when it starts: setting one while it runs is refused rather than kept, because the running scan cannot be reached by it.
  • Tasks outlive the server. One restarted over the same --tasks-dir reads them back, so a task id still answers for what its run found and said.
  • A scan is always run with --batch: there is nobody at the other end to answer a question.
  • report_json and output_dir are settled by the server, and setting them has no effect.

Contents

User's manual

Exploitation

Miscellaneous

Clone this wiki locally