Skip to content

Forensic Investigation — Methodology & Analysis — Incident 2026-05-31

Forensic Investigation — Methodology & Analysis

Section titled “Forensic Investigation — Methodology & Analysis”

The full investigation behind the 31 May 2026 force-push incident: each line of inquiry, why it was worth pursuing, how it was run, and what it returned. Read-only forensics across 87 repositories, two cloud environments, and two developer endpoints.

← Incident summary & timeline

Every thread below was run under the same five rules. They are the reason the conclusions hold up.

Commit author names and dates are attacker-writable. The unforgeable record is the push/activity event (actor, before, after, timestamp). Every attribution rests on that, never on commit metadata.

Code analysis ran against full local bare clones of all 87 repos; the GitHub API was used only for data that exists nowhere else (activity, settings, check-runs). Nothing was mutated during analysis.

A stolen credential is a shared commodity — several unrelated actors routinely reuse the same access. Detection was never scoped to one malware family; we tried to find a second and disprove a single-payload assumption.

The payload was decoded statically, then executed only under --network none --read-only --cap-drop ALL with every dangerous primitive hooked. The C2 was never contacted from a live host.

Every load-bearing number was re-derived from primary evidence; the prose was independently fact-checked. Open questions are stated as open, with the exact datum that would close them.

Twelve threads. Each is framed as the question that opened it, the reasoning for spending effort on it, the method, and the result.

Thread 1 Which credential pushed the payload?

Section titled “Thread 1 Which credential pushed the payload?”

Scope, blame, and remediation all hinge on this, and the commit metadata pointed at ~20 different people — none of which could be trusted.

Matched every payload-bearing commit to the push that introduced it via the activity log (actor / before / after / timestamp), across all 87 repos.

A single credential — numberyy — landed every payload, in one 13:52:45–13:59:44Z burst. dependabot[bot] force-pushes were collateral (its tips reset onto poisoned bases). zabir-n1 and syamil-n1 pushed no payload.

Thread 2 Is any of this genuinely old malware?

Section titled “Thread 2 Is any of this genuinely old malware?”

Author-dates ran Nov 2025–Jun 2026; an early reading proposed a months-old “patient-zero” and a long-standing internal pattern. If true, scope and attribution change completely — and real people’s names sat on those commits.

Checked first-seen for every loader SHA in the full activity history; verified GPG signatures on the impersonated identities’ real commits.

Every loader commit first appears as the after of a 05-31 numberyy force-push — zero appearances before. All back-dating is forged. 1,379 of the most-impersonated identity’s commits are GPG-verified genuine; 61 are forgeries. No old malware exists.

Thread 3 What does the payload actually do?

Section titled “Thread 3 What does the payload actually do?”

Two distinct families were planted (an obfuscated blob and an atob→fetch→eval loader). We needed the real capability and whether it harvests credentials — and a marker-only scan had already missed the second family.

Static deobfuscation, then isolated-sandbox detonation with hooked primitives. The loader’s staged on-chain payloads were recovered by reading public blockchain records read-only.

An EtherHiding multi-stage chain: BeaverTail-class downloader → on-chain loaders → an InvisibleFerret-class socket-C2 RAT (host fingerprint, fs+child_process, port 1224). Both families run this same chain — Family A (PolinRider blob) inlines the loader; Family B (eval-loader) fetches it from the C2. The toolchain is publicly attributed to the DPRK-aligned Contagious Interview campaign (external attribution, not independently confirmed). Stage 3 self-aborted with no C2 reachable; nothing was exfiltrated in the sandbox. The on-chain dead-drops (IOC table below) are publicly inspectable on BscScan / Tronscan.

Thread 4 Was the C2 built for us, or are we one of many?

Section titled “Thread 4 Was the C2 built for us, or are we one of many?”

A bespoke endpoint implies targeting and a live operator focused on N1; a shared one implies an automated sweep. The response posture differs.

Traced the C2 string through our full git history and web-archive; cross-referenced the identical base64 C2 against unrelated public repos pushed the same day.

No history of the endpoint in our environment before the plant; the same C2 string appears in unrelated repos force-pushed the same day, with a sibling ipcheck-six.vercel.app. This was an automated, multi-victim sweep — not infrastructure built for N1.

