---
title: 'Domain Name and Registration Lookup'
source: 'https://academia.sh/en/courses/penetration-testing/domain-name-and-registration-lookup'
course: 'Security Tooling and Penetration Testing'
language: en
updated: '2026-09-26T03:02:55+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - "Separates the claims returned by record lookup by evidentiary strength and measures each criterion's hit rate"
    - 'Calculates the trade-off between coverage and hit rate as the criterion set grows'
    - 'Compares the cache and direct query paths in terms of trace and certainty'
---

# Domain Name and Registration Lookup

Measuring a record's evidentiary strength: four record criteria's hit rates on 20 candidates ranging from 100% to 25%, the three strong criteria combined splitting off 10 candidates and capturing 8 evidence with zero false positives, the weak fourth criterion bringing 0 new evidence and dropping the hit rate from 80% to 62%, 60% of the 10 silent candidates being false positives while one is a real asset, and the cache path leaving 0 records while missing 1 evidence item and adding 2 false positives.

The previous lesson split twenty candidates into nine evidence, five leads, and six false
positives — but what made that split was a referee that came from outside: the inventory the
scope owner approved. In a real assessment, that inventory is the output, not the input. This
lesson tries a split without a referee: the candidates' names are queried, and the **evidentiary
strength** of the records that come back is measured.

What record types are for and how they are managed was covered in the Endpoint and System
Hardening course. What is measured here is different: a record carries a **claim**, and the
claim's value depends on who could write that record. Some records can only be created by the
party controlling the name; others are the business of the third party hosting the name and say
nothing about ownership.

## A Record Is a Claim, Not Evidence

**RE4:** four pieces of information are kept for each candidate — the real class the referee
assigned, whether the organization's name appears in the registration record, whether a
verification record exists that only the party controlling the name could write, and the address
block the name resolves to. **RE5:** a *criterion* is a rule that counts a candidate as evidence;
it is referred to by two numbers — **hit rate** (of the candidates it triggers, how many are
actually evidence) and **coverage** (how much of all the evidence it captures). **RE6:** a
response from the cache leaves no record on the target but can be stale; a response queried
directly from the authoritative server is fresh, and every query leaves a record.

This lookup is part of an authorized assessment and is limited strictly to the names within the
defined scope.

```js
// recon/record.mjs — fictional record-lookup results for the twenty candidates from lesson 01
// (a model): no real domain name, no real address, no real registration record. No randomness.

// RE4: each row carries four things for a candidate — the real class per the scope owner's
// inventory (referee), whether the organization's name appears in the registration record,
// whether a verification record exists that only the party controlling the name could write,
// and the address block the name resolves to. Block A is org-assigned, B is shared hosting,
// C is third party, "-" means it does not resolve. "zone" says who answers the name's query,
// and "stale" gives the cache's outdated block value.
export const CANDIDATE = `
  subscriber-portal            evidence  1 1 A org -
  field-app                    evidence  1 1 A org -
  branch-vpn-endpoint          evidence  1 0 A org -
  collector-gateway            evidence  0 0 A org C
  report-endpoint              evidence  1 1 A org -
  test-portal                  evidence  0 0 C org -
  shared-interface              evidence  1 0 B org -
  work-order-endpoint          evidence  1 1 A org -
  meter-update-endpoint        evidence  0 1 A org -
  old-subscriber-portal        lead      1 0 C org -
  marketing-page                lead      0 0 C org -
  cloud-storage-bucket          lead      0 0 C outside -
  identity-provider-endpoint    lead      0 1 C outside -
  training-environment          lead      0 0 B org -
  payment-provider-endpoint     falseP    0 0 C outside -
  old-field-app                 falseP    0 0 B outside -
  spare-domain                   falseP    0 0 - outside -
  dealer-portal                  falseP    0 0 B outside -
  support-portal                 falseP    0 0 C outside A
  old-report-endpoint            falseP    0 0 - org A`
  .trim().split("\n").map((r) => {
    const [name, cls, registration, ownership, block, zone, stale] = r.trim().split(/\s+/);
    return { name, class: cls, registration: +registration, ownership: +ownership, block, zone, stale };
  });

// RE5: a rule is a criterion that counts a candidate as evidence. Hit rate = of the triggered
// candidates, how many are actually evidence; coverage = how much of all the evidence this
// rule alone finds.
export const RULE = [
  { name: "ownership record", f: (a, b) => a.ownership === 1 },
  { name: "registration record", f: (a, b) => a.registration === 1 },
  { name: "org-assigned block", f: (a, b) => b === "A" },
  { name: "shared hosting block", f: (a, b) => b === "B" },
];

// RE6: a response from the cache leaves no record on the target but can be stale; a response
// queried directly from the authoritative server is fresh and leaves two records for every
// name in the org's zone.
export const blockOf = (a, path) => (path === "cache" && a.stale !== "-" ? a.stale : a.block);
export const EVIDENCE = CANDIDATE.filter((a) => a.class === "evidence").length;
```

```js
// recon/ownership.mjs — which record proves ownership, which one only leaves a lead.
import { CANDIDATE, RULE, EVIDENCE, blockOf } from "./record.mjs";

const write = (g, ...s) => console.log(s.map((v, i) =>
  (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join(""));
const count = (l, s) => l.filter((a) => a.class === s).length;
const rate = (l) => l.length ? ((count(l, "evidence") / l.length) * 100).toFixed(0) : "-";

console.log(`${CANDIDATE.length} candidates, ${EVIDENCE} evidence, ${count(CANDIDATE, "lead")} leads, ` +
  `${count(CANDIDATE, "falseP")} false positives; ${RULE.length} record criteria, 3 queries per candidate`);

const A = [-24, 7, 7, 7, 9, 9, 10];
console.log("\n1. hit rate and coverage per record criterion");
write(A, "criterion", "cand", "evid", "lead", "falseP", "hit %", "cover %");
for (const k of RULE) {
  const l = CANDIDATE.filter((a) => k.f(a, a.block));
  write(A, k.name, l.length, count(l, "evidence"), count(l, "lead"), count(l, "falseP"), rate(l),
    ((count(l, "evidence") / EVIDENCE) * 100).toFixed(1));
}

const B = [-32, 7, 7, 7, 9, 9, 10];
console.log("\n2. as criteria are combined (strongest to weakest)");
write(B, "set", "cand", "evid", "lead", "falseP", "hit %", "cover %");
const combined = [];
for (const k of RULE) {
  combined.push(k);
  const l = CANDIDATE.filter((a) => combined.some((x) => x.f(a, a.block)));
  write(B, `+ ${k.name}`, l.length, count(l, "evidence"), count(l, "lead"), count(l, "falseP"), rate(l),
    ((count(l, "evidence") / EVIDENCE) * 100).toFixed(1));
}

const strong = RULE.slice(0, 3);
const triggered = CANDIDATE.filter((a) => strong.some((k) => k.f(a, a.block)));
const silent = CANDIDATE.filter((a) => !strong.some((k) => k.f(a, a.block)));
const C = [-32, 7, 7, 7, 9, 10];
console.log("\n3. the two lists the first three criteria split off");
write(C, "list", "cand", "evid", "lead", "falseP", "falseP %");
for (const [name, l] of [["triggered", triggered], ["no criterion triggered", silent]])
  write(C, name, l.length, count(l, "evidence"), count(l, "lead"), count(l, "falseP"),
    ((count(l, "falseP") / l.length) * 100).toFixed(1));
console.log("evidence not triggered: " +
  silent.filter((a) => a.class === "evidence").map((a) => a.name).join(", "));

const D = [-26, 9, 7, 9, 8, 8, 10];
console.log("\n4. two query paths: trace vs. certainty");
write(D, "path", "records", "alerts", "stale", "evid", "falseP", "hit %");
for (const path of ["cache", "direct"]) {
  const l = CANDIDATE.filter((a) => strong.some((k) => k.f(a, blockOf(a, path))));
  const record = path === "direct"
    ? CANDIDATE.filter((a) => a.zone === "org").length * 2 : 0;
  const stale = path === "cache" ? CANDIDATE.filter((a) => a.stale !== "-").length : 0;
  write(D, path, record, 0, stale, count(l, "evidence"), count(l, "falseP"), rate(l));
}
const cacheTriggered = CANDIDATE.filter((a) => strong.some((k) => k.f(a, blockOf(a, "cache"))));
console.log(`evidence missed by the cache path: ` +
  triggered.filter((a) => !cacheTriggered.includes(a)).map((a) => a.name).join(", ") +
  `; false positives added: ` +
  cacheTriggered.filter((a) => !triggered.includes(a)).map((a) => a.name).join(", "));
```

