All lessons › Function C — Application Security with an AI SDLC › The AI SDLC — an Agentic AppSec Pipeline, Before and After Deploy
C2.8

Dynamic exploitation (DAST)

C2
▶
Lightboard recording

Not recorded yet. The lesson below is complete and runnable today — the video is added later without touching this page.

Video placeholderpublishes automatically from recordings/C2.8.mp4
Risk

A SAST finding is a hypothesis, and hypotheses get argued about instead of fixed.

Control

Stage 12: generate and run an actual exploit against the sandbox, so the finding is confirmed or dropped.

The lab

Turn static findings into executable probes against the replica and separate confirmed from unconfirmed.

◉Use case relevance

An investigation lands — somebody else's, into somebody else's incident. The research function's job is not to summarise it. It is to index it so each finding can be cited alone, type every control, and give each one an owner who will test it.

At CyberTravels. The hypotheses become demonstrations, in your own replica. exploit_sql_injection turns one row into four across three owners; exploit_idor asks for booking 2 as Dana and gets Priya's, with no payload and nothing malformed — which is why stage 7 could not see it. An exploit that does not fire returns undetermined, never refuted.

◴What this lesson is — Day 0, Day 1, Day 2

Turning hypotheses into demonstrations against the replica, and the half that matters as much: a hypothesis that cannot be demonstrated is dropped rather than shipped as a medium somebody has to carry. An exploit that does not fire is undetermined, not refuted.

Day 0 — why

A static finding is a hypothesis, and hypotheses get argued about in triage meetings instead of fixed.

Day 1 — how

Generate and run an actual exploit against the sandbox, so each finding is confirmed or dropped.

Day 2 — measure

Findings confirmed by execution versus refuted. Both are useful answers; only one of them is a ticket.

Measure this

what to measuredenominator or target
findings confirmed by execution, against those refutedtwo counts; only one of them is a ticket

▦The framework, and how it works

   report ------> register ------> owners

   T<row>.<n>   threat        typed P / D / C
   E<row>.<n>   evidence      anchored to a control family
   C<row>.<n>   control       assigned to a lesson that tests it

   10 rows . 34 threats . 40 evidence items . 40 controls

   rows 1, 2 and 5 share one surface:
        [ artifact repository ] = covert channel
                               = supply-chain target
                               = escape vector
   filed apart, three teams each fix a third and the surface remains

Stage 12 — Dynamic exploitation. The stage that converts an argument into a fact.

Everything Phase 3 produced is a hypothesis: the code looks vulnerable and the sink appears reachable. Hypotheses get argued about in triage meetings. An executed exploit does not — either the probe achieved the effect or it did not.

Two things this stage produces that static analysis cannot:

  • Confirmation. A finding that survives an exploit attempt is real, regardless of how the model felt about it.
  • Refutation. A finding that fails is either not exploitable in this configuration or not real, and both are useful answers.

The discipline that makes it trustworthy is that the probe must assert a concrete effect — rows returned that should not be, a file read outside the root — not merely that the request did not error. "No exception" is the DAST equivalent of a shape check, and C2.0 already established what those are worth.

Where you are in the pipeline. `` [Ingestion & Mapping] ──> [Threat Modelling] ──> [Discovery] └─ stages 1-4 └─ stages 5-6 └─ stages 7-10 ──> [Dynamic Validation] ──> [Reporting] └─ stages 11-14 └─ stage 15 ``

2 · The replica application, running

3 · Stage 12 — probes that assert a concrete effect

4 · Where it breaks — the probe that asserts nothing

The most common DAST bug: treating "the request succeeded" as confirmation. Both probes below hit the app and return 200-equivalents; only one of them proves anything.

5 · The stage, as a skill

A dynamic probe proves nothing unless its assertion can fail. The skill runs the probes against a live build with a control probe alongside, then re-runs them under a weak assertion so you can watch it flag the control too.

▶Real time execution as skill

The skill — skills/appsec/dynamic-exploitation-probe/SKILL.md

name: dynamic-exploitation-probe
description: >-
  Run probes against a live build to turn a static finding into a demonstrated
  one, with a control probe and an assertion strong enough to tell them apart.
  Use when confirming exploitability, or when a dynamic test reports everything
  as vulnerable.
allowed-tools: Read, Grep, Glob, Bash

The assertion is the experiment

A dynamic probe proves nothing unless its assertion can fail. A weak assertion — "the response contains rows", "the request did not error" — confirms the control probe as readily as the attack, and a test that confirms everything has measured nothing.

When to use this

After static analysis has produced hypotheses, in a replica confirmed by an isolation check, and before any finding is reported as exploitable.

Procedure

1 — State each hypothesis as a prediction. Not "SQL injection in list_reports" but "requesting one owner returns rows for more than one". A hypothesis you cannot write as a prediction is not ready to test.

2 — Include a control probe. A well-formed, benign request whose result is known. It is the thing that tells you your assertion discriminates.

3 — Write the assertion against the prediction. Count owners returned, not whether rows came back. Read the file's contents, not whether a path resolved. The difference between these is the entire value of the stage.

4 — Run all probes, including the control, and record raw results. Then apply the assertion. Keep both: the raw result is what somebody re-reads when they doubt the finding.

