Lesson 11 / 18
Exploitation Frameworks
Measuring an exploitation framework strictly by its validation limit: of 16 candidates 9 are actually open while only 3 turn into evidence, only one of four validation options has a negative result that shows the vulnerability's absence, two of the three candidates that come back negative are actually open, three candidates get wrongly closed if negative and weak results are counted as closure grounds, and two targets are never touched at all because of scope.
Published Updated
In this lesson
- Classifies validation options by risk level, evidence strength, and the meaning of a negative result
- Separates what a framework proves and does not prove by counting candidate items
- Counts how scope and hosting class narrow the validation option
Contents
Up to this point five tool classes have hit the same limit: every time, the last step that would show an item is really open was left undone. The exploitation framework is the tool class that takes that last step, and for exactly that reason it is used within the narrowest limits.
This lesson does not describe how a framework is used; no tool, module, step sequence, command, or payload is written. Three things are measured: what a validation attempt proves, what it does not prove, and under what condition it damages the target. Pointing a framework at a target without written authorization and a defined scope is unauthorized access regardless of the outcome, and it carries legal consequences.
Four Options, Two Separate Questions
There is more than one way to validate a candidate, and these are separated by two independent properties.
The first is evidence strength. Passive indicator matching — comparing configuration and version information against a known definition — does not touch the target and never produces conclusive evidence; it only strengthens a candidate. Side-effect-free validation deterministically tests whether the condition needed for the vulnerability to exist is met. State-changing and disruption-risk options produce conclusive evidence and in return leave a trace and risk on the target.
The second, and the less-discussed one, is the meaning of a negative result. When a condition test comes back negative, it is known that the condition is not met; this is proof of the vulnerability’s absence. When an attempt comes back negative, nothing is known. The attempt may have failed because of the target’s state at that moment, its load, a middle layer’s behavior, or timing. A failed validation attempt does not prove the vulnerability does not exist. Losing this distinction is the report’s most expensive mistake.
- AT36. The option catalog, candidate list, and hosting classes are a model; there is no real target, no real vulnerability identity, and no real tool.
- AT37. The
openfield is the candidate’s actual state; it is not known before validation and is written only so the model can compute the result. - AT38. In
conditionaloptions the result depends on the target’s environment condition at that moment; if the condition is not met, the attempt comes back negative even on an open item. - AT39. The hosting class narrows the option set.
managedis a service operated by a third party, and no attempt is in scope there. - AT40. The rules of engagement set the highest risk level; the selection is the option with the highest evidence strength among those allowed, and the lowest risk in case of a tie.
- AT41. Duration, record, and alert counts were written by hand per option.
// cerceve/limit.mjs — this is a model of an exploitation framework's VALIDATION LIMIT. No tool, // module, step sequence, command, or payload is written; options are referred to only by their // type, risk level, evidence strength, and THE MEANING OF A NEGATIVE RESULT. // negative: 'absence' = a negative result shows the vulnerability's absence; 'inconclusive' = it shows nothing. // conditional = the result depends on the target's environment condition at that moment; if the // condition is not met, the attempt comes back negative even on an open item. A conditional // attempt's negative result shows NOTHING. export const OPTION = { 'indicator-match': { risk: 0, evidence: 'weak', negative: 'inconclusive', conditional: false, min: 5, record: 1, alert: 0 }, 'side-effect-free-validation': { risk: 1, evidence: 'conclusive', negative: 'absence', conditional: false, min: 15, record: 2, alert: 1 }, 'state-changing': { risk: 2, evidence: 'conclusive', negative: 'inconclusive', conditional: true, min: 30, record: 4, alert: 3 }, 'disruption-risk': { risk: 3, evidence: 'conclusive', negative: 'inconclusive', conditional: true, min: 45, record: 6, alert: 5 }, }; // Hosting class: up to which risk level an operation can be performed on the target. // managed = a service operated by a third party; no attempt is in scope there. export const HOSTING = { own: 3, shared: 1, managed: -1 }; export const ALLOWED_RISK = 2; // the highest risk level the rules of engagement allow // row: candidate | is it open | passive indicator | is the environment condition met | hosting | options export const CANDIDATE = ` c01 open yes no own indicator-match,side-effect-free-validation c02 open yes yes own indicator-match,state-changing c03 open no yes own indicator-match,side-effect-free-validation c04 open yes no own indicator-match,state-changing c05 open yes yes shared indicator-match,state-changing c06 open no no own indicator-match,state-changing c07 open yes yes managed indicator-match,side-effect-free-validation c08 open yes yes own indicator-match c09 closed yes yes own indicator-match,side-effect-free-validation c10 closed yes yes own indicator-match,state-changing c11 closed no yes own indicator-match,side-effect-free-validation c12 closed yes yes own indicator-match c13 closed yes yes shared indicator-match,side-effect-free-validation c14 closed no yes own indicator-match,disruption-risk c15 closed yes yes managed indicator-match,state-changing c16 open no yes own indicator-match,disruption-risk `.trim().split('\n').map((r) => { const [name, state, indicator, cond, hosting, opts] = r.trim().split(/\s+/); return { name, open: state === 'open', indicator: indicator === 'yes', condition: cond === 'yes', hosting, options: opts.split(',') }; }); export const allowed = (a, s) => OPTION[s].risk <= Math.min(ALLOWED_RISK, HOSTING[a.hosting]); // Selected option: among those allowed, the one with the highest evidence strength, and the lowest risk in case of a tie. export function selection(a) { const fit = a.options.filter((s) => allowed(a, s)); if (fit.length === 0) return null; return fit.sort((x, y) => { const g = (s) => (OPTION[s].evidence === 'conclusive' ? 1 : 0); return g(y) - g(x) || OPTION[x].risk - OPTION[y].risk; })[0]; } // Result: evidence | absence-proof | inconclusive-negative | strong-candidate | weak-candidate | unverifiable export function result(a) { const s = selection(a); if (!s) return { option: '-', result: 'unverifiable', min: 0, record: 0, alert: 0 }; const o = OPTION[s]; let d; if (o.evidence === 'conclusive') { if (a.open && (a.condition || !o.conditional)) d = 'evidence'; else if (a.open) d = 'inconclusive-negative'; // open, but the attempt came back negative else d = o.negative === 'absence' ? 'absence-proof' : 'inconclusive-negative'; } else d = a.indicator ? 'strong-candidate' : 'weak-candidate'; return { option: s, result: d, min: o.min, record: o.record, alert: o.alert }; } export function total() { const t = { evidence: 0, 'absence-proof': 0, 'inconclusive-negative': 0, 'strong-candidate': 0, 'weak-candidate': 0, unverifiable: 0, min: 0, record: 0, alert: 0, unprovenOpen: 0 }; for (const a of CANDIDATE) { const r = result(a); t[r.result] += 1; t.min += r.min; t.record += r.record; t.alert += r.alert; if (r.result !== 'evidence' && a.open) t.unprovenOpen += 1; } return t; }
Sixteen Candidates, Six Result Types
// cerceve/measure.mjs — sixteen candidates, what the framework proves and does not prove. import { OPTION, HOSTING, ALLOWED_RISK, CANDIDATE, selection, result, total } from './limit.mjs'; const y = (g, ...s) => console.log(s.map((v, i) => (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join('').trimEnd()); console.log(`${CANDIDATE.length} candidates (${CANDIDATE.filter((a) => a.open).length} actually open), ` + `${Object.keys(OPTION).length} validation options, highest allowed risk ${ALLOWED_RISK}`); const A = [-28, 6, 6, 7, 7, 13, 14]; console.log('\n1. option types'); y(A, 'option', 'risk', 'min', 'record', 'alert', 'conditional', 'negative'); for (const [s, o] of Object.entries(OPTION)) y(A, s, o.risk, o.min, o.record, o.alert, o.conditional ? 'yes' : 'no', o.negative); const B = [-10, 8, 11, 11, 10, 30, 23]; console.log('\n2. option selected and result per candidate'); y(B, 'candidate', 'actual', 'indicator', 'condition', 'hosting', 'selected', 'result'); for (const a of CANDIDATE) { const r = result(a); y(B, a.name, a.open ? 'open' : 'closed', a.indicator ? 'yes' : 'no', a.condition ? 'yes' : 'no', a.hosting, r.option, r.result); } const t = total(); console.log('\n3. decision distribution produced by the framework'); for (const k of ['evidence', 'absence-proof', 'inconclusive-negative', 'strong-candidate', 'weak-candidate', 'unverifiable']) console.log(` ${k.padEnd(24)} ${String(t[k]).padStart(3)}`); console.log('\n4. what it does not prove'); const inconclusiveNeg = CANDIDATE.filter((a) => result(a).result === 'inconclusive-negative'); const wronglyClosed = CANDIDATE.filter((a) => ['inconclusive-negative', 'weak-candidate'].includes(result(a).result) && a.open); console.log(` options whose negative result counts as absence-proof ` + `${Object.values(OPTION).filter((o) => o.negative === 'absence').length} / ${Object.keys(OPTION).length}`); console.log(` candidates that came back negative but show nothing ${inconclusiveNeg.length}`); console.log(` of these, actually open ` + `${inconclusiveNeg.filter((a) => a.open).length}`); console.log(` candidates wrongly closed if negative and weak results are read as 'not open' ` + `${wronglyClosed.length}`); console.log(` candidates that are actually open but cannot be proven ${t.unprovenOpen} / ` + `${CANDIDATE.filter((a) => a.open).length}`); console.log('\n5. cost and trace'); console.log(` total validation time ${t.min} min`); console.log(` records produced on target ${t.record}`); console.log(` alerts produced on target ${t.alert}`); console.log(` targets never touched because they are out of scope ` + `${CANDIDATE.filter((a) => a.hosting === 'managed').length}`); console.log(` targets whose risk limit was lowered in a shared environment ` + `${CANDIDATE.filter((a) => a.hosting === 'shared').length}`);
16 candidates (9 actually open), 4 validation options, highest allowed risk 2 1. option types option risk min record alert conditional negative indicator-match 0 5 1 0 no inconclusive side-effect-free-validation 1 15 2 1 no absence state-changing 2 30 4 3 yes inconclusive disruption-risk 3 45 6 5 yes inconclusive 2. option selected and result per candidate candidate actual indicator condition hosting selected result c01 open yes no own side-effect-free-validation evidence c02 open yes yes own state-changing evidence c03 open no yes own side-effect-free-validation evidence c04 open yes no own state-changing inconclusive-negative c05 open yes yes shared indicator-match strong-candidate c06 open no no own state-changing inconclusive-negative c07 open yes yes managed - unverifiable c08 open yes yes own indicator-match strong-candidate c09 closed yes yes own side-effect-free-validation absence-proof c10 closed yes yes own state-changing inconclusive-negative c11 closed no yes own side-effect-free-validation absence-proof c12 closed yes yes own indicator-match strong-candidate c13 closed yes yes shared side-effect-free-validation absence-proof c14 closed no yes own indicator-match weak-candidate c15 closed yes yes managed - unverifiable c16 open no yes own indicator-match weak-candidate 3. decision distribution produced by the framework evidence 3 absence-proof 3 inconclusive-negative 3 strong-candidate 3 weak-candidate 2 unverifiable 2 4. what it does not prove options whose negative result counts as absence-proof 1 / 4 candidates that came back negative but show nothing 3 of these, actually open 2 candidates wrongly closed if negative and weak results are read as 'not open' 3 candidates that are actually open but cannot be proven 6 / 9 5. cost and trace total validation time 220 min records produced on target 31 alerts produced on target 17 targets never touched because they are out of scope 2 targets whose risk limit was lowered in a shared environment 2
What It Proves
The third block counts six result types, and the first row is smaller than expected: only three of sixteen candidates turn into evidence. Nine candidates are actually open; the framework proves a third of them.
Of the ones that turn into evidence, two were determined by side-effect-free validation, one by a state-changing attempt. This distribution points a direction: the cheapest way to produce conclusive evidence is not an attempt, it is a condition test. For fifteen minutes, two records, and one alert it produces both evidence and — if the candidate is not open — proof of absence. A state-changing attempt gives the same certainty for thirty minutes, four records, and three alerts, and when it comes back negative it says nothing.
The second row counts something rarely discussed in the course: three proofs of absence. These three candidates drop off the list for certain; they do not go to the remediation team and are not looked at again. They do the same job as the false positives eliminated in earlier lessons; the difference is that the grounds are deterministic.
What It Does not Prove
The fourth block is this lesson’s actual subject. Of four validation options, only one can count its negative result as proof of absence. In the remaining three, a negative result carries no information.
Three candidates came back negative, and two are actually open. For c04 and c06, the
environment condition needed was not met during the attempt; the attempt came back negative, the
vulnerability is still sitting there. If these go into the report as “vulnerability not found,” two
real vulnerabilities get closed. The correct line reads “validation was attempted, came back
negative, result inconclusive,” and it keeps candidate status in the remediation priority list.
The second form of the same mistake is in weak results. c16 is actually open, its passive
indicator does not match, and the conclusive validation option cannot be used because it carries
disruption risk. The result is “weak candidate.” If negative and weak results are all read as “not
open,” three candidates get wrongly closed.
In total, six of nine open candidates cannot be proven. The reasons are different and are written separately in the report: two had an unmet attempt condition, two had the risk limit close off the conclusive option, one had no conclusive option to begin with, one had an out-of-scope target. None of these is “no vulnerability.”
Damage Risk and Scope
The fifth block gives the cost: 220 minutes, 31 records, 17 alerts. The alert count is higher than in any earlier lesson, and the reason is obvious — every option that produces conclusive evidence touches the target.
Two targets were never touched at all. Two candidates running on a service operated by a third party were tested by no option because they are out of scope; one of them is actually open, and it is reported not as a finding but as risk left outside the assessment. On the two targets in the shared environment, the risk limit was lowered: every option that could affect other tenants in the same environment was closed off; one had only indicator matching left available, the other still allowed side-effect-free validation.
There is one more risk item not shown in the table, and it explains why the scope check is run on a schedule before every attempt. An attempt’s target comes by hand or from a list; the list may be stale, a record may have moved, or an address may have been reassigned. An attempt pointed at the wrong target is unauthorized access regardless of intent, and it invalidates the authorization given. Verifying a target’s identity right before the attempt is therefore not a habit but an audit step.
The disruption-risk option was not used on any candidate because the rules of engagement did not allow it. This is the normal decision in an environment like a measurement network: taking a meter collector offline stops field teams’ work and shifts the billing cycle. When the cost of evidence is greater than the value of what it proves, the attempt is not made — the candidate stays a candidate, and the reason is written down.
Summary
- Nine of sixteen candidates are actually open; the framework proves only three of them. The cheapest way to conclusive evidence is not an attempt, it is a deterministic condition test.
- Only one of four validation options has a negative result that shows the vulnerability’s absence; in the remaining three, a negative result carries no information.
- Two of the three candidates that come back negative are actually open; if negative and weak results are read as “not open,” three candidates get wrongly closed.
- The grounds for the six unprovable open candidates are written separately: condition not met, risk limit closed it off, no conclusive option, target out of scope. None of these is “no vulnerability.”
- Three proofs of absence drop off the list for certain and do the same job as eliminated false positives.
- Validation produces 220 minutes, 31 records, and 17 alerts; two targets are never touched because of scope, two have their risk limit lowered, and the disruption-risk option is used on no candidate.
Next Step
Everything measured in this lesson — how many requests, which risk class, which option, how many alerts — must have been learned somewhere. A step tried for the first time during validation puts both the target and the assessment itself at risk. A real institutional network is not the place to practice.
The next lesson takes up legal practice labs in terms of scope, and closes out the topic. What is measured is not what these environments teach, it is what they do not teach: the skill list produced by seven lessons is drawn up, each environment’s coverage is counted, and the items no environment covers are written down separately. How many items the gap between a real assessment and a lab comes down to is this topic’s final number.
To keep your progress and take notes, Log in
My notes
Log in to take notes.