Skip to content
academia.sh

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 actual field are a model. actual is 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: o zero, s one, d three. 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.

Start typing to search.

↑↓ ↵ Esc navigate · open · close