---
title: 'Password Attacks'
source: 'https://academia.sh/en/courses/attack-techniques/password-attacks'
course: 'Attack Techniques and the Threat Landscape'
language: en
updated: '2026-09-26T03:02:49+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Separates three password-attack forms by account count and attempts per account'
    - 'Measures which forms account lockout stops and which one it never sees at all'
    - "Compares each threshold's count of forms seen against its legitimate-work cost in the same run"
---

# Password Attacks

Separating three password-attack forms along the cost-and-threshold axis: across four hundred fictional accounts, brute force drops one hundred ninety, dictionary one hundred eighty, and spraying eighteen; account lockout stops two of the three forms and cuts the number of dropped accounts by eighty-seven percent; spraying, at three attempts per account, never triggers the lockout threshold at all; and the total legitimate-work cost of the two thresholds that see all three forms is four.

In the previous lesson's chain, a field had no boundary, and the defense was about putting
that boundary in place. In this lesson's class, the boundary already exists: an identity attempt
is either accepted or rejected, the number of attempts is counted, and it is tied to a threshold.
The attacker is not trying to cross the boundary, but to **stay under it**.

Three forms are separated. **Brute force** tries a large number of candidates against a single
account. **Dictionary** does the same thing with a shorter, more likely candidate sequence.
**Spraying** reverses this: a large number of accounts, only a few candidates against each. All
three use the same chain, but their behavior against the threshold differs, and this lesson
counts that difference. No password, candidate list, or attempt script gets written; each
account's password is represented only by the number giving **its rank in an ordered candidate
sequence**.

## The Chain's Six Steps

1. **Valid username set.** Precondition: the account names are known or can be learned.
2. **Attempt channel open.** Precondition: the endpoint accepts identity attempts.
3. **Attempt budget stays under the threshold.** Precondition: no threshold counting at the
   scale used sees this rate.
4. **Correct match is found.** Precondition: the password is within the candidate range the
   budget covers.
5. **Second factor is not requested.** Precondition: no second verification factor is defined on
   the account.
6. **Session is established and used.** Precondition: there is no check on session behavior.

The third step is the one this lesson measures, and it does not depend on a single threshold but
on the threshold's **scale**. The same threshold value stops one form when counted per account,
and a different one when counted per source.

The fifth and sixth steps are kept outside this measurement, but they are part of the chain. If a
second verification factor is defined, the chain breaks at the fifth step even when the fourth
step completes: the correct password has been found, and it is useless. A check on session
behavior breaks the sixth step; there, the chain has completed and the measure has shifted to
detection. These two steps are the only defense left in cases where the third step can never be
broken — and every number in this lesson is calculated as if they did not exist, because what is
being measured is the threshold itself.

**TA26:** the account set is fictional; `rank` is the password's position in the ordered
candidate sequence. **TA27:** the three thresholds count at three separate scales. **TA28:** the
three forms are separated by only two numbers — how many accounts it touches and how many
attempts it makes per account. **TA29:** duration is calculated at the maximum rate the rate
limit allows.

```js
// pw/model.mjs — password-attack cost model. No password, dictionary, or attempt script at
// all; each account's password is represented only by the number giving "its rank in an
// ordered candidate sequence."
export const ACCOUNT_COUNT = 400;

// Self-written generator; seed is visible, gives the same result run to run.
function generator(seed) {
  let s = seed >>> 0;
  return () => (s = (s * 1664525 + 1013904223) >>> 0) / 4294967296;
}

export const BUCKET = [[24, 1, 5], [60, 6, 500], [96, 501, 50000], [40, 50001, 5000000]];

export function accounts(seed = 20260802) {
  const r = generator(seed);
  const h = [];
  for (const [count, lo, hi] of BUCKET)
    for (let i = 0; i < count; i++) h.push({ rank: lo + Math.floor(r() * (hi - lo + 1)) });
  while (h.length < ACCOUNT_COUNT) h.push({ rank: Infinity });
  return h;
}

// legit = fictional daily false-positive rate (blocking a legitimate user).
export const THRESHOLD = [
  { name: "account-lockout", scale: "account", limit: 5, legit: 30 },
  { name: "source-rate-limit", scale: "source", limit: 60, legit: 4 },
  { name: "global-rate-threshold", scale: "system", limit: 200, legit: 0 },
];

export const FORM = [
  { name: "brute-force", accountCount: 1, budget: 1000000 },
  { name: "dictionary", accountCount: 1, budget: 50000 },
  { name: "spraying", accountCount: ACCOUNT_COUNT, budget: 3 },
];

export const duration = (attempt) => {
  const min = attempt / THRESHOLD[1].limit;
  return min < 1 ? `${(min * 60).toFixed(0)} sec` : min < 60 ? `${min.toFixed(0)} min`
    : min < 1440 ? `${(min / 60).toFixed(1)} hr`
    : `${(min / 1440).toFixed(1)} day`;
};

export const write = (g, ...s) => console.log(s.map((v, i) =>
  (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join(""));
```

```js
// pw/measure.mjs — three forms are run under three thresholds: how many attempts per account,
// at which attempt each threshold kicks in, how many accounts drop, and in how much time.
// Randomness comes from the generator, the seed is visible; the same result comes out run to run.
import { ACCOUNT_COUNT, accounts, THRESHOLD, FORM, duration, write } from "./model.mjs";

const H = accounts();
const bucket = (h) => h.rank <= 5 ? "first 5" : h.rank <= 500 ? "first 500"
  : h.rank <= 50000 ? "first 50,000" : h.rank <= 5000000 ? "first 5,000,000" : "out of budget";
const BUCKETS = ["first 5", "first 500", "first 50,000", "first 5,000,000", "out of budget"];

console.log(`${ACCOUNT_COUNT} fictional accounts, ${FORM.length} forms, ${THRESHOLD.length} thresholds\n`);

const A = [-18, 11, 9];
console.log("1. distribution of accounts across the candidate sequence");
write(A, "bucket", "accounts", "share");
for (const k of BUCKETS) {
  const n = H.filter((h) => bucket(h) === k).length;
  write(A, k, n, `${((n / ACCOUNT_COUNT) * 100).toFixed(1)}%`);
}

// Does a threshold see a form: account scale counts per account, source and system scale
// count over the total attempts.
const sees = (b, e) => (e.scale === "account" ? b.budget : b.accountCount * b.budget) > e.limit;

const B = [-13, 10, 15, 16, 16, 27];
console.log("\n2. threshold behavior per form");
write(B, "form", "accounts", "per account", "total attempts", "lockout", "first threshold seeing it");
for (const b of FORM) {
  const seeing = THRESHOLD.filter((e) => sees(b, e));
  write(B, b.name, b.accountCount, b.budget, b.accountCount * b.budget,
    sees(b, THRESHOLD[0]) ? `at attempt ${THRESHOLD[0].limit}` : "never triggers",
    seeing.length ? seeing[0].name : "none");
}

// How many accounts would drop if each form targeted every account in turn.
const drop = (b, lockoutOn) => {
  const budget = lockoutOn ? Math.min(b.budget, THRESHOLD[0].limit) : b.budget;
  return H.filter((h) => h.rank <= budget).length;
};

const C = [-13, 14, 15, 22, 8, 22];
console.log("\n3. how many of the targeted accounts drop");
write(C, "form", "no lockout", "with lockout", "lockout's difference", "diff", "duration per account");
for (const b of FORM) {
  const y = drop(b, false), v = drop(b, true);
  write(C, b.name, `${y}/${ACCOUNT_COUNT}`, `${v}/${ACCOUNT_COUNT}`, y - v,
    `${(((y - v) / Math.max(y, 1)) * 100).toFixed(0)}%`, duration(b.budget));
}
console.log(`forms where lockout reduces the number of dropped accounts: ` +
  `${FORM.filter((b) => drop(b, false) > drop(b, true)).length}/${FORM.length}`);

const p = FORM[2];
const D = [-24, 10, 10, 18];
console.log("\n4. which threshold sees spraying");
write(D, "threshold", "scale", "limit", "sees it?");
for (const e of THRESHOLD)
  write(D, e.name, e.scale, e.limit, sees(p, e) ? `at attempt ${e.limit}` : "does not see it");
console.log(`spraying makes ${p.budget} attempts per account; since the lockout threshold is ` +
  `${THRESHOLD[0].limit}, it never triggers on any account. Total ${p.accountCount * p.budget} attempts ` +
  `exceeds the rate threshold by ${(p.accountCount * p.budget / THRESHOLD[2].limit).toFixed(1)}x, taking ${duration(p.accountCount * p.budget)}`);

const E = [-24, 9, 18, 36];
console.log("\n5. the legitimate-work cost of the thresholds");
write(E, "threshold", "legit", "forms seeing it", "forms it sees");
for (const e of THRESHOLD) {
  const g = FORM.filter((b) => sees(b, e));
  write(E, e.name, e.legit, `${g.length}/${FORM.length}`, g.map((b) => b.name).join(",") || "-");
}
console.log(`total legitimate-work cost ${THRESHOLD.reduce((t, e) => t + e.legit, 0)} fictional items/day; ` +
  `the total cost of the two thresholds that see all three forms ${THRESHOLD[1].legit + THRESHOLD[2].legit}`);
```

```
400 fictional accounts, 3 forms, 3 thresholds

1. distribution of accounts across the candidate sequence
bucket               accounts    share
first 5                    24     6.0%
first 500                  60    15.0%
first 50,000               96    24.0%
first 5,000,000            40    10.0%
out of budget             180    45.0%

2. threshold behavior per form
form           accounts    per account  total attempts         lockout  first threshold seeing it
brute-force           1        1000000         1000000    at attempt 5            account-lockout
dictionary            1          50000           50000    at attempt 5            account-lockout
spraying            400              3            1200  never triggers          source-rate-limit

3. how many of the targeted accounts drop
form             no lockout   with lockout  lockout's difference    diff  duration per account
brute-force         190/400         24/400                   166     87%              11.6 day
dictionary          180/400         24/400                   156     87%               13.9 hr
spraying             18/400         18/400                     0      0%                 3 sec
forms where lockout reduces the number of dropped accounts: 2/3

4. which threshold sees spraying
threshold                    scale     limit          sees it?
account-lockout            account         5   does not see it
source-rate-limit           source        60     at attempt 60
global-rate-threshold       system       200    at attempt 200
spraying makes 3 attempts per account; since the lockout threshold is 5, it never triggers on any account. Total 1200 attempts exceeds the rate threshold by 6.0x, taking 20 min

5. the legitimate-work cost of the thresholds
threshold                   legit   forms seeing it                       forms it sees
account-lockout                30               2/3              brute-force,dictionary
source-rate-limit               4               3/3     brute-force,dictionary,spraying
global-rate-threshold           0               3/3     brute-force,dictionary,spraying
total legitimate-work cost 34 fictional items/day; the total cost of the two thresholds that see all three forms 4
```

## Two Numbers, Three Forms

The second output separates the three forms with only two numbers: how many accounts it
touches, how many attempts it makes per account. Brute force is 1 account and 1,000,000
attempts; dictionary 1 account and 50,000; spraying 400 accounts and **3** attempts.

The total attempt count does not reverse the ranking, but it is misleading: spraying's total is
1,200, which is 1.2 thousandths of brute force's. Against that, brute force targets a single
account, spraying all four hundred. **Naming an attack form by its total attempt count does not
say which threshold will see it.**

The lockout column gives the difference directly. Brute force and dictionary both hit lockout on
the fifth attempt; spraying never hits it at all. The reason is a single comparison: lockout
counts **per account**, and spraying's per-account attempt count is under the threshold.
Lowering the threshold from 5 to 3 is not enough either; spraying reduces its budget to 2 and
still stays under. **A threshold that counts at the account scale, by definition, does not see a
form that stays small at the account scale.**

## How Many Accounts Drop

The third output measures the case where each form targets every account in turn. With no
lockout, brute force drops 190 of 400 accounts, dictionary 180, spraying 18. With lockout, all
three forms' per-account budget drops to 5: brute force and dictionary drop to **24 accounts**,
spraying **stays at 18**.

Lockout's difference is 166 and 156 accounts in the first two forms — **87%** of the number of
dropped accounts. In spraying the difference is zero. These two numbers sitting side by side is
this lesson's result: the same threshold is the strongest item of the defense in two forms, and
nothing at all in the third.

It is also notable that all three forms converge on nearly the same number under lockout: 24, 24,
and 18. When the per-account budget drops to five, all the difference between the forms is
erased, because the difference was coming from the budget itself. Lockout does not target an
attack form, it targets the **budget**; that is why it has no effect at all on a form whose budget
was already small.

The duration column gives the other side of the cost. At the maximum rate the rate limit allows,
brute force takes **11.6 days** for a single account, dictionary **13.9 hours**, spraying **3
seconds**. Spraying does not just avoid lockout; it drops eighteen accounts in three-millionths of
the time brute force spends on one.

Duration itself is not a defense either — it is a **budget constraint**. Even if no threshold ever
kicks in over an eleven-day run, the attempt has to stay sustainable over that whole time: the
connection has to stay open, the source cannot get blocked, the account cannot get deleted. The
rate limit's real function is to grow this budget; it does not directly reduce the number of
dropped accounts, it stretches the time needed to drop one, and opens up room during that time
for detection to work.

The first output says where this difference comes from. 6% of accounts are within the first five
candidates, 45% cannot be found in any budget. Raising the budget from 50,000 to 1,000,000 only
moves the dropped-account count from 180 to 190 — **twenty times the budget, ten accounts**. The
first few items in the candidate sequence give more than everything after them combined; that is
the observation spraying rests on.

## The Threshold's Scale and Cost

The fourth output puts spraying to each of the three thresholds separately. The threshold that
counts at the account scale does not see it. The threshold that counts at the source scale sees it
at attempt 60, the one that counts at the system scale at attempt 200. The total of 1,200
attempts exceeds the rate threshold sixfold, and takes 20 minutes at the maximum rate.

This shows where the measure shifts to: against spraying, the chain might not break at the third
step, but it **is seen**. In the link that cannot be broken, the measure shifts to detection, and
detection's unit is the attempt: how many attempts pass under the threshold, how many accounts
get tried before the alarm fires.

The fifth output writes the cost. Account lockout's legitimate-work cost is 30 fictional items a day,
and it sees two of the three forms. The source rate limit's cost is 4, and it sees all three. The
rate threshold's cost is **0**, and it sees all three too — because it rejects no request at all,
it only counts and reports.

The three thresholds' total cost is 34, and the total cost of the two thresholds that see all
three forms is **4**. The two items with the widest scope carry one-eighth of the cost. This
does not mean lockout is unnecessary: lockout is the single item that brings the dropped-account
count from 190 down to 24. What it means is that scope and cost **do not accumulate on the same
item** — and a defense set cannot be evaluated without writing the two down separately.

The threshold with zero cost has a separate place, and it should be named at this course's
scale of measurement: that threshold **breaks no link at all**. Because it rejects no request, it
does not remove the third step's precondition; it only counts. Writing it in a defense list as
"the threshold that sees all three forms" is correct; writing it as "the threshold that stops all
three forms" is not. The difference between these two sentences is exactly the distinction this
course draws between a broken link and detection: an item that only counts does not shorten the
chain, it only surfaces the point where the chain is seen.

## Summary

- Three forms are separated by only two numbers: how many accounts it touches and how many
  attempts it makes per account. Brute force is 1 account/1,000,000 attempts, dictionary
  1/50,000, spraying 400/3.
- Account lockout stops brute force and dictionary at the fifth attempt; it never sees spraying
  at all, because it counts at the account scale and spraying's per-account budget is under the
  threshold.
- Lockout brings the dropped-account count down from 190 and 180 to 24 (an 87% reduction); in
  spraying the difference is 0.
- At the maximum rate, brute force takes 11.6 days for a single account, dictionary 13.9 hours,
  spraying 3 seconds for 18 accounts. Raising the budget from 50,000 to 1,000,000 raises the
  dropped-account count by only 10.
- The threshold that counts spraying at the source scale sees it at attempt 60, the one that
  counts at the system scale at attempt 200; the total of 1,200 attempts exceeds the rate
  threshold sixfold. In the link that cannot be broken, the measure shifts to detection.
- The three thresholds' total legitimate-work cost is 34 fictional items/day; the total of the two
  thresholds that see all three forms is 4. Scope and cost do not accumulate on the same item,
  and the zero-cost threshold breaks no link at all — it only counts.

## Next Step

In all six classes so far, the chain always started with a request from the attacker: a field
was filled in, a link was sent, an attempt was made. In the next class, the first step is thrown
by the victim, and what they do is ordinary — they open a page. The next lesson counts this class
as a **precondition chain**: how many conditions have to hold at the same time, how many
combinations remain once one of the conditions falls away, and which browser and network checks
break how many conditions at once.