5 — Demonstrate the weak assertion too. Run the same probes under the loose check and show it flagging the control. That comparison is what stops the next person writing one.

6 — Mark each hypothesis confirmed, refuted, or untested — and never "probably". A hypothesis whose probe could not run is untested, which is different from refuted and much more common.

Example

Input — the fixture committed at the top of scripts/dynamic_exploitation_probe.py. Edit it and re-run: the buckets, counts and verdicts below are derived from it, not hard-coded.

Output — the opening lines of a real run:

normal use:
   list_reports('test-user-a') → [(1, 'test-user-a', 100), (2, 'test-user-a', 250)]
   fetch_doc('readme.txt')     → public readme
probe  cwe      exploited  observation
--------------------------------------------------------------------------
P-01   CWE-89   True       4 rows, owners=['test-user-a', 'test-user-b', 'test-user-c']
P-02   CWE-22   True       returned 'synthetic-not-real-9999'
P-03   CWE-89   False      2 rows, owners=['test-user-a']

The run continues past this. test_skills.py executes the script on every build, with no model configured — so what CI proves is that it runs and refuses legibly, not that it prints these lines. The output above is a recorded run against a real model, and nothing re-diffs it: a model's answer is not reproducible, and this repository does not claim otherwise anywhere it can be checked.

Output contract

{
  "hypotheses": [{"id": "str", "prediction": "str"}],
  "probes": [{"id": "str", "kind": "attack|control", "raw": "str"}],
  "assertions": [{"id": "str", "strong": true, "verdict": "confirmed|refuted|untested"}],
  "weak_assertion_run": {"flagged": ["str"], "includes_control": true}
}

Failure modes

  • No control probe. Nothing tells you the assertion discriminates.
  • Asserting on shape. "Rows came back" is true for the legitimate query.
  • Recording untested as refuted. They have different owners and different next steps.

Three ways to do this lesson. Pick one. They run the same procedure against the same code — what changes is who drives it, and how much you see.

1In your AI agent — you are taught

The agent reads the lesson, explains the idea, fetches the code, runs the check and answers your questions as you go. This is the one to take if you are here to learn.

Install it, once. From the folder you cloned. --all does every agent it finds on your machine:

python3 scripts/install_skills.py --all --lessons C2.8

Then restart your agent and open it in that folder. On Windows, if python3 says Python was not found, use python instead.

Then pick c2-8-dynamic-exploitation-dast:

agentinstall just for this agentstart it
Claude Codepython3 scripts/install_skills.py --tool claude --lessons C2.8type /c2-8-dynamic-exploitation-dast
GitHub Copilot (VS Code)python3 scripts/install_skills.py --tool copilot --lessons C2.8type /c2-8-dynamic-exploitation-dast in Chat
Cursorpython3 scripts/install_skills.py --tool cursor --lessons C2.8type / in Agent chat and search for it
Codexpython3 scripts/install_skills.py --tool agents --lessons C2.8type $c2-8-dynamic-exploitation-dast, or run /skills

2As one Python command — you just run it

No agent, no conversation. The same lesson executes and prints the same readback. Take this one if you have no assistant, if you are reviewing rather than learning, or if you want the result without the teaching around it.

python3 scripts/lesson.py C2.8

3Read the code, and the difference — nothing runs

Neither of the above, and no model needed. The first command writes CyberTravels exactly as it stood at the end of this lesson; the second prints the diff — the lines this lesson added or changed, and nothing from any later one.

python3 scripts/checkpoint.py --at C2.8 --out work/cybertravels
python3 scripts/checkpoint.py --at C2.8 --diff

On Windows, type python wherever these say python3 — and use \ instead of / in the paths. Everything else is identical. A0.0 step 2 explains why.

Ways 1 and 2 need a model; way 3 does not. A signed-in Claude Code CLI needs no API key, and any OpenAI-compatible endpoint works too. With neither, the audit exits 2 and says so rather than inventing an answer. A0.0 sets this up.

Ways 1 and 2 both end with the same readback: What I did, What changed, The number and Read this next. A step that was skipped or refused is reported as exactly that, never as a pass.

If you did the last lesson’s challenge, expect this to stop. Most challenges ask you to edit the code, and ways 1 and 2 will not overwrite your edits: you get stopped, not a failure, with two options — --out for a fresh folder that keeps your work, or --force to discard it and start this lesson clean.

✓What you just proved

The SQL injection probe returns rows for three owners when one was requested, and the traversal probe returns the synthetic token from outside the document root; the control probe returns a single owner and is not flagged. The weak assertion confirms all three including the control. Stage 12 marks two findings CONFIRMED and one UNVALIDATED for having no probe.

✎Your turn

Look at your DAST assertions. If any of them checks only for a non-error response, it is confirming findings it has not tested — and the control probe above is how you prove that in five minutes.

Tools used

OWASP ZAPNucleiGLM-4.6Kimi K2Claude Haiku 4.5

Skill source: skills/appsec/dynamic-exploitation-probe

← PreviousC2.7 · Supply chain — SBOM, dependency vulnerabilities, and decompiling the libraries