```
20 candidates, 9 evidence, 5 leads, 6 false positives; 4 record criteria, 3 queries per candidate

1. hit rate and coverage per record criterion
criterion                  cand   evid   lead   falseP    hit %   cover %
ownership record              6      5      1        0       83      55.6
registration record           7      6      1        0       86      66.7
org-assigned block            7      7      0        0      100      77.8
shared hosting block          4      1      1        2       25      11.1

2. as criteria are combined (strongest to weakest)
set                                cand   evid   lead   falseP    hit %   cover %
+ ownership record                    6      5      1        0       83      55.6
+ registration record                 9      7      2        0       78      77.8
+ org-assigned block                 10      8      2        0       80      88.9
+ shared hosting block               13      8      3        2       62      88.9

3. the two lists the first three criteria split off
list                               cand   evid   lead   falseP  falseP %
triggered                            10      8      2        0       0.0
no criterion triggered               10      1      3        6      60.0
evidence not triggered: test-portal

4. two query paths: trace vs. certainty
path                        records alerts    stale    evid  falseP     hit %
cache                             0      0        3       7       2        64
direct                           26      0        0       8       0        80
evidence missed by the cache path: collector-gateway; false positives added: support-portal, old-report-endpoint
```

The numbers belong to the **measurement** class; their inputs are RE4–RE6.

## Four Criteria, Four Different Hit Rates

The first table runs all four criteria over the same twenty candidates, and their hit rates
spread **between 100% and 25%**. That spread is the lesson's main finding: the sentence "a record
lookup was performed" says nothing at all, because which record carries which claim changes from
criterion to criterion.

The org-assigned address block criterion triggers seven candidates and **all seven are
evidence** — 100% hit rate. The reason is structural: if an address block is allocated to an
organization, for a name resolving into that block to belong to another organization requires the
block owner's cooperation. Registration record follows at 86% and ownership record at 83%; both
produce evidence, but both also cover only a little more than half.

The fourth criterion shows the opposite. Shared hosting block triggers four candidates; **one is
evidence, one is a lead, two are false positives** — 25% hit rate. On a shared hosting block,
hundreds of organizations' names resolve to the same address; the observation "it resolves to the
same address" says almost nothing about ownership there. Two observations that look identical —
a name resolves to an address — carry a fourfold difference between evidence and noise depending
on the block's kind.

The single lead item triggered by the ownership record criterion is also instructive.
`identity-provider-endpoint` has a valid ownership record; the organization has proven it
controls that name. The item still is not evidence, because the record proves control of the
**name**, not who the **asset** belongs to. The name is the organization's; the service running
on it is a third party's.

## Where the Return on Combining Stops

The second table combines criteria from strongest to weakest. The first three criteria combined
trigger **10 candidates** and contain 8 evidence, 2 leads, **0 false positives**: 80% hit rate,
88.9% coverage. When the fourth criterion is added, candidates rise to 13, evidence **stays at
8**, false positives rise from 0 to 2, and the hit rate drops to 62%.

This is the same curve as the previous lesson's, and here its shape is stricter: the weak
criterion **brings in no new evidence at all**, it only adds elimination burden. The coverage
column reads 88.9% on both rows. The question to ask when adding a new rule to a criterion set
is not "does it find more candidates," it should be "**does it increase coverage**"; in this table
the fourth criterion's contribution to coverage is zero.

## The Ten Candidates That Stay Silent

The third table shows the other half of the list. None of the three strong criteria fire for
**10 candidates**, and **6 of these ten are false positives** — 60%. So record lookup really does
work as an elimination tool: **all six** of the six false positives are on this silent list, and
none of them has leaked into the evidence list.

In return, one real asset is lost. `test-portal` belongs to the organization, sits in the
inventory, and no criterion triggers for it: it is not in a dedicated block because it is hosted by
a third party, it does not appear in the registration record, and it has no ownership record.
Record lookup tells this candidate "I cannot say anything about it," and a binary elimination rule
reads that as "does not belong to the organization."

The numeric summary is this: eight of nine pieces of evidence can be confirmed by a record
(88.9%); one cannot be confirmed and falls into the silent list. The silent list **is not a list to
discard, it is a separate list**; inside it, the 10% real asset stands next to the 60% noise.

## The Trade-off Between Trace and Freshness

The fourth table opens this course's trace column for the second time, and for the first time
shows a **trade-off**. If queries are served from a resolver's cache, **0 records** remain on the
target; if the target's authoritative server is queried directly, two queries for each of the
thirteen names in the org's zone produce **26 records**.

The path that looks free is not free. The cache holds three stale responses, and the result breaks
in three places: `collector-gateway` stops being evidence (the stale response places it in the
third-party block), while `support-portal` and `old-report-endpoint` show up in the org-assigned
block because of their stale records and **enter the evidence list as false positives**. The hit
rate drops from 80% to 64%.

The choice between the two paths carries a pattern this course will run into again and again:
**leaving no trace means giving up certainty**. Read from the blue team's side, it is symmetric
too — the 26 records on the authoritative server produce no alert at all, because a name query is
the network's most ordinary event. This step is visible on the defense side but not noticed;
noticing it depends on later correlating **which names** the queries targeted and in what order.

## The Cost of Sixty Queries

Each of the twenty candidates required three queries: registration record, address, and
ownership record. A total of **60 queries** produced eight pieces of evidence — **7.5 queries per
evidence item**. The previous lesson's validation cost was 5.00 steps per evidence item; this
lesson runs above that cost and gives something back in return: the previous lesson's nine pieces
of evidence were read from a referee, this lesson's eight **verify themselves**.

The cost is not evenly distributed either. The ten candidates that return no record still spend
their three-query share; the silent list's total cost is 30 queries and it produces **zero
evidence** in return. Half of the validation budget goes to a list whose outcome is "unknown" —
and this spending is not wasted, because it is what drops six false positives from the list.

There is also a boundary worth distinguishing in terms of scope: **seven** of the twenty names sit
outside the organization's zone. Querying these names is reading publicly available data and
creates no scope problem; **acting on** those names, however, does step outside scope. An item
that is free to query is not necessarily an item that is free to work on.

## Summary

- A record carries a claim, and the claim's strength depends on who can write the record: the
  four criteria's hit rates are 100%, 86%, 83%, and 25%.
- The org-assigned address block criterion is correct on 7 of 7 candidates; the shared hosting
  block criterion only on 1 of 4 — the same kind of observation carries a fourfold difference in
  value depending on the block's kind.
- Ownership record proves control of the name, not ownership of the asset: a candidate carrying a
  valid ownership record is a third party's service and does not count as evidence.
- The three strong criteria combined trigger 10 candidates, capture 8 evidence, and produce 0
  false positives; the fourth criterion adds 3 candidates and 2 false positives, brings 0 new
  evidence, and drops the hit rate from 80% to 62%.
- 6 of the 10 candidates no criterion triggers for are false positives (60%), but one is a real
  asset; the silent list is not a list to discard, it is a separate list.
- The cache path leaves 0 records on the target, misses 1 evidence item due to 3 stale responses,
  and adds 2 false positives; the direct path leaves 26 records, produces 0 alerts, and keeps the
  hit rate at 80%.

## Next Step

Up to this point the candidate list arrived ready-made: twenty names came out of the previous
lesson's publicly available items, and this lesson only classified them. But the list is not a
closed set. Under an organization's domain name there can be names that appear in no publicly
available document and have a link on no page; the way to find them is not reading documents, it is
**scanning the name space**.

This scan produces two new numbers. The first is coverage: how many candidates do different
discovery methods pull out of the same name space, how much do they overlap, and which one alone
is enough. The second is this course's most attention-grabbing item: some of the names found
point to **a target that no longer exists** — items whose record has stopped, whose counterpart
has fallen away, whose owner is unclear. The next lesson measures subdomain and asset discovery
along these two axes and counts how many records each method leaves on the defense side.