The burst was a replay — the credential was used remotely, not minted. Without the theft mechanism we can’t say what else the actor holds or whether the vector is still open.

Ranked candidate vectors against verified negatives (no new PAT, no new OAuth grant, no auth event in the burst window, IP/location-blind exports), then weighed the victim-reported lure and the platform’s own malware notice.

Most-consistent hypothesis (not proven): an off-platform fake-recruiter lure (ML / “crypto trading” role) ran a browser/credential infostealer that lifted a pre-existing credential — invisible material (a session cookie / OAuth token) the victim had no signal to revoke — later replayed on 31 May. The screenshots prove solicitation and a platform malware warning, not that any file was run. N1 can’t “invalidate” a stolen session cookie/OAuth token — but required signed commits now mean it can no longer land a commit in N1’s repos, which is the gap that mattered. The theft itself happened on a personal device, off N1’s surface — outside N1’s reach by design. Exact credential and theft event remain unconfirmed; the closing datum is the account’s IP-stamped personal security log.

Thread 6 Is there a second campaign hiding behind the same credential?

Section titled “Thread 6 Is there a second campaign hiding behind the same credential?”

Stolen creds are resold and reused; one prior compromise elsewhere drew 4+ competing malware families. Scoping detection to one family would miss a second implant.

Broad-spectrum signature sweep across every branch tip (reverse shells, miners, env/token exfil, generic obfuscation/eval, suspicious network calls), plus a review of every in-window force-push by every actor and a GitHub-persistence check.

No second campaign and no third payload family. Aside from numberyy, every in-window force-push was ordinary developer rebasing. A repo-level persistence sweep across all 87 repos (webhooks, deploy keys, Actions secrets) found no object planted in the window: no webhook created/modified, no deploy key created (the one write-capable key predates the incident by three months), and zero secrets created — the only in-window secret updates were the team’s own 06-03 credential rotation. Org-level webhooks: zero. No persistence object exists across the swept surface (the 87 active repos + org level; the ~10 non-code org repos were not separately swept).

Thread 7 Did a malicious dependency get us — the campaign’s usual entry?

Section titled “Thread 7 Did a malicious dependency get us — the campaign’s usual entry?”

This actor’s documented entry path is a poisoned npm package. If one was in our tree, the blast radius is far larger than a config-file plant.

3,206 npm packages at HEAD + 2,258 historical names checked against the campaign’s known-bad lists; every lockfile resolution confirmed registry-sourced; Python/Go across 58 repos against OSV’s live malicious-package set; all 86 internal R2 wheels content- inspected against their manifests and pinned hashes.

Clean. No campaign package present, no off-registry resolution, no tampered internal wheel. litellm specifically is not pinned to a compromised version. (Coverage rests on OSV’s live set — the bundled scanners’ DBs were stale.)

Thread 8 Did it execute or harvest anything in CI?

Section titled “Thread 8 Did it execute or harvest anything in CI?”

The whole point of the payload is credential theft at lint/build/test time. “Present in a repo” and “ran” are different blast radii.

Per-repo workflow run analysis: head SHAs of every post-plant run, npm ci outcomes, and which build steps load the poisoned config.

No execution, no observed harvest. Runs were disabled, died at npm ci (the attacker’s own loader deps were absent from our lockfiles), or ran a build that never loads the poisoned eslint config. Breadth came from the stolen personal credential, not a harvested CI token.

Thread 9 Did it reach production, staging, or the edge?

Section titled “Thread 9 Did it reach production, staging, or the edge?”

Source-repo poison is contained; a tainted running workload or deployed bundle is not. This is the question that maps to customer/patient impact.

Read-only scans of both EKS clusters (workloads, images, env, cronjobs) and a full Cloudflare account download (Workers, Pages, served bundles, DNS).

Clean. No C2 host, no AUTH_API_KEY, no markers, no rogue workloads; all images from trusted registries; no DNS record points at the C2. (Honest limit: a manifest scan can’t read image-layer contents or secret values.)

Thread 10 Where is the implant on the developers’ machines?

Section titled “Thread 10 Where is the implant on the developers’ machines?”

The credential was stolen somewhere; the endpoints are the only place that holds the dropper and the vector. Both engineers use Claude Code heavily, so their session archives are a rich detection surface.

Examined the full Claude Code archive (2,260 sessions) and machine config for the numberyy holder; the endpoint-IR bundle and session transcript for the other.

Both are victims. No payload ever transits the archives; one engineer was the first responder who caught the poisoned branch on a rebase and isolated it. The actual theft executed off the Claude surface (a plain terminal / IDE / personal context), which these archives can’t see — that trail is cold.

Thread 11 Can we recover the attacker’s source IP from GitHub?

Section titled “Thread 11 Can we recover the attacker’s source IP from GitHub?”

An IP or a “new device” session would pin the replay and the origin geography.

Confirmed the org plan tier and pulled the available personal token/OAuth exports.

Not recoverable from GitHub: the Team plan records no per-push source IP and exposes no audit-log API; the exports are IP- and location-blind. The only remaining lead is the account’s owner-only security log (sign-in/session events).

Thread 12 Why did the blast radius stay small?

Section titled “Thread 12 Why did the blast radius stay small?”

Understanding the actual containment mechanism tells us which control to invest in, versus which were incidental.

Correlated which branches were poisoned against each repo’s protection state and the org-wide default permission.

Branch protection was the primary determinant: protected defaults rejected the force-push at the mainline, confining payload to dormant feature branches; only three repos with unprotected defaults were hit directly. The amplifier was the org default permission being write.

Closing the root cause and the structural gaps the investigation surfaced. Current org ruleset posture:

Control State
Verified signatures required — every commit, every branch, every repo active, no bypass
main + develop protected (no force-push/deletion, PR required) — all repos, current and future active, owner-only emergency bypass
Default branch (develop) — no force-push/deletion active, no bypass
Org default permission read
Org-wide 2FA requirement enforced
All poisoned default branches cleaned (signed commits); attacker account removed
Poisoned feature branches all deleted
Forward work — a focused ~2-week, open-source program owned by Arun and Eyad, in two
tracks. Prevention: one blocking security-gate CI check on every repo
(current and future) — a shape-based malware/obfuscation scanner (the upgrade that would have caught the
loader) chained with dependency-CVE, secret, trusted-registry, Actions, and test/coverage checks; mirrored
by pre-commit hooks; plus commit-signing onboarding and the discrete audit-finding fixes (public wheel
registry, CF TLS/token TTLs, app least-privilege, bastion pinning). Detection: a GitHub
org-webhook → Slack alert on force-push burst / new collaborator / new app / ruleset change, a nightly
snapshot-diff to fill the audit-log gap, an AWS honey-key tripwire for stolen-credential use, and a
read-only metadata Slack bot. Tracked separately (own budget): SignOz PHI-redaction and endpoint EDR.
  • The exact stolen credential and the attacker’s IP are not recoverable from GitHub Team telemetry.
  • The lure is the most-consistent entry hypothesis, not a proven fact — no malicious file or its hash was recovered.
  • A repo scan cannot see a local-only “take-home” repository; the dropper is confirmable only from the endpoint.
  • Stage-3 exfil targets arrive over a live operator socket that was deliberately never contacted, so they are not observable.
  • Manifest/registry scans cannot read container-image layer contents or secret values.
  • The nation-state attribution rests on indicator overlap with public reporting, not independent confirmation.
Type Value
C2 auth-confirm-six.vercel.app/api; sibling ipcheck-six.vercel.app; <word>-six.vercel.app
PolinRider markers global['!'] ('9-0315-1'), _$_1e42, rmcej%otb%; createRequire + a single >2000-char line in *.config.*
Loader atob(process.env.AUTH_API_KEY)node-fetcheval; committed .env; log string Auth Error!
On-chain BSC 0x80a1148e…, BSC 0x54b8bde1…, TRON TA48dct6…; stage-3 socket port 1224
Lure Upwork “Princes A.” / “Princes Ampalayo”; ML + crypto-trading freelance pretext
Delivery force-push burst from one account with back-dated author dates + forged identities — trust the push event, not commit metadata

Marker-scanning alone missed the loader family entirely. Pair marker strings with behaviour-based rules: dynamic decode-and-execute, oversized single-line config blobs, and build processes making outbound network calls.

Confidential — Internal Only

N1 Healthcare · Forensic analysis of the incident of 31 May 2026 · Report dated 4 June 2026 · Contains no live malware.