Lesson 12 / 22
Watering Hole Attacks
Modeling the watering hole attack as a shared dependency: eight external resources that six internal applications reach, how many users a single resource affects at once, at which link the six-step chain breaks per resource, the three chains that a resource-integrity hash breaks, and the 48-day-a-year delay cost of version pinning.
Published Updated
In this lesson
- Maps shared external resources to user counts and calculates the audience a single resource covers
- Runs the watering hole chain per resource and derives the earliest-breaking link
- Measures the user-weighted gain of resource-integrity checking and the cost of version pinning
Contents
The previous lesson’s spoof called the target to itself: a name is registered and the target is expected to go to it. The reverse is also possible — corrupting a place the target already goes to. A portion of an organization’s users visit the same external resource every day: a shared script library, a font resource, a document repository, an industry announcement page. These resources are outside the organization’s control, and each one is a single point.
A watering hole attack therefore targets not the target but the target’s dependency. In its narrow definition, a watering hole is the corruption of a page the user goes to directly — a document repository and an announcement page fit this definition; the script and font resources an application embeds and runs overlap with a supply chain attack. This lesson combines the two into a single measure, because the question it asks is the same for both: when an external resource is corrupted, how many users does it affect at once, and at which link does their chain break? The resource type is decisive here, because the type determines which control can be applied. A resource whose version can be pinned can have its hash computed; an external page the user goes to directly has no hash. The step at the end of the chain — foothold after a drive-by download — was measured in another course; here it appears only as the chain’s last link.
- SE31. The applications, user counts, and external resource list belong to the fictional regional
measurement network. No real resource is contacted; what is measured is the model built with
node. - SE32. The applications’ user sets were counted as disjoint. In reality a user uses more than one application; the disjointness makes the total user count look larger than it is and makes overlapping resource sharing look smaller than it is.
- SE33. The controls applicable to a resource were hard-coded in the model. In reality, whether a resource’s version can be pinned depends on the provider and changes over time.
- SE34. The chain’s second step — the resource being corrupted — is outside the organization’s control and was therefore not mapped to any control. The provider’s own defenses were not modeled.
- SE35. The dropped item is the count of sessions, files, and endpoint items that become accessible after that step. Item types were not weighted separately.
- SE36. The manual-approval delay and the annual update count are fictional. Both vary widely by resource type; the fixed value shows the direction of the cost, not its magnitude.
What Sharing Means in Users
// resource.mjs — internal applications, shared external resources, and the watering hole chain; model // [application, daily users] export const APPLICATION = [ ['field work order app', 180], ['subscriber portal', 1400], ['billing generation interface', 60], ['branch endpoint dashboard', 90], ['measurement monitoring dashboard', 240], ['internal announcement page', 620], ]; export const users = (name) => APPLICATION.find((u) => u[0] === name)[1]; const [F, S, B, N, M, I] = APPLICATION.map((u) => u[0]); // control -> which step of the chain it breaks export const CONTROL = { hash: 3, policy: 4, signature: 5, endpoint: 6 }; // [external resource, type, applications that use it, applicable controls, annual updates] export const RESOURCE = [ ['shared script library', 'script', [F, S, B, N, M, I], ['hash', 'policy', 'endpoint'], 12], ['font resource', 'font', [S, M, I], ['hash', 'policy', 'endpoint'], 2], ['chart drawing library', 'script', [B, M], ['hash', 'policy', 'endpoint'], 6], ['map tile service', 'image', [F, M], ['policy', 'endpoint'], 0], ['measurement tool download', 'binary', [F, N], ['signature', 'endpoint'], 4], ['calendar feed', 'data', [S, I], ['policy', 'endpoint'], 0], ['shared document repository', 'external page', [F, B, N, I], ['endpoint'], 0], ['industry announcement page', 'external page', [B, I], ['endpoint'], 0], ]; // [step, precondition, item dropped once the step completes] export const CHAIN = [ ['identifying the shared resource', 'the external resource list being visible from outside', 0], ['corruption of the resource', 'access on the resource provider side', 0], ['delivery', 'the client fetching the resource', 1], ['execution on the client', 'the client running the resource', 2], ['local download', 'user approval', 3], ['foothold', 'permission to execute', 6], ]; export const CUMUL = (() => { let s = 0; return CHAIN.map((a) => (s += a[2])); })(); export const reached = (k) => k[2].reduce((a, u) => a + users(u), 0); export function links(k, hashOn = true) { return k[3].filter((d) => hashOn || d !== 'hash').map((d) => CONTROL[d]).sort((a, b) => a - b); } export const dropped = (h) => (h.length ? CUMUL[h[0] - 2] ?? 0 : CUMUL[CHAIN.length - 1]);
// run1.mjs — user counts of shared external resources and the chain's earliest-breaking link import { APPLICATION, RESOURCE, CHAIN, CUMUL, reached, links, dropped } from './resource.mjs'; const total = APPLICATION.reduce((a, u) => a + u[1], 0); console.log(`${APPLICATION.length} internal applications, ${total} daily users; ${RESOURCE.length} shared external resources`); console.log(`chain has ${CHAIN.length} steps; if it never breaks, dropped items ${CUMUL[CHAIN.length - 1]}\n`); console.log(`${'#'.padStart(2)} ${'step'.padEnd(33)} ${'precondition'.padEnd(53)} ${'dropped'.padStart(7)}`); CHAIN.forEach((a, i) => console.log(`${String(i + 1).padStart(2)} ${a[0].padEnd(33)} ${a[1].padEnd(53)} ${String(CUMUL[i]).padStart(7)}`)); console.log(`\n${'external resource'.padEnd(27)} ${'type'.padEnd(13)} ${'apps'.padStart(8)} ${'users'.padStart(9)} ` + `${'controls'.padStart(8)} ${'earliest link'.padStart(14)} ${'dropped'.padStart(7)}`); const sorted = [...RESOURCE].sort((a, b) => reached(b) - reached(a)); for (const k of sorted) { const h = links(k); console.log(`${k[0].padEnd(27)} ${k[1].padEnd(13)} ${String(k[2].length).padStart(8)} ${String(reached(k)).padStart(9)} ` + `${String(h.length).padStart(8)} ${String(h[0]).padStart(14)} ${String(dropped(h)).padStart(7)}`); } const matches = RESOURCE.reduce((a, k) => a + reached(k), 0); console.log(`\ntotal user-resource matches ${matches}; most shared resource ${sorted[0][0]} ` + `(${sorted[0][2].length}/${APPLICATION.length} applications, ${reached(sorted[0])} users)`); console.log(`the corruption of a single resource affects at most ${reached(sorted[0])} users at once, ` + `which is ${Math.round((reached(sorted[0]) / total) * 100)}% of the total users`);
6 internal applications, 2590 daily users; 8 shared external resources chain has 6 steps; if it never breaks, dropped items 12 # step precondition dropped 1 identifying the shared resource the external resource list being visible from outside 0 2 corruption of the resource access on the resource provider side 0 3 delivery the client fetching the resource 1 4 execution on the client the client running the resource 3 5 local download user approval 6 6 foothold permission to execute 12 external resource type apps users controls earliest link dropped shared script library script 6 2590 3 3 0 font resource font 3 2260 3 3 0 calendar feed data 2 2020 2 4 1 shared document repository external page 4 950 1 6 6 industry announcement page external page 2 680 1 6 6 map tile service image 2 420 2 4 1 chart drawing library script 2 300 3 3 0 measurement tool download binary 2 270 2 5 3 total user-resource matches 9490; most shared resource shared script library (6/6 applications, 2590 users) the corruption of a single resource affects at most 2590 users at once, which is 100% of the total users
The resource table’s first row carries the whole measure. The shared script library is used in all six of the six applications and reaches all 2,590 users — one hundred percent of the model’s total users. The corruption of a single external resource affects every user at once, without touching anything inside the organization. In phishing, a target list is built; here the target list is already built, and it is the organization’s own dependency choice that built it.
User count and application count do not give the same ranking. The calendar feed is used in only two applications but reaches 2,020 users; the shared document repository is used in four applications and reaches 950. The measure of sharing is not how many applications use a resource but how many people those applications serve. A study that ranks the dependency inventory by application count brings the wrong three items to the top.
The last two columns show where the chain breaks. In three resources, the earliest link is the third step, and zero items have dropped there. In two external pages, the earliest link is the sixth step — the chain’s last step — and by the time it is reached, six items have dropped. The difference between them is in the control count: one has three applicable controls, the other has one.
The Gain and Cost of Integrity Checking
// run2.mjs — how many chains resource-integrity checking breaks, the user-weighted gain, and the cost import { APPLICATION, RESOURCE, CUMUL, reached, links, dropped } from './resource.mjs'; const APPROVAL_DAYS = 2; // fictional: manual-approval delay for an update under version pinning console.log(`${'external resource'.padEnd(27)} ${'users'.padStart(9)} ${'no-hash link'.padStart(14)} ${'dropped'.padStart(7)} ` + `${'hash link'.padStart(13)} ${'dropped'.padStart(7)} ${'gain'.padStart(6)}`); let breaking = 0, weighted = 0; for (const k of [...RESOURCE].sort((a, b) => reached(b) - reached(a))) { const y = links(k, false), v = links(k, true), dy = dropped(y), dv = dropped(v); if (dy !== dv) { breaking++; weighted += (dy - dv) * reached(k); } console.log(`${k[0].padEnd(27)} ${String(reached(k)).padStart(9)} ${String(y[0]).padStart(14)} ${String(dy).padStart(7)} ` + `${String(v[0]).padStart(13)} ${String(dv).padStart(7)} ${String(dy - dv).padStart(6)}`); } const hashed = RESOURCE.filter((k) => k[3].includes('hash')); const matches = RESOURCE.reduce((a, k) => a + reached(k), 0); console.log(`\nresource-integrity hashing breaks ${breaking}/${RESOURCE.length} chains earlier; ` + `user-resource matches it covers ${hashed.reduce((a, k) => a + reached(k), 0)}/${matches} (${Math.round(hashed.reduce((a, k) => a + reached(k), 0) / matches * 100)}%)`); console.log(`user-weighted early-break gain ${weighted} item-users`); const latest = RESOURCE.filter((k) => links(k)[0] === 6); console.log(`chains that break latest (link 6, dropped ${CUMUL[4]}): ${latest.map((k) => `${k[0]} (${reached(k)} users)`).join('; ')}`); console.log(`hashing cannot be applied to these two resources; both are external pages outside the organization's control`); const signed = RESOURCE.filter((k) => k[3].includes('signature')); const approvals = [...hashed, ...signed].reduce((a, k) => a + k[4], 0); // annual updates of pinned resources console.log(`\ncost: ${hashed.length + signed.length} resources need version pinning, ${approvals} manual approvals a year, ` + `${APPROVAL_DAYS} days of delay per approval = ${approvals * APPROVAL_DAYS} days/year of resource staleness`); console.log(`out-of-scope ${RESOURCE.length - hashed.length - signed.length} resources, ` + `${RESOURCE.filter((k) => !k[3].includes('hash') && !k[3].includes('signature')).reduce((a, k) => a + reached(k), 0)} user-resource matches`); console.log(`total daily users ${APPLICATION.reduce((a, u) => a + u[1], 0)}; out-of-scope matches are ` + `${(RESOURCE.filter((k) => !k[3].includes('hash') && !k[3].includes('signature')).reduce((a, k) => a + reached(k), 0) / APPLICATION.reduce((a, u) => a + u[1], 0)).toFixed(2)} times that`);
external resource users no-hash link dropped hash link dropped gain shared script library 2590 4 1 3 0 1 font resource 2260 4 1 3 0 1 calendar feed 2020 4 1 4 1 0 shared document repository 950 6 6 6 6 0 industry announcement page 680 6 6 6 6 0 map tile service 420 4 1 4 1 0 chart drawing library 300 4 1 3 0 1 measurement tool download 270 5 3 5 3 0 resource-integrity hashing breaks 3/8 chains earlier; user-resource matches it covers 5150/9490 (54%) user-weighted early-break gain 5150 item-users chains that break latest (link 6, dropped 6): shared document repository (950 users); industry announcement page (680 users) hashing cannot be applied to these two resources; both are external pages outside the organization's control cost: 4 resources need version pinning, 24 manual approvals a year, 2 days of delay per approval = 48 days/year of resource staleness out-of-scope 4 resources, 4070 user-resource matches total daily users 2590; out-of-scope matches are 1.57 times that
The hash columns count what resource-integrity checking does: it breaks three of eight chains one link earlier. The gain column is only one item per resource, but the user count it covers is large — the user-weighted gain is 5,150 item-users, covering fifty-four percent of the total matches. The control’s value is not in the item count but in how many users that one item does not drop for, all at once.
For the remaining five resources, hashing changes nothing. In the two external pages, the reason is clear: the user goes there directly, and what is fetched is not a pinned version but the page as it is at that moment. These two resources are the two whose chains break latest — the sixth link, with six items already dropped — and together they reach 1,630 users. There is no relationship between how shared a resource is and how checkable it is; the most shared resource happened to be the most checkable, while the two latest-breaking resources sit in the middle of the ranking.
The cost collects into two items. The four resources that need version pinning produce twenty-four manual approvals a year; at a two-day delay per approval, that means the resource is kept on a stale version for forty-eight days a year. This delay produces an exposure of its own: an issue the provider has closed stays open inside the organization until the approval completes. The cost of the control here is not a labor item — it is a risk running in the opposite direction.
The four out-of-scope resources carry 4,070 user-resource matches; that is 1.57 times the total daily user count. In other words, the average user visits one and a half external resources with no integrity check at all every day. Breaking the chain through these resources is left to endpoint protection alone, and the dropped item there is six.
The First Two Links That Cannot Be Broken
Neither of the chain table’s first two steps has a control across from it, and the reasons differ. The first step — identifying the shared resource — is invisible from inside the organization, because the external resource list is already written into everything the client fetches; hiding it is not an option. The second step takes place on the resource’s own side and is entirely outside the organization’s control.
Since these two links cannot be broken, only one lever remains: reducing how many resources are depended on. The table gives the measure for this too. If the shared script library is brought in-house, the 2,590-user match drops off the external list, but the item moves into the organization’s inventory and keeping it current becomes the organization’s job. The 4,070 matches of the four out-of-scope resources cannot be moved this way: two of them are external pages the user goes to directly, and they have no version to bring in-house.
This is where the measure shifts. If the first two links cannot be broken, the remaining work is detection: a record that sees which resource changed and when does not break the chain, but it shows when the control at the third link kicks in. The dependency inventory is therefore not a document but a measurement item: how many resources, how many users, how many have a control.
Summary
- 6 internal applications serve 2,590 users a day and share 8 external resources. The most shared resource is used in all 6 applications and reaches 100% of users.
- User count and application count do not give the same ranking: a resource used in 2 applications reaches 2,020 users, while another used in 4 applications reaches 950.
- The chain has 6 steps; if it never breaks, dropped items reach 12. In 3 resources the earliest link is step 3 (0 items); in 2 external pages it is step 6 (6 items).
- Resource-integrity hashing breaks 3 of 8 chains one link earlier. The user-weighted gain is 5,150 item-users, 54% of the total matches.
- The 2 external pages where hashing cannot be applied reach 1,630 users and are the two whose chains break latest. No relationship emerges between the degree of sharing and checkability.
- Cost: version pinning on 4 resources, 24 manual approvals a year, 48 days/year on a non-current version. The 4 out-of-scope resources carry 4,070 matches — 1.57 times the daily user count.
Next Step
Every control measured across six lessons was technical: a procedure, a threshold, a door, a hash. Yet the breakable link in most of the chains depended on a decision one person made — whether to call back, whether to notice and report an indicator, whether to hold a door. That decision itself is a trainable behavior, and training’s effect can be measured. That is the next lesson’s question: how many points does an awareness program gain on two metrics — reporting rate and click rate — how many weeks does that gain take to decay back to its old value, and what is the program’s own cost. It is the topic’s closing lesson.
To keep your progress and take notes, Log in
My notes
Log in to take notes.