InnoCTF
recon Bug-class explainer

Dormant GitHub accounts: how ghost profiles quietly map corporate orgs

Abstract art of years-old dormant GitHub accounts waking to scrape a corporate organization graph
A years-old ghost account activates and enumerates an org through the API. Illustration.

Datadog Security Labs is tracking several overlapping campaigns that walk corporate GitHub organizations one API call at a time. The tooling is unremarkable. What makes it hard to spot is the identity behind the traffic: accounts created years ago, left idle, then switched on to enumerate members, repositories, and contribution graphs. This is a reconnaissance bug class more than a memory-corruption one, and the fix is detection, not a patch.

What the campaign actually does

The activity is enumeration, not exploitation. Operators drive automated scraping tooling against the GitHub API to build a map of a target organization: its public repositories, its members, who those members follow, and which projects each account touches. Individually every request is legitimate. Read together they reconstruct a company's internal structure, its team boundaries, and the developers most worth phishing later. In a smaller number of cases the same tooling escalated from public-data collection to cloning private repositories using credentials it had already harvested.

Because a large part of GitHub's API surface answers without authentication, a lot of this map can be assembled with no login at all. The unauthenticated tier is rate-limited but generous, and the data it returns is exactly what a threat actor needs for the first phase of an intrusion: names, project affinities, and the shape of the org chart.

Why dormant accounts are the trick

The reconnaissance itself is old news. The interesting part is how the operators stay under the noise floor. Datadog describes the widest band of traffic coming from networks of ghost accounts: profiles registered two to five years ago, left dormant, then activated to spray API queries across many organizations.

An account with a multi-year history reads as more legitimate than one registered the same week it starts scraping. the core of the evasion

Age is the disguise. Most naive abuse heuristics weight account age heavily: a week-old account hitting a hundred orgs looks like a bot, a four-year-old account doing the same looks like a long-time user. The campaign weaponizes that assumption. Alongside the ghost accounts, the operators fold in dozens of legitimate accounts whose personal access tokens (PATs) or OAuth tokens were exposed or stolen, so some of the traffic comes from genuinely trusted identities. Rotating across more than fifty dormant profiles plus the compromised set also spreads the load under per-account rate limits, so no single identity trips a throttling alarm.

The bug class: identity is not intent

Frame this the way you would frame any detection problem. The underlying weakness is that account reputation is being used as a proxy for good intent, and the two are not the same thing. A dormant account is not a trustworthy account; it is an account with no recent behavior to judge. When your baseline is "old and quiet equals safe," an adversary who can buy or age quiet accounts inherits your trust for free.

This is the same shape as credential-stuffing that rides real accounts, or residential-proxy traffic that borrows real home IPs to look human. The defensive lesson transfers: reputation signals decay, and any signal an attacker can cheaply manufacture stops being a signal. What does not decay is behavior. A four-year-old account that has never touched your org and suddenly enumerates every member in an afternoon is anomalous no matter how old it is.

Detecting it on your own org

If you run a GitHub organization, the enterprise and organization audit logs are the primary evidence source. You are not looking for a known-bad indicator; you are looking for access patterns that do not match how humans use the API. Practitioner starting points:

audit-triage.sh
# pull org audit events and rank actors by breadth of read access
gh api "/orgs/$ORG/audit-log?phrase=action:git.clone&per_page=100" \
  --paginate | jq -r '.[] | [.actor, .repo, .created_at] | @tsv' \
  | sort | uniq -c | sort -rn | head -20
# actors touching many distinct repos in a narrow window rise to the top

Reducing the attack surface

You cannot stop the internet from reading your public data, and you should not try. What you can do is shrink the private blast radius and make the leaked-token path narrower.

The broader ecosystem is moving the same direction. GitHub's release of npm 12 disables install scripts by default and deprecates the granular access tokens that used to sidestep two-factor prompts, both aimed at cutting the number of long-lived, over-scoped credentials floating around developer machines. Fewer standing tokens means fewer that can be quietly folded into a scraping fleet.

Reproducing the shape in a lab

You do not need anyone's real org to understand this. Create a throwaway organization, add a couple of test accounts, and script read-only enumeration against your own targets with a token you control. Watch how the audit log records it, then write a query that separates your enumeration run from normal activity. The goal is not the scraper; it is building the detection that would have caught it. An adversary's whole strategy here is to look normal, so your whole job is to define, precisely and in your own environment, what normal is not.

Disclosure: This article was researched and drafted with AI assistance and edited by the InnoCTF Editorial Team. It summarizes public reporting for education and authorized testing only, and describes reconnaissance and detection against systems you own or are permitted to test. It does not target any live organization.

Sources

  1. The Hacker News. "Dormant GitHub Accounts Help Attackers Blend In While Mapping Corporate Orgs." Read the report
  2. Datadog Security Labs. "Coordinated GitHub API enumeration and access token abuse." Read the analysis
  3. The Hacker News. "npm 12 Disables Install Scripts by Default to Reduce Supply Chain Risk." Read the report
  4. GitHub Docs. "Managing dormant users." Read the documentation