Chapter 5 / 13

Carry a session

An authenticated test is a login you do once and a token you carry everywhere after. This playbook captures the login, binds its rotating token to a name, writes that name onto every later request, does the same thing headless in a single command, carries several sessions side by side, and finally fetches a fresh token for every request when the app hands out single-use ones. Budget about ten minutes.

Before you begin. Set up an engagement first, and be able to log into the target through the proxy so its auth response is captured. Only replay sessions against a target you are authorized to test; the examples use api.example.com as a stand-in.

1. Capture a login

You need the response that authenticates you before you can reuse it. Log in to the target through gori the way the Quick Start covers: an Open browser session, or your own client pointed at 127.0.0.1:8070. The flow you are after is the one whose response hands you a session: a Set-Cookie: session=…, or a token in a JSON body such as {"access_token": …}. Find it in History:

gori run history -q 'path:/login status:200'

Note the flow id; the headless step at the end replays exactly this flow.

Checkpoint. The login response is in History and carries the token, whether as a Set-Cookie header or a field in its body.

2. Extract the token into a variable

A rotating token is worthless to a rule that has to spell it out in advance, so gori binds it to a name it fills in at send time. Open the Rewriter tab, extract sub-tab, and add a rule that reads the token out of the login response and binds $BIND.SESSION to it. A descriptor picks where the value lives (a cookie, a response header, a regex over the body, a JSON path, or a byte range) alongside a condition (path:/login AND status:200) and an optional host glob, so the rule only reads the response you mean.

gori run rewriter extract add --name SESSION --kind cookie --selector session \
  --when 'path:/login AND status:200' --host '*.example.com'

For a token in a JSON body instead, use --kind jsonpath --selector '$.access_token' (or --kind regex with a capture group over the body).

Checkpoint. gori run rewriter bindings lists $BIND.SESSION. Extraction runs on proxy traffic and on hand sends (a Repeater send), not on sweeps, so replay the login once and the bindings sub-tab shows the name bound. The value lives in memory only; it is never written to settings.json or the project database.

3. Write it back on every request

Binding the name only captured the value; you still have to put it back on the wire, and how depends on who sends. For traffic through the proxy (your browser or client), a Match & Replace rule does it: on the Rewriter tab add a set header rule on the request side that sets Authorization (or Cookie) to $BIND.SESSION. The $BIND.SESSION is resolved when each request goes out, not when you saved the rule, so every proxied request from here leaves authenticated.

gori run rewriter add --op set_header --target request \
  --find Authorization --value 'Bearer $BIND.SESSION' --host '*.example.com'

Match & Replace rules touch proxied traffic only: a Repeater or Fuzzer send goes out exactly as you wrote it. So for those, write the name into the request itself. An Authorization: Bearer $BIND.SESSION line in the Repeater editor or a fuzz template is resolved at send time the same way, and a session slot (step 5) adds the header for you without editing each request.

Checkpoint. A request through the proxy to a protected endpoint that returned 401 before now returns 200, and so does a Repeater send whose Authorization line reads Bearer $BIND.SESSION. If the rule is skipped instead, the events feed says the name resolved to nothing; recapture the login to rebind it.

4. Do it headless

gori run is one process per invocation, and a binding lives only in the memory of the process that observed the login, so a fresh fuzz or mine has nothing to resolve $BIND.SESSION with, and the token goes out as literal text. A sweep is deliberately not an extraction source either: a response echoing an attack payload back could otherwise rebind your session to a payload-derived value. --bind-from closes the gap. It replays one captured flow, the login, first, so its response fills the binding table for the rest of the run in the same process. The template has to name the token, so fuzz a request file whose Authorization line reads Bearer $BIND.SESSION (a captured flow's own bytes never mention it; step 5's --slot adds the header to those):

gori run fuzz --request req.http --target https://api.example.com --bind-from 17 --wordlist ids.txt
# bind-from: flow #17 replayed → bound $BIND.SESSION

The same flag works on mine, sequence, and discover.

Checkpoint. The run prints a bind-from: flow #… replayed → bound $… line, and its responses come back authenticated instead of a wall of 401s.

5. Carry more than one session

Steps 2-4 carry a session. A real engagement usually needs several at once (an admin, a low-privilege user, an anonymous client), and $BIND.SESSION can only mean one thing at a time. A session slot is that name: an identity with its own header overlay and its own binding table, and the one that is active is what a send goes out as.

Slots are the same rows the Authorize tab's identities card edits, so a set you already configured there is already here. Add one headless:

gori run session add --name admin    --set 'Authorization: Bearer $BIND.SESSION' --rule SESSION
gori run session add --name low-priv --set 'Authorization: Bearer $BIND.SESSION' --rule SESSION
gori run session list

Both slots write the same header off the same $BIND.SESSION, and they mean different tokens: a slot that claims an extract rule (--rule SESSION) takes that rule's observed value into its own table instead of the global one. Which token each ends up holding is decided by which login you replay while that slot is active.

Then name the identity on the send. In the TUI it is Ctrl-P → Session slot (or the session:NAME chip in the top bar), and every later send says who it is going out as. Headless it is --slot:

gori run fuzz 42 --slot low-priv --bind-from 17 --wordlist ids.txt
# slot: sending as low-priv
# bind-from: flow #17 replayed → bound $BIND.SESSION

--slot is applied before --bind-from, so the login replay fills the active slot's table and the sweep resolves $BIND.SESSION out of the same one. Run the identical command with --slot admin and a different login flow, and the two runs are two sessions of the same target.

The overlay is header-only (Content-Length never moves and the body is byte-exact), so a slot is safe on bytes you did not author: a captured replay, a fuzz template with its payload already spliced.

Two limits worth knowing before you lean on it. The active slot is never persisted: reopening the project starts as-captured, because a slot's values are memory-only and restoring the pointer into an empty table would send an overlay whose $BIND.SESSION is literal. And there is no cookie jar; a slot carries the headers you wrote and the values gori observed, and --bind-from is the explicit version of "log in again".

For a run long enough to outlive the token, give the slot refresh steps: the Repeater sessions that log in, in order. gori run session edit admin --refresh 12,14 --refresh-before jwt-exp makes a --slot admin send refresh the slot first whenever its bound JWT is about to expire, and gori run session refresh admin runs the steps by hand to check them. In the TUI, Ctrl-P → Use as refresh for slot… on a Repeater sub-tab appends that sub-tab to a slot's steps. It acts before a send and never retries after a 401. See refresh steps.

Checkpoint. gori run session list shows both slots, and a --slot low-priv run prints slot: sending as low-priv before its first request.

6. Fetch a fresh token for every request

Some values die long before the session does: a form's CSRF token or a nonce the app hands out on each page load and accepts once. Refresh steps renew a value before it expires, but a sweep still spends one value on every candidate, so the first candidate gets 200 and every one after it 403. A request-time macro fetches a new one each time: a saved Repeater session that runs before each candidate, so the extract rule from step 2 rebinds the name just in time.

Save the request that serves the token (the form page, or the API call that returns the nonce) as a Repeater session, and keep an extract rule that binds it:

gori run rewriter extract add --name CSRF --kind regex \
  --selector 'name="csrf" value="([^"]+)"' --when 'path:/profile AND status:200'

Then write $BIND.CSRF where the token goes in the request you sweep: a Repeater session (here 12) whose body reads csrf=$BIND.CSRF&email=§x§. Name the token's session (here 15) as the macro:

gori run fuzz --repeater 12 --macro 15 --wordlist emails.txt

The default cadence runs the macro before every candidate, which means the sweep goes one candidate at a time; --macro-every 10 lets ten share a value when the app allows it. A macro that fails (an error, a 4xx/5xx, or no binding rebound) never sends its candidate with a stale token, and three failures in a row end the run. The sweep has to be seeded from a Repeater session or a draft, because a captured flow is sent exactly as captured and never mentions $BIND.CSRF; a token that travels in a header can ride the active slot's overlay instead. In the TUI the macro is a row on the Fuzzer's ADVANCED card and in the Miner's popup, and over MCP it is macro_steps on fuzz_start / mine_start. See Rotating Tokens with a Macro.

Checkpoint. Rows after the first come back with real answers instead of a wall of 403s, and gori run history -q 'src:macro' lists the macro's steps.

Next Steps

  • Test access control: replay your captured requests as each slot and find what the server forgets to check
  • Authorize: replay one request under every slot at once to find broken access control
  • Decode and transform: read and rewrite the encoded values a session rides on
  • Session bindings: the full reference for extract rules and where a value may live
  • Scripting: the headless sweep contract, exit codes, and --bind-from