Agent Skillselementalsouls/Claude-OSINT › cloud-saas-exposure

cloud-saas-exposure

GitHub

用于授权外部侦察中云存储桶发现、AWS账号ID离线恢复、供应链依赖混淆确认及云原生/K8s基础设施指纹识别的被动探测技能。

skills/cloud-saas-exposure/SKILL.md elementalsouls/Claude-OSINT

Trigger Scenarios

枚举目标云存储足迹并评估风险 从泄露的AKIA密钥离线恢复AWS账号ID 确认npm/PyPI内部依赖是否为供应链攻击向量 对云原生或K8s控制平面进行被动指纹识别

Install

npx skills add elementalsouls/Claude-OSINT --skill cloud-saas-exposure -g -y
More Options

Use without installing

npx skills use elementalsouls/Claude-OSINT@cloud-saas-exposure

指定 Agent (Claude Code)

npx skills add elementalsouls/Claude-OSINT --skill cloud-saas-exposure -a claude-code -g -y

安装 repo 全部 skill

npx skills add elementalsouls/Claude-OSINT --all -g -y

预览 repo 内 skill

npx skills add elementalsouls/Claude-OSINT --list

SKILL.md

Frontmatter
{
    "name": "cloud-saas-exposure",
    "version": 1,
    "triggers": [
        "cloud attack surface",
        "cloud exposure",
        "SaaS exposure",
        "cloud bucket enumeration",
        "S3 bucket enum",
        "GCS bucket enum",
        "Azure blob enum",
        "bucket takeover",
        "dangling CNAME bucket",
        "public cloud bucket",
        "listable bucket",
        "bucket ownership",
        "object storage exposure",
        "bucket permutation",
        "AWS account ID",
        "AWS account ID from access key",
        "AKIA decode",
        "access key account ID",
        "offline AWS decode",
        "base32 AWS account",
        "AWS account enumeration",
        "cross-account trust",
        "IAM role phishing",
        "sts assume role",
        "dependency confusion",
        "npm dependency confusion",
        "PyPI dependency confusion",
        "supply chain attack surface",
        "unclaimed package",
        "internal package registry",
        "private npm registry",
        "scoped npm package",
        "npm scope claimability",
        "package registry leak",
        "cloud native fingerprint",
        "Lambda function URL",
        "API Gateway exposure",
        "Cloud Run exposure",
        "App Service exposure",
        "serverless exposure",
        "Kubernetes exposure",
        "K8s exposure",
        "kubelet exposure",
        "etcd exposure",
        "Docker API exposure",
        "CI CD exposure",
        "Jenkins exposure",
        "GitLab exposure",
        "control plane exposure",
        "cloud footprint",
        "cloud account attribution"
    ],
    "description": "Organization-grade cloud and supply-chain attack-surface discovery: S3\/GCS\/Azure Blob bucket discovery via observed-name mining (CNAME\/cert-SAN\/Wayback) and bounded two-class permutation (6 prefixes x 15 suffixes on trusted tokens, bounded target-bound expansion on subdomain stems), existence (HEAD\/GET) vs public-listing confirmation, object-key triage into 9 value tiers (database dumps, credentials, IaC state, kubeconfig, VCS dirs, config, archives, PII, logs), dangling-CNAME bucket-takeover detection, and the ownership-gated severity model that stops an unattributable public bucket from becoming a false CRITICAL; the fully offline AWS-account-ID recovery from a leaked AKIA\/ASIA\/AROA access key (base32 decode, runnable stdlib Python, canonical test vector, AWS-documentation-example-ID screening); dependency-confusion confirmation for npm\/PyPI (internal-signal classifier -- private-registry binding vs org-namespace match -- paired with a read-only public-registry 404 check and the npm scope-claimability nuance the public search API misses); and passive cloud-native\/container\/Kubernetes\/CI control-plane fingerprinting (Lambda URLs, API Gateway, Cloud Run, App Service, kubelet\/etcd\/K8s API\/dashboard, Jenkins\/GitLab\/Argo CD) as an org-attribution and exposure surface. Passive\/discovery only -- no exploitation, no credential submission, no active control-plane confirmation (a stage-6 validate_cloud active tier is described but out of scope). Use when enumerating a target's cloud storage footprint, recovering an AWS account ID from a leaked key, confirming a supply-chain dependency-confusion vector, or fingerprinting cloud-native\/K8s\/CI infrastructure for an authorized external recon engagement."
}

Cloud & SaaS Exposure — Buckets, Offline AWS Account-ID Recovery, Dependency Confusion, and Cloud-Native/K8s Fingerprinting

Companion skills: osint-methodology (the pipeline this plugs into — Stage 2 asset expansion, Stage 4 exposure analysis, Stage 5 supply-chain confirmation). offensive-osint §16.8 (bucket-permutation raw wordlist), §16.17 (cloud-native URL pattern table), §16.18– §16.19 (container/K8s/CI paths + active curl recipes), §44 (package-registry search). This skill does not repeat those lists — it builds the reasoning layer on top: the ownership-gated bucket severity model, the offline AKIA→account-ID decode, the dependency-confusion two-part confirmation contract, and cloud-native/K8s as an org-attribution surface, not just another probe list.

0. When to Use / When NOT

Use this skill when: you're mapping an authorized target's cloud and supply-chain footprint — enumerating storage buckets and judging whether a hit is actually the target's risk (not a stranger's public bucket that happens to match a permutation); recovering the AWS account ID behind a leaked access key you already hold (dead or live); confirming whether an internal-looking npm/PyPI dependency is a registerable supply-chain vector; or fingerprinting cloud-native (Lambda/Cloud Run/App Service/…) and container/K8s/CI control-plane surface for org attribution and exposure triage.

Do NOT use this skill when: you just need the raw bucket-permutation wordlist, cloud-native URL pattern table, or container/K8s/CI path list with no reasoning layer — go straight to offensive-osint §16.8/§16.17–16.19/§44. Do NOT use it for anything past discovery/confirmation: registering an unclaimed package, submitting AWS credentials, authenticating to a Kubernetes API, or confirming a fingerprinted control plane actually answers unauthenticated (that's a stage-6 --validate --validate-cloud active tier — described, never performed, §5/§9.4).


1. Authorization & Legal Posture

Reuses osint-methodology §1 — assets you own or have written authorization to assess. Three of this skill's four subsystems carry a distinct authorization shape, worth being explicit about before you run any of them:

  • Bucket probing (§6) is a real HTTP GET against bucket infrastructure that may or may not be the target's — the same "active but low-intrusion" tier as any other GET against target-adjacent infra. Standard engagement authorization applies.
  • AWS account-ID decode (§7) is fully offline. No authorization question beyond already lawfully holding the leaked key.
  • Dependency-confusion confirmation (§8) issues live GETs, but only against the public npm/PyPI registries — zero packets to the target. This is why it's explicitly in scope even though it's "active."
  • Cloud-native/K8s/CI fingerprinting (§9) is pattern-matching over hostnames and ports already resolved by earlier recon — no new network traffic of its own.

2. Confidence Levels

Reuses osint-methodology §2. Domain-specific anchors:

Level Cloud/SaaS example
TENTATIVE Org-namespace-matched dependency name (medium strength) with no private-registry binding — namesake-prone.
FIRM Bucket exists (403/private) and is name-tied or CNAME/SAN/Wayback-observed; AWS account ID offline-decoded from a leaked key with no live validation; cloud-native endpoint pattern-matched via an owned CNAME/cert-SAN/subdomain FQDN; a container/K8s orchestration port is open (directly observed — its auth posture is not).
CONFIRMED Bucket is publicly listable/readable and ownership-verified; AWS account ID corroborated by a live STS-validated key; dependency-confusion "strong" signal (private-registry binding) confirmed unclaimed via the public-registry 404 + scope-claimability check.

An unattributable public bucket hit does not get a confidence label at all — it never reaches the finding stream (§6.3).


3. Output Format

Finding:
  id:          <stable hash or UUID>
  module:      <technique that discovered it>
  asset_key:   <typed key, e.g. bucket:s3:acme-backup, account:aws:609629065308>
  category:    <PUBLIC_BUCKET | INFO_DISCLOSURE | TAKEOVER | DEPENDENCY_CONFUSION | MISCONFIG | OPEN_SERVICE | EXPOSED_PANEL>
  severity:    <info|low|medium|high|critical>
  confidence:  <tentative|firm|confirmed>
  title:       <one-line summary>
  description: <2-5 sentences — issue + attacker impact, not a terse restatement of the title>
  evidence:
    url:       <where found>
    timestamp: <UTC ISO8601>
    sha256:    <hash of any downloaded artifact — never an object body, see §5>
    raw:       <truncated to 2 KiB>
  references:  [<advisory URL, vendor doc>]
  remediation: <action the asset owner can take>

UTC timestamps everywhere.


4. Source Hygiene & Citations

URL + UTC timestamp + SHA-256 + tool version + run_id, every artifact. For bucket listings, hash the listing response (the XML/JSON), never an object's contents — this skill never fetches an object body (§5). For a decoded AWS account ID, cite the key string it was derived from (redacted to first/last 4 chars in client-facing output) and the algorithm version. For a dependency-confusion hit, cite both registry-check timestamps (the package 404 and, for scoped npm, the scope-ownership 404) — claimability is a point-in-time fact that can flip the moment someone else registers the name.


5. Do NOT

  • Do NOT fetch or download the contents of any object inside a bucket. Listing keys / sampling object names from the listing response is in scope (§6.3); retrieving an object's body is not — the reference implementation gates raw object reads behind --validate.
  • Do NOT claim CRITICAL severity for a bucket, endpoint, or account hit that isn't ownership-verified. An unattributable public hit is not the client's risk — see the ownership-gated model, §6.3.
  • Do NOT register, reserve, or publish a package name found unclaimed by the dependency-confusion check. Confirmation only — §8.7.
  • Do NOT use a decoded/derived AWS account ID to call AWS APIs (sts:AssumeRole, GetCallerIdentity) against real infrastructure, with your own or the target's credentials. That reverse step needs the operator's own AWS credentials against a third party's account, is CloudTrail-logged on the target's side, and is out of this skill's scope entirely — describe the pivot value (§7.1), never perform it.
  • Do NOT actively probe or authenticate against a fingerprinted Kubernetes API, etcd, kubelet, or Docker daemon endpoint to confirm its auth posture. Passive fingerprint only — live confirmation is a stage-6 --validate --validate-cloud active tier, out of scope.
  • Do NOT single-source attribute a bucket/account/dependency/endpoint to the target — apply the rule of three (osint-methodology §2) or the explicit ownership signals in §6.3/§7.7/§9.1.

6. Storage Bucket Discovery — Candidate Generation, Existence vs. Listing, and the Ownership-Gated Severity Model

6.1 Two-class candidate generation

Two distinct expansion classes — mixing them either misses brand buckets or produces a candidate storm (an unconstrained full-expansion-per-subdomain-stem approach measured at ~95k candidates on a large-subdomain target).

Class A — trusted tokens (apex root, the domain with dots replaced by hyphens, the domain with dots stripped, and a sanitized --company token if supplied): full prefix × suffix expansion.

6 prefixes: "" (bare) backup- assets- static- dev- prod-

15 suffixes: "" (bare) -backup -assets -static -media -data -uploads -dev -prod -staging -logs -private -public -dump -archive

→ up to 90 candidates per trusted token.

Class B — subdomain stems (the first label of every discovered subdomain, excluding www/mail/ns1/ns2): bounded and target-bound only —

  • Bare probe, but only when the stem is distinctive: not in the broad (~90-entry) generic-stem filter (api, admin, backup, dev, staging, mail, data, docs, internal, vault, secure, sandbox, preprod, … — a stricter, larger list than the 47-word variant in offensive-osint §16.8) and longer than 3 characters — or the stem already contains a trusted token.
  • Target-bound permutation only: {apex_root|company}-{stem} and {stem}-{apex_root|company} (hyphen joiner, both orders).

No standalone prefix/suffix expansion is ever applied to a subdomain stem. Every candidate must satisfy the bucket-name shape ^[a-z0-9][a-z0-9.\-]{1,61}[a-z0-9]$ (3–63 characters).

Observed-name mining bypasses the filter entirely. A bucket name mined from the target's own DNS, certificate, or archived pages is near-certain to be theirs, so it's probed regardless of the rules above:

  • subdomain CNAME chains resolving to *.s3(.<region>).amazonaws.com, *.storage.googleapis.com, or *.blob.core.windows.net
  • certificate SANs matching the same three host patterns
  • archived (Wayback) URLs, path- or vhost-style, referencing an S3/GCS/Azure Blob host

6.2 Probe technique — existence vs. listing

One GET per candidate per provider — no separate HEAD, since the GET body is needed anyway to classify listing:

Provider URL Listing marker (200) Exists, no listing Not found
S3 GET https://{name}.s3.amazonaws.com/ <ListBucketResultLISTABLE 200/301/307 (no marker) or 403 → exists (403 = private; parse <Code> for the deny reason) NoSuchBucket (404) → absent
GCS GET https://storage.googleapis.com/{name} <ListBucketResultLISTABLE 403 → exists, private 404 → absent
Azure Blob GET https://{name}.blob.core.windows.net/?comp=list <EnumerationResultsLISTABLE 400/403 → account exists, private 404 → absent

S3 region redirects (301/307) are followed once before final classification. On a listable hit, sample up to 200 object keys straight out of the listing body already returned (<Key>…</Key> for S3/GCS, <Blob><Name>…</Name> for Azure) — never a second request per object, never a fetch of an object's contents.

6.3 The ownership-gated severity model

This is the discipline that stops a permutation hit on a stranger's globally-registered bucket from becoming a false CRITICAL — the exact class of false positive an earlier, unconstrained version of this logic produced: unverified public/listable hits emitted at MEDIUM/LOW severity were the documented source of a 171-finding false-positive flood before this gate was added. (A related, separate incident — filing a merely-exists-but-private 403/400 bucket as a PUBLIC_BUCKET exposure — produced a "33 listable buckets" false read on a fully-private estate; §6.2's reachability classification is what fixed that one.) Two independent axes decide the outcome — what's reachable and whether ownership is verified — and the outcome is no finding at all, not a downgraded severity, whenever the second axis fails.

Ownership verified means at least one of:

  • name-tied — the candidate name contains a trusted token (apex root / domain variant / --company)
  • observed — the name was mined from the target's own CNAME, cert SAN, or archived URL (§6.1)
  • branded object key — on a listable bucket, at least one returned object key itself names the target (checked after listing, so this can promote an otherwise-generic name)
Reachability Ownership unverified Ownership verified
Listable (public listing) No finding. Recorded as a Bucket asset only. PUBLIC_BUCKET, CRITICAL, CONFIRMED
Public-read, not listable No finding. Asset only. PUBLIC_BUCKET, MEDIUM, CONFIRMED
Exists, access denied (403/400) No finding. Asset only. INFO_DISCLOSURE, INFO, FIRM
Not found (404) Nothing — unless a live subdomain CNAME still points at the absent name (§6.4)

The unverified rows are a deliberate zero, not a soft downgrade to LOW/INFO. A generic permutation like static-delivery or logs-archive matches thousands of unrelated companies' real, live buckets globally; surfacing every one as the client's finding is exactly what this model exists to prevent. The candidate is still recorded as a graph asset for correlation and re-probe on a future scan — it simply never enters the finding stream unattributed.

Object-key triage on a listable bucket — classify the exposed keys into value tiers so the write-up states what is exposed, not merely that listing is open. Pure filename-pattern match over the listing already returned — no object body is ever fetched:

Category Severity tier Pattern examples
Database dump critical *.sql[.gz|.bz2|.zip], *dump*, *-backup*/*_backup*, *.bak, *.mdb, *.sqlite[3], *.dmp
Credentials critical .env*, id_rsa, id_ed25519, *.pem, *.p12, *.pfx, *.kdbx, *.ppk, *.jks, *credential*
IaC state critical *.tfstate[.backup], terraform.tfvars
Kubeconfig critical *kubeconfig*, .kube/config
VCS directory high .git/, .svn/, .hg/
Config high config.yml/.json, settings.py, database.yml, wp-config.php, .npmrc, .dockercfg, appsettings*.json
Archive high *.zip/.tar/.tar.gz/.tgz/.7z/.rar
PII high employee, customer, passport, aadhar/aadhaar, ssn, salary, payroll, kyc (substring match)
Logs medium *.log, access[-_.]log, error[-_.]log, audit[-_.]log

Lead the finding title with the highest tier present (e.g. "publicly listable — credentials exposed") — that one line is what turns a generic bucket finding into something the client acts on immediately.

6.4 Dangling-CNAME bucket takeover — a bonus lead, always in scope

A candidate that returns 404/NoSuchBucket but is still the live target of a subdomain's CNAME is a claimable takeover, independent of the ownership-gating table above — the live CNAME is the ownership evidence (a stranger's dangling bucket doesn't happen to have the target's subdomain pointed at it). Detection only: never register the bucket to prove it. Category TAKEOVER, HIGH, CONFIRMED — the 404-plus-dangling-CNAME combination is unambiguous.

6.5 Recipes

Probe (bash):

NAME="acme-backup"
for url in \
  "https://${NAME}.s3.amazonaws.com/" \
  "https://storage.googleapis.com/${NAME}" \
  "https://${NAME}.blob.core.windows.net/?comp=list"; do
  echo "== $url =="
  curl -sk -m 8 -o /tmp/body.xml -w 'status=%{http_code}\n' "$url"
  grep -oE '<(ListBucketResult|EnumerationResults)|<Key>[^<]+</Key>|<Code>[A-Za-z]+</Code>' /tmp/body.xml | head -20
done

Probe (PowerShell):

$Name = "acme-backup"
$Targets = @(
  "https://$Name.s3.amazonaws.com/",
  "https://storage.googleapis.com/$Name",
  "https://$Name.blob.core.windows.net/?comp=list"
)
foreach ($u in $Targets) {
  try {
    $r = Invoke-WebRequest -Uri $u -TimeoutSec 8 -SkipHttpErrorCheck   # -SkipHttpErrorCheck is PS7+; on PS 5.1 read $_.Exception.Response in the catch
    "{0,-70} status={1}" -f $u, $r.StatusCode
    if ($r.Content -match '<ListBucketResult|<EnumerationResults') {
      [regex]::Matches($r.Content, '<Key>([^<]+)</Key>|<Name>([^<]+)</Name>') |
        Select-Object -First 15 |
        ForEach-Object { $_.Groups[1].Value + $_.Groups[2].Value }
    }
  } catch { "$u -> error: $($_.Exception.Message)" }
}

Candidate generation — apex + full prefix/suffix expansion (bash):

APEX="acme"
PREFIXES=("" "backup-" "assets-" "static-" "dev-" "prod-")
SUFFIXES=("" "-backup" "-assets" "-static" "-media" "-data" "-uploads" "-dev" "-prod" "-staging" "-logs" "-private" "-public" "-dump" "-archive")
for p in "${PREFIXES[@]}"; do
  for s in "${SUFFIXES[@]}"; do
    echo "${p}${APEX}${s}"
  done
done

Candidate generation (PowerShell):

$Apex = "acme"
$Prefixes = @("", "backup-", "assets-", "static-", "dev-", "prod-")
$Suffixes = @("", "-backup", "-assets", "-static", "-media", "-data", "-uploads", "-dev", "-prod", "-staging", "-logs", "-private", "-public", "-dump", "-archive")
foreach ($p in $Prefixes) { foreach ($s in $Suffixes) { "$p$Apex$s" } }

7. AWS Account-ID Recovery from a Leaked Access Key (Offline Decode)

Every AWS access key ID has the 12-digit owning account number encoded inside the key string itself — recoverable by a deterministic base32 decode with no secret key, no network call, and no live AWS credential. A single leaked AKIA…/ASIA…/AROA… string already in hand — even one that's long dead or rotated — discloses the org's AWS account number. This is genuinely offline OSINT (no authorization concern beyond already lawfully holding the key, §1), and it's a real gap in the general arsenal: offensive-osint §22.7 covers ARN-regex extraction and accountId JSON-field scraping, but not this decode.

7.1 Why the account ID matters

  • Cross-account trust-policy abuse — target trust policies that reference the account by number.
  • IAM role/principal enumeration — an sts:AssumeRole error-message differential ("not authorized" vs. "does not exist") distinguishes real principals from guesses. This reverse step needs the operator's own AWS credentials against the target account and is intrusive/CloudTrail-logged on the target's side — out of scope for this skill, described here only.
  • Targeted IAM-role phishing — a real account number lends legitimacy to a role-assumption pretext.

7.2 Key prefixes → entity type

The decode is identical across every prefix; the prefix only tells you which IAM entity type the key belongs to.

Prefix Entity
AKIA IAM user long-term access key
ASIA Temporary (STS) credentials
AROA IAM role
AIDA IAM user
AGPA IAM user group
AIPA EC2 instance profile
ANPA Managed policy
ANVA Managed policy version
ABIA AWS STS service bearer token
ACCA Context-specific credential

Shape: 4-character prefix + exactly 16 base32 characters (A–Z, 2–7) = 20 characters total.

7.3 The algorithm

  1. Uppercase the string; take the first 20 characters.
  2. Confirm the first 4 characters are a valid prefix (§7.2) and the remaining 16 match [A-Z2-7]{16}.
  3. Base32-decode those 16 characters → 10 raw bytes.
  4. Take the first 6 bytes, interpret as a big-endian integer z.
  5. account = (z & 0x7FFFFFFFFF80) >> 7
  6. Zero-pad to 12 digits. If the result exceeds 999,999,999,999, the input wasn't really a key (reject).

Published by Aidan Steele / Tenable; reference implementation credit to A. Frichetten (hackingthe.cloud).

7.4 Runnable — stdlib-only Python

#!/usr/bin/env python3
"""Offline AWS account-ID decode. No secret, no network, no live key needed.
Mirrors core/aws_account.py's account_id_from_access_key(). Stdlib only.

Usage:
  echo "ASIAY34FZKBOKMUTVV7A" | python3 aws_account_decode.py
  python3 aws_account_decode.py AKIA... ASIA...
  python3 aws_account_decode.py < file_with_keys.txt
"""
import base64
import re
import sys

_VALID_PREFIXES = {"AKIA", "ASIA", "AROA", "AIDA", "AGPA", "AIPA",
                    "ANPA", "ANVA", "ABIA", "ACCA"}
_KEY_RE = re.compile(r"\b(A[A-Z0-9]{3}[A-Z2-7]{16})\b")
_MASK = 0x7FFFFFFFFF80
_MAX_ACCOUNT = 999_999_999_999

# AWS documentation / reserved example account IDs — never report these as a
# real disclosure; they pervade IAM policy docs, Terraform tutorials, SDK samples.
EXAMPLE_ACCOUNT_IDS = {
    "123456789012", "111122223333", "222233334444", "333344445555",
    "444455556666", "555566667777", "666677778888", "777788889999",
    "888899990000", "999900001111", "012345678901", "000000000000",
    "123412341234", "101010101010",
}


def account_id_from_access_key(key: str):
    k = (key or "").strip().upper()[:20]
    if len(k) != 20 or k[:4] not in _VALID_PREFIXES:
        return None
    body = k[4:]
    if not re.fullmatch(r"[A-Z2-7]{16}", body):
        return None
    try:
        decoded = base64.b32decode(body)
    except Exception:
        return None
    z = int.from_bytes(decoded[:6], "big")
    account = (z & _MASK) >> 7
    if account > _MAX_ACCOUNT:
        return None
    return f"{account:012d}"


def main() -> int:
    text = " ".join(sys.argv[1:]) or sys.stdin.read()
    seen = set()
    for m in _KEY_RE.finditer(text):
        key = m.group(1)
        if key in seen:
            continue
        seen.add(key)
        acct = account_id_from_access_key(key)
        if acct is None:
            continue
        flag = " (AWS EXAMPLE ID -- not a real disclosure)" if acct in EXAMPLE_ACCOUNT_IDS else ""
        print(f"{key} -> {acct}{flag}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

7.5 Test vector

ASIAY34FZKBOKMUTVV7A -> 609629065308

Verify any reimplementation against this before trusting its output.

7.6 Example-account screening

Screen every decoded (and every ARN-extracted) account ID against the 14 documented AWS example account IDs above — 123456789012 chief among them, present in nearly every AWS IAM policy doc, Terraform tutorial, and SDK sample. Skip this check and a plain documentation page in the response corpus mints a false "owned AWS account" finding.

Two complementary offline extraction paths, in addition to the decode:

  • ARN regex (any JSON/HTML/JS body): arn:aws[a-z0-9\-]*:[a-z0-9\-]*:[a-z0-9\-]*:(\d{12}):
  • AWS-qualified accountId field only — deliberately requires the aws qualifier (aws[_-]?account[_-]?id), never a bare "accountId":"…", since a bare field is just as likely an internal billing/customer record that happens to be 12 digits.

7.7 Ownership + severity/confidence

Decode every access key found in the corpus, plus pull account IDs from ARNs and AWS-qualified accountId fields. Ownership is structural, not scored:

Disclosure path Ownership Severity Confidence
Key/ARN found on an in-scope host (the subdomain equals the target, or is a subdomain of it) Owned MEDIUM FIRM
Key/ARN found on an out-of-scope/third-party host that merely references the account Referenced INFO FIRM
Either of the above, additionally corroborated by a live STS-validated key (read-only GetCallerIdentity, offensive-osint §23.2) Owned MEDIUM CONFIRMED

Every leaking page emits a DISCLOSES_ACCOUNT edge to the account it references — for owned and referenced accounts alike. An owned account additionally earns an OWNED_BY-target edge — a legitimate attack-path pivot. A referenced-only account has just the DISCLOSES_ACCOUNT edge and is never treated as the org's own escalation target — it's a third party's account, though still worth recording.

Note the deliberate severity ceiling: MEDIUM even for an owned account, because the finding itself is "your account number is now public" — real but modest on its own (rotate the leaked key; the number can't be un-disclosed). Severity escalates only through what an attacker does with the number next (§7.1), which this skill does not perform.


8. Dependency Confusion — Internal Signal + Public-Registry Unclaimed Confirmation

offensive-osint §44 covers registry search and secret hunting inside packages the target published. This is the opposite direction: what does the target's own manifests reference? A dependency name that carries an internal signal and is unclaimed on the public registry is a name an attacker can register today and have pulled into the target's build the next time a resolver falls back to the public index.

8.1 Manifest mining (read the target's own captured corpus — never re-fetch for this)

File What it yields
package.json (dependencies/devDependencies/peerDependencies/optionalDependencies) npm package names
package-lock.json (v1 nested dependencies; v2/v3 packages keyed by node_modules/…) npm package names — richest source, includes transitive/scoped deps
.npmrc {scope}:registry= bindings + a bare default registry= — the private-registry signal
requirements.txt PyPI names + --index-url/--extra-index-url; screens out -e/local-path/git+/direct-URL deps (never a confusion vector)
pyproject.toml PEP 621 project.dependencies + Poetry tool.poetry.dependencies/group.*.dependencies, plus [[tool.poetry.source]] private-index URLs; a Poetry dep carrying an explicit git/url/path/file key is direct-source and screened out
pip.conf / pip.ini index-url/extra-index-url → private pip index signal
Any .js/.mjs/.cjs/.map body scoped npm specifiers (@scope/name) referenced in bundled/sourcemap code

8.2 The internal-signal classifier (zero I/O — pure text logic)

A dependency name is a candidate only when it carries one of these signals:

Ecosystem Signal Strength
npm, scoped (@scope/pkg) the scope is bound to a private registry in .npmrc strong
npm, scoped or unscoped .npmrc sets a private default registry strong
npm, scoped or unscoped package name/scope matches an org-namespace token (≥4 chars, not a generic corporate word — corp, solutions, technologies, holdings, …) medium
PyPI a private index (--index-url / Poetry source / pip.conf) is configured anywhere in the corpus strong
PyPI package name matches an org-namespace token medium

Org-namespace tokens come from the target's registrable brand label — never the leftmost DNS label; a target reached via staging.acme.com yields the token acme, never staging — plus any ORG-typed asset already in the graph. A bare unscoped name with no private-registry binding and no namespace match carries no signal — indistinguishable from a typo or a yanked release — and is dropped, never emitted as a candidate.

8.3 The two-part confirmation contract — neither half alone is a finding

An internal signal alone is not proof the name is available — plenty of internal packages are also correctly published privately-but-globally-unique. A public-registry 404 alone is meaningless — a typo, an unpublished/yanked version, or a local-only dep all 404 too. Both must hold:

  1. Internal signal (§8.2) — screens out ordinary public dependencies and unknown bare names.
  2. Public-registry existence check returns 404 — a read-only GET against the real registry.npmjs.org / pypi.org, never the target. This is exactly why it's in scope under the pack's "public-registry 404 checks" line even though it's a live network call: the traffic lands entirely on a third-party public registry, zero packets to the client.

Fail closed on any ambiguity — a 200 (published), 403, 5xx, or timeout never emits a finding. Uncertainty is not evidence of claimability.

8.4 npm scope claimability — the nuance a bare-package check misses

For a scoped name (@scope/pkg), the package 404ing isn't the interesting fact — the whole scope must be unclaimed for the name to be squattable. Check the scope's org and user profile pages, not the public search API:

GET https://www.npmjs.com/org/{scope}    -> 200 = scope owned as an org
GET https://www.npmjs.com/~{scope}       -> 200 = scope owned as a user

The name is registerable only if both 404. The npm search API is deliberately not used for this: it returns total:0 for a scope that is privately owned but hosts no public packages, which would falsely read as "unclaimed." The org/user profile pages return a real 404 only when the scope genuinely doesn't exist, regardless of what it might privately host.

8.5 Severity / confidence

Signal strength Severity Confidence
strong (private-registry binding) HIGH FIRM
medium (org-namespace match only) MEDIUM TENTATIVE — namespace matching alone is namesake-prone

Never CONFIRMED — this is static manifest analysis over a captured corpus, not a live resolver-fallback observation. There is no "the build actually pulled the malicious package" event to point at.

8.6 Recipes

bash:

# unscoped npm
curl -sk -m 15 -o /dev/null -w '%{http_code}\n' "https://registry.npmjs.org/acme-internal-utils"
# scoped npm -- package + both scope-ownership pages
curl -sk -m 15 -o /dev/null -w '%{http_code}\n' "https://registry.npmjs.org/@acme%2Fdesign-system"
curl -sk -m 15 -o /dev/null -w '%{http_code}\n' "https://www.npmjs.com/org/acme"
curl -sk -m 15 -o /dev/null -w '%{http_code}\n' "https://www.npmjs.com/~acme"
# PyPI
curl -sk -m 15 -o /dev/null -w '%{http_code}\n' "https://pypi.org/pypi/acme-internal-tools/json"

PowerShell:

function Test-RegistryStatus {
  param([string]$Url)
  try {
    $r = Invoke-WebRequest -Uri $Url -Method Get -TimeoutSec 15 -SkipHttpErrorCheck   # PS7+; on PS 5.1 read $_.Exception.Response in the catch
    return $r.StatusCode
  } catch { return "error" }
}
Test-RegistryStatus "https://registry.npmjs.org/acme-internal-utils"
Test-RegistryStatus "https://www.npmjs.com/org/acme"
Test-RegistryStatus "https://www.npmjs.com/~acme"
Test-RegistryStatus "https://pypi.org/pypi/acme-internal-tools/json"

Bound the check volume — filter to internal-signalled candidates before hitting the registry (hundreds, not thousands, per engagement) and cache scope-ownership lookups (one org/user check per distinct scope, never per package under that scope).

8.7 Out of scope

Reserving the name — publishing a stub the org controls — is a remediation the client performs, not a step this skill takes. This skill's ceiling is: "this name is internal-signalled and currently unclaimed." Confirming that a live resolver fallback actually pulls a planted package, or registering anything under the discovered name, is active supply-chain interference and out of scope regardless of authorization — hand it to the client as the remediation action (§8.5 evidence + §10 business translation).


9. Cloud-Native & Container/K8s/CI Control-Plane Fingerprinting — an Org-Attribution Surface

Modern infrastructure increasingly is the org-attribution signal: a Lambda Function URL, a *.run.app Cloud Run service, or an exposed kubelet is both an exposure and a fingerprint of which cloud account/cluster belongs to the target. Everything in this section is passive — pattern-matching over hostnames and ports already resolved by earlier recon, zero new network calls of this skill's own. offensive-osint §16.17–§16.19 carries the fuller URL-pattern and active-probe tables (20 cloud-native providers, the full container-registry search list, 11 CI/CD platforms with active curl recipes); this section is the reasoning layer that decides what a hit is worth and whether it's the target's.

9.1 Cloud-native endpoint fingerprint

Match already-resolved hostnames — subdomain CNAME targets, certificate SANs, subdomain FQDNs themselves, and webapp URLs — against provider URL patterns:

Provider Pattern Service
AWS *.execute-api.<region>.amazonaws.com API Gateway
AWS *.lambda-url.<region>.on.aws Lambda Function URL
AWS *.<region>.elasticbeanstalk.com Elastic Beanstalk
Azure *.azurewebsites.net App Service
Azure *.azurecontainerapps.io Container Apps
Azure *.file|queue|table.core.windows.net Storage (Blob object storage is handled by bucket discovery, §6 — not duplicated here)
GCP *.run.app Cloud Run
GCP *.appspot.com App Engine
GCP *.cloudfunctions.net Cloud Function

(offensive-osint §16.17 extends this to 20 providers — App Runner, CloudFront, ALB/ELB, Amplify, Static Web Apps, Vercel, Netlify, Cloudflare Workers/Pages, Heroku, Render, Fly.io, Railway, DigitalOcean App Platform.)

Ownership gating is structural, not scored — the same discipline as §6.3's bucket model, applied to endpoints:

Provenance of the match Ownership
A subdomain CNAME points at the pattern Owned
A certificate SAN names the pattern Owned
The subdomain's own FQDN matches the pattern (it is the cloud-native host) Owned
A webapp URL matches the pattern with no independent tie to the target from the above Unverified

Owned → MEDIUM/FIRM. Unverified (a bare pattern match with no corroborating tie) → LOW/TENTATIVE. Confirming whether the endpoint is actually invocable without authentication is the stage-6 --validate --validate-cloud step — out of scope; this section only says "this cloud-native surface exists and appears to belong to the target," never "it's exploitable."

9.2 Container / Kubernetes control-plane exposure — passive flag, not active probe

Rather than issuing new probes, treat orchestration control-plane ports already discovered by port/service enumeration as a likely-exposed control plane. All five carry HIGH severity, FIRM confidence once flagged — the port being open is directly observed; the auth posture behind it is not, which is exactly why confirmation is gated separately:

Port Service Why it matters if unauthenticated
2375 Docker API (plain HTTP, no TLS) Anyone who reaches it controls the daemon — launch a host-mounting container, escape to root on the host.
2376 Docker API (TLS) Equivalent to 2375 unless client-cert auth is strictly enforced.
2379 etcd client API Holds the entire cluster state — every Secret, in plaintext. Read = full credential harvest; write = cluster tampering.
6443 Kubernetes API server The cluster's control plane — a valid token, leaked kubeconfig, or anonymous-auth misconfig creates privileged pods and reads every Secret.
10250 kubelet No auth required = pod exec on the node.

(offensive-osint §16.18 has the fuller table — kube-proxy/controller-manager/scheduler health/metrics endpoints, cAdvisor, Helm Tiller — plus the active curl recipe for each and the public-container-registry search list across Docker Hub/Quay/GHCR/ECR Public.)

The port being open says nothing about its auth posture. Flag it; do not connect to it to find out. A live GET/HEAD against the endpoint to confirm whether it answers unauthenticated is exactly the line this skill stops at.

9.3 CI/CD platform surface

Fingerprint from tech strings already collected by passive web/HTTP enumeration — Jenkins, GitLab (self-hosted), Argo CD, Harbor, TeamCity, Drone. MEDIUM/TENTATIVE — a tech-string match is a weaker signal than a resolved hostname pattern, hence TENTATIVE rather than FIRM. A CI/CD console or API, when reachable unauthenticated, exposes build pipelines, stored credentials, and artifact registries — a supply-chain foothold. offensive-osint §16.19 has the per-platform active probe paths (/script, /api/v4/version, /gate/info, …) and the GitHub Actions secret-leak anti-pattern catalog (workflows that echo ${{ secrets.* }} to logs, or check out fork-PR code under pull_request_target) for an operator who chooses to go further under their own authority.

9.4 What's out of scope here

Any GET/HEAD issued specifically to confirm a fingerprinted cloud-native endpoint or control-plane port is reachable/unauthenticated is a stage-6 --validate --validate-cloud active tier — hard-gated behind explicit --validate plus per-target scope confirmation, default OFF. This skill's ceiling is the passive fingerprint and the "likely exposed, auth posture unconfirmed" flag; describe the confirmation step, never perform it.


10. Severity & Business Translation

Technical finding Business language Severity
Publicly listable bucket, ownership-verified, credentials/db-dump objects present Customer/internal data — including live credentials — is downloadable by anyone on the internet right now. CRITICAL
Same listable bucket, but the name can't be tied to the client Someone's bucket is open; not provably this client's exposure. Logged for the graph, not reported as their risk. No finding (§6.3)
AWS account ID recovered from a dead/rotated key, owned host The org's cloud account number is now public. Not itself a breach, but every cross-account trust policy referencing that number needs review. MEDIUM
Dependency-confusion vector, strong signal (private-registry binding), unclaimed on npm An attacker can publish a package under this exact internal name today; the next CI run with a misconfigured registry fallback pulls it and runs arbitrary code in the build. HIGH
Cloud-native endpoint (Lambda URL / Cloud Run), owned, auth posture unconfirmed A managed-service endpoint is reachable; whether it's invocable without authentication is not yet proven — an open question requiring confirmation, not an assumed breach. MEDIUM
Kubernetes API / etcd / kubelet port open, passively flagged The cluster's control plane appears internet-reachable; authentication enforcement is unconfirmed. If it isn't enforced, this is a path to full cluster compromise. HIGH (unconfirmed auth)

11. Skill Self-Test

Drop these into a fresh session to verify the skill loads and routes correctly.

  1. "Generate S3/GCS/Azure bucket candidates for acme.com with subdomains api/billing/hr and probe them." → §6.1–§6.2, §6.5.
  2. "We found a listable S3 bucket named marketplace-media from a permutation guess — can't tie it to the client. Is that CRITICAL?"negative. §6.3 — no; an unverified public/listable hit produces no finding at all (not even INFO); it's recorded as an asset only.
  3. "Same bucket, but its object listing includes acme-employee-payroll.csv." → §6.3 branded-object-key ownership signal — now verified, promote to CRITICAL.
  4. "What's exposed inside a listable bucket, without downloading any object?" → §6.3 object-key triage table (filename-pattern classification of the listing body only).
  5. "A subdomain has a live CNAME to a bucket that returns 404 NoSuchBucket. What's the finding?" → §6.4 — dangling-CNAME takeover, HIGH, CONFIRMED, independent of the ownership-gating table.
  6. "Decode the AWS account ID from ASIAY34FZKBOKMUTVV7A." → §7.3–§7.5, test vector 609629065308.
  7. "We found 123456789012 as an accountId in a Terraform snippet on the target's site. Real disclosure?" → §7.6 — no; that's a documented AWS example account ID, screen it out.
  8. "Should I use the decoded account ID to enumerate IAM roles via sts:AssumeRole?"negative. §7.1/§5 — out of scope; needs the operator's own AWS credentials against the target account and is CloudTrail-logged; describe the pivot value, don't perform it.
  9. "package.json references @acme/design-system, and .npmrc binds @acme to a private registry. Is that a finding by itself?" → §8.2–§8.3 — internal signal only; still needs the public-registry 404 (and scope-claimability, §8.4) check before it's a finding.
  10. "npm's public search API returns total:0 for @acme. Is the scope claimable?" → §8.4 — no; the search API is blind to privately-owned scopes. Use the org/user profile-page 404 check instead.
  11. "Confirmed unclaimed dependency-confusion vector. Should we reserve the package name to protect the client?"negative. §8.7 — out of scope; that's a client remediation action, not this skill's step.
  12. "A subdomain CNAMEs to *.run.app. Is that Cloud Run endpoint owned or unverified?" → §9.1 — CNAME provenance = owned.
  13. "Port 6443 is open per a prior port scan. What do I do next?" → §9.2/§9.4 — flag as a likely-exposed Kubernetes API control plane (passive, HIGH/FIRM); do not curl /api to confirm auth posture — that's stage-6 --validate --validate-cloud.
  14. "Write the business-translation line for a HIGH-signal dependency-confusion finding." → §10.

12. Changelog

  • v1.0 (2026-08-06) — initial release. Reproduces exact logic from the reference implementation: modules/cloud_buckets.py (two-class candidate generation, HEAD/GET→listing probe technique, the ownership-gated severity model, 9-tier object-key triage, dangling-CNAME takeover), core/aws_account.py + modules/aws_account_enum.py (offline AKIA/ASIA/AROA→account-ID base32 decode, canonical test vector, AWS documentation-example-ID screening, ownership/severity/confidence mapping), core/dep_confusion.py + modules/dependency_confusion.py (manifest mining across 7 file types, the internal-signal classifier, the two-part confirmation contract, the npm scope-claimability nuance), and modules/cloud_exposure.py (cloud-native endpoint fingerprint with structural ownership gating, passive container/K8s/CI exposure flags). Deepens offensive-osint §16.8/§16.17–16.19/§44 rather than duplicating their wordlists.

Version History

  • 66ffd91 Current 2026-08-16 07:06

Same Skill Collection

skills/continuous-exposure-monitoring/SKILL.md
skills/email-domain-security/SKILL.md
skills/exposure-risk-quantification/SKILL.md
skills/identity-provider-recon/SKILL.md
skills/org-attack-surface/SKILL.md
skills/osint-autopilot/SKILL.md
skills/osint-methodology/SKILL.md

Metadata

Files
0
Version
66ffd91
Hash
59777a2b
Indexed
2026-08-16 07:06

inicio - Wiki
Copyright © 2011-2026 iteam. Current version is 2.155.2. UTC+08:00, 2026-08-17 20:03
浙ICP备14020137号-1 $mapa de visitantes$