Lesson 08 / 18
Request Manipulation
Measuring intercepting-proxy request manipulation as a validation step: 18 candidates' 40 planned requests, 6 candidates decided with observation-only requests producing zero alerts, 14 candidates decided under the broadest rule set producing 16 alerts, the number of requests to an out-of-scope target staying at zero across all three rule sets, and two candidates that are actually open being unprovable under any rule set.
Published Updated
In this lesson
- Plans a candidate's validation in terms of request count and risk class
- Turns the scope class into a programmatic check and separates the request type that spills outside scope
- Computes validation's evidence yield together with the records, alerts, and time it produces
Contents
The previous two tools only watched. Both shared the same limit: seeing that a field passed with a particular value does not say what the system will do when a different value comes into that field. The only way to show that a candidate is really open is to change the request and see the response — and this is the first step in the course that touches the target.
An intercepting proxy sits between client and server, halts the request, changes it, and resends it. This lesson’s subject is not the tool’s use but the accounting of the work done with that tool: how many requests it takes to prove a candidate, how many of these requests change something persistent on the target, how many carry a risk of disrupting service, and which ones spill outside scope. No request content, string, or payload is written; requests are counted only by their class.
Two Separate Classifications
A validation request is classified along two independent axes, and if the two are mixed up, scope checking cannot be established.
The first is the risk class. o is an observation-only request: it does not change the
target’s state, and sending the same request again gives the same result. s is a request that
changes state on the target: it writes a record, opens a session, advances a counter; being
reversible is a separate condition. d is a request carrying a risk of disrupting service; it
becomes unavoidable when validating candidates related to rate limiting and resource consumption.
The second is the scope class. An in-scope target is one named in the rules of engagement
and permitted to be modified. A shared target is a place where other tenants also exist in the
same environment: observing is allowed, changing state is not, because it carries the risk of
touching someone else’s data. An out-of-scope target belongs to a third party, and no request
can be sent. Even sending a single observation request to an out-of-scope target voids the
authorization granted; at that point the work being done becomes unauthorized access and carries
legal consequences.
- AT15. The candidate list, plan lengths, scope classes, and the
actualfield are a model.actualis the candidate’s status as it will turn out at the end of validation; it is unknown before validation and is written only so the model can compute the outcome. - AT16. The validation plan is the shortest request sequence needed to show that a candidate is open. Plan lengths were written by hand.
- AT17. A plan produces a result only if all of its steps can be run. A plan that runs
incompletely does not say “not open”; it says
unverifiable. - AT18. Every request produces a record on the target. The alert count depends on the risk
class:
ozero,sone,dthree. Times were written by hand per request. - AT19. Scope checking is programmatic: the permitted request class is the intersection of the rule set and the scope class. If the intersection is empty, the request is not sent.
- AT20. The three rule sets represent three separate strictness levels of the rules of engagement.
- AT21. A false positive is a candidate whose plan runs in full and is seen not to be open; this candidate has no place in the report.
// vek/validate.mjs — this is a model of the STEP COUNT and RISK CLASS of intercepting-proxy // validation. No request content, string, or payload is written; requests are counted only by // class. o = observation-only (no state change), s = state-changing on the target, // d = carrying a risk of service disruption. export const RISK = { o: { record: 1, alert: 0, min: 2 }, s: { record: 1, alert: 1, min: 5 }, d: { record: 1, alert: 3, min: 15 }, }; // Scope class: which request classes can be sent to each target. export const SCOPE = { 'in-scope': ['o', 's', 'd'], 'shared': ['o'], // other tenants exist in the same environment 'out-of-scope': [], // third party; no request can be sent }; // Rules of engagement: which risk class is permitted. export const RULE = { 'observation-only': ['o'], 'reversible': ['o', 's'], 'written-extra-permission': ['o', 's', 'd'], }; // row: candidate | candidate class | target scope | actually open | validation plan export const CANDIDATE = ` c01 information-disclosure in-scope yes o,o c02 authorization-check in-scope yes o,o,s c03 session-management in-scope no o,o,s c04 configuration in-scope yes o c05 input-validation in-scope yes o,s,s c06 rate-limit in-scope yes o,d c07 information-disclosure shared no o,o c08 authorization-check shared yes o,s c09 configuration out-of-scope yes o c10 input-validation in-scope no o,s c11 session-management in-scope yes o,o,o c12 rate-limit in-scope no o,d c13 information-disclosure in-scope no o c14 authorization-check in-scope yes o,o,s,s c15 input-validation shared no o,s c16 configuration in-scope no o,o c17 session-management out-of-scope no o,s c18 rate-limit in-scope yes o,o,d `.trim().split('\n').map((r) => { const [name, cls, target, actual, plan] = r.trim().split(/\s+/); return { name, cls, target, actual: actual === 'yes', plan: plan.split(',') }; }); // A candidate's outcome under a given rule set. export function result(c, rule) { const allowed = RULE[rule].filter((s) => SCOPE[c.target].includes(s)); const ran = c.plan.filter((s) => allowed.includes(s)); const complete = ran.length === c.plan.length; const s = { ran: ran.length, skipped: c.plan.length - ran.length, record: 0, alert: 0, min: 0 }; for (const step of ran) { s.record += RISK[step].record; s.alert += RISK[step].alert; s.min += RISK[step].min; } s.status = complete ? (c.actual ? 'evidence' : 'false-positive') : 'unverifiable'; return s; } export function ruleMeasure(rule) { const t = { evidence: 0, falsePositive: 0, unverifiable: 0, request: 0, skipped: 0, record: 0, alert: 0, min: 0, outOfScopeTouch: 0 }; for (const c of CANDIDATE) { const s = result(c, rule); t[s.status === 'false-positive' ? 'falsePositive' : s.status] += 1; t.request += s.ran; t.skipped += s.skipped; t.record += s.record; t.alert += s.alert; t.min += s.min; if (c.target === 'out-of-scope' && s.ran > 0) t.outOfScopeTouch += 1; } return t; }
Eighteen Candidates, Three Rule Sets
// vek/measure.mjs — eighteen candidates are run through three rule sets. import { CANDIDATE, RULE, RISK, result, ruleMeasure } from './validate.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()); const plan = CANDIDATE.flatMap((c) => c.plan); console.log(`${CANDIDATE.length} candidates, ${plan.length} planned requests: ` + ['o', 's', 'd'].map((s) => `${s}=${plan.filter((x) => x === s).length}`).join(' ')); console.log(`actually open candidates ${CANDIDATE.filter((c) => c.actual).length} (model; ` + `unknown before validation)`); const A = [-6, -24, -13, 6, 8, 16]; console.log('\n1. plan and outcome per candidate (rule: reversible)'); y(A, 'cand', 'class', 'target', 'plan', 'ran', 'status'); for (const c of CANDIDATE) { const s = result(c, 'reversible'); y(A, c.name, c.cls, c.target, c.plan.join(''), s.ran, s.status); } const B = [-26, 9, 16, 14, 9, 9, 8, 8, 7]; console.log('\n2. totals per rule set'); y(B, 'rule', 'evidence', 'false positive', 'unverifiable', 'request', 'skipped', 'record', 'alert', 'min'); for (const k of Object.keys(RULE)) { const t = ruleMeasure(k); y(B, k, t.evidence, t.falsePositive, t.unverifiable, t.request, t.skipped, t.record, t.alert, t.min); } console.log('\n3. scope check'); for (const k of Object.keys(RULE)) console.log(` ${k.padEnd(26)} requests to out-of-scope target: ${ruleMeasure(k).outOfScopeTouch}`); const g = ruleMeasure('observation-only'), e = ruleMeasure('written-extra-permission'); console.log(`\ncandidates decided by observation only ${g.evidence + g.falsePositive} / ${CANDIDATE.length}`); console.log(`candidates decided by the broadest rule set ${e.evidence + e.falsePositive} / ${CANDIDATE.length}`); console.log(`candidates unverifiable under any rule set ${e.unverifiable}`); const openButUnproven = CANDIDATE.filter((c) => c.actual && result(c, 'written-extra-permission').status !== 'evidence'); console.log(` of these, actually open ${openButUnproven.length} ` + `(${openButUnproven.map((c) => c.name).join(', ')})`); console.log(`evidence gained per alert ` + `${((e.evidence - g.evidence) / (e.alert - g.alert)).toFixed(2)}`);
18 candidates, 40 planned requests: o=27 s=10 d=3 actually open candidates 10 (model; unknown before validation) 1. plan and outcome per candidate (rule: reversible) cand class target plan ran status c01 information-disclosure in-scope oo 2 evidence c02 authorization-check in-scope oos 3 evidence c03 session-management in-scope oos 3 false-positive c04 configuration in-scope o 1 evidence c05 input-validation in-scope oss 3 evidence c06 rate-limit in-scope od 1 unverifiable c07 information-disclosure shared oo 2 false-positive c08 authorization-check shared os 1 unverifiable c09 configuration out-of-scope o 0 unverifiable c10 input-validation in-scope os 2 false-positive c11 session-management in-scope ooo 3 evidence c12 rate-limit in-scope od 1 unverifiable c13 information-disclosure in-scope o 1 false-positive c14 authorization-check in-scope ooss 4 evidence c15 input-validation shared os 1 unverifiable c16 configuration in-scope oo 2 false-positive c17 session-management out-of-scope os 0 unverifiable c18 rate-limit in-scope ood 2 unverifiable 2. totals per rule set rule evidence false positive unverifiable request skipped record alert min observation-only 3 3 12 25 15 25 0 50 reversible 6 5 7 32 8 32 7 85 written-extra-permission 8 6 4 35 5 35 16 130 3. scope check observation-only requests to out-of-scope target: 0 reversible requests to out-of-scope target: 0 written-extra-permission requests to out-of-scope target: 0 candidates decided by observation only 6 / 18 candidates decided by the broadest rule set 14 / 18 candidates unverifiable under any rule set 4 of these, actually open 2 (c08, c09) evidence gained per alert 0.31
Validation’s Real Yield
The first two columns of the second table are read together. The observation-only rule yields 3 pieces of evidence and 3 false positives; the broadest rule yields 8 evidence and 6 false positives. From the report’s point of view, these two columns carry equal value. Eliminating six false positives keeps the remediation team from dealing with six lines; each line means a review, a discussion, and often a change request. An eliminated candidate is as much a product of validation as an evidenced finding.
The third column is the most honest part of the report. unverifiable does not mean “this
candidate is not open”; it means “I could not decide this candidate while staying within the rules.”
Losing this distinction is a known flaw of penetration-test reports: an unverifiable candidate is
silently dropped, and the organization thinks an item was checked when it never was. Even under the
broadest rule set, four of the eighteen candidates stay in this column, and looking at the model’s
actual field shows that two of them are really open.
Which two candidates those are is the most important line in this lesson. c08 is in a shared
environment: the state-changing request required to prove it cannot be sent because it might touch
another tenant’s data in the same environment. c09 belongs to a third party, and not even a
single observation request can be sent. Both are really open, and both are reported unproven — not
as a finding, but as risk left out of scope.
The Length of the Plan Is a Finding
The plan column in the first table carries information on its own. Three candidates have a plan
of a single request; one candidate’s plan is four requests long, and two of those are
state-changing. This difference is not random: the number of requests needed to prove a candidate
is directly proportional to the number of controls standing in front of it. A configuration
item that can be shown with a single request has stayed at one request precisely because no
control stands in front of it; an authorization-check item that needs four requests only reveals
itself after a chain of conditions is satisfied.
This is used in the report in two directions. A finding with a short plan is more serious, because that same shortness also holds on the attacker’s side. A finding with a long plan can drop in remediation priority, but the justification for that is not “it is hard” — it is how many conditions have to be satisfied together, and those conditions are written out one by one.
The plan’s length is also a direct multiplier on the cost side. Of the forty planned requests, 27 are in the observation class; of the 35 requests that run under the broadest rule set, 25 are observation, 7 are state-changing, and 3 carry a risk of disruption. Of the 130 total minutes, 50 come from observation requests, and the remaining 80 minutes come from only ten requests. Validation’s duration is set not by the request count but by the request class.
The request and response pairs the proxy records become raw material for reproduction steps at the next stage. What goes into the report is not this raw record: the class, the count, the condition, and the observed outcome are written. Adding a validation record to the report as-is means giving everyone who reads the report the ability to resend the same request, and this needlessly enlarges the risk of the remediation process.
Step Count and the Trace Left Behind
As the rule set widens, three numbers rise together. Requests go from 25 to 35, time from 50 minutes to 130 minutes, alerts from 0 to 16. The record count tracks the request count one-to-one, because every request leaves a record on the target; the alert column is the distinguishing one.
Validation run with observation-only requests produces 25 records and zero alerts. Seen from the defense side, this is an ordinary pile of requests in the log file; it is noticed only later, through analysis based on volume and pattern. Opening up state-changing requests brings seven alerts; opening up service-disruption-risking requests brings sixteen. The real number here is the marginal rate: moving from observation-only to the broadest rule gains five extra pieces of evidence while producing sixteen alerts — 0.31 evidence per alert.
This ratio explains why the rules of engagement are a subject of negotiation. Some of the alerts are a sign that the defense is really working, and the blue team is meant to see them. Some of them put the on-call team on the trail of what they think is a real incident and trigger a response; the test window, the communication channel, and the previously reported source list are written for exactly this reason. Keeping the alert count at zero is also an option, but its cost is clear: twelve of the eighteen candidates stay undecided.
The third table gives the same number across all three rule sets: requests going to an out-of-scope target are zero. This number is not an outcome, it is an invariant; whichever rule set is chosen, it has to stay at zero. Running the scope check programmatically on the plan, rather than leaving it to the tool or the person, secures this: before a request is sent, the target’s class is asked, if the intersection is empty the step is skipped, and the skip is written down.
Summary
- The validation plans of the eighteen candidates contain 40 requests in total: 27 observation-only, 10 state-changing, 3 carrying a risk of service disruption.
- As the rule set widens, decided candidates go from 6 to 14; evidence goes from 3 to 8, and eliminated false positives go from 3 to 6. An eliminated candidate is also a product of validation.
- Validation run with observation-only requests leaves 25 records and zero alerts; under the broadest rule, 35 records and 16 alerts arise. The marginal rate is 0.31 evidence per alert.
- Four candidates cannot be verified under any rule set, and two of them are really open; these are reported not as findings but as risk left out of scope.
- The plan’s length measures the number of controls standing in front of a candidate; an item shown with a single request is more serious precisely because that same shortness also holds on the attacker’s side.
- Scope checking is programmatic, and the number of requests to an out-of-scope target is zero under all three rule sets; this is not an outcome, it is an invariant that must be preserved.
Next Step
All three of these tools worked over the network: they recorded traffic, read fields, changed a request and saw the response. A portion of the items that need examining during an assessment, though, come not from the network but from a file: an unknown executable found on an endpoint, an archive extracted from an email attachment, a helper tool the field team obtained from outside. There is no way to learn what these do by sending requests.
The next lesson takes up sandbox analysis on this axis. The Endpoint and System Hardening course measured the sandbox’s isolation strength; here that measurement is taken as input, and a single question is asked: how many of an item’s behaviors can be observed, and how many of them notice that the environment is a sandbox and hide. Hidden behavior feeds the unverifiable-candidate column this time not from the network’s limit, but from the analysis environment’s own limit.
To keep your progress and take notes, Log in
My notes
Log in to take notes.