3l0h1m.s3c:     file format elf64-x86-64

 Disassembly of section .text:

 0000000000401000 <_start>:
   401000:   48 89 e5          mov    rbp, rsp
   401003:   bf 01 00 00 00    mov    edi, 0x1        ; web
   401008:   be 02 00 00 00    mov    esi, 0x2        ; mobile
   40100d:   ba 03 00 00 00    mov    edx, 0x3        ; active directory
   401012:   b9 04 00 00 00    mov    ecx, 0x4        ; binary exploitation
   401017:   41 b8 05 00 00 00 mov    r8d, 0x5        ; exploit dev
   40101d:   41 b9 06 00 00 00 mov    r9d, 0x6        ; low level
   401023:   e8 00 00 00 00    call   <blog_main>

Incident Report: a Second PolinRider Attempt — Caught by the Attacker's Own Bug

2026-08-22 | author: elohim | tags: incident-response, supply-chain, malware, lazarus-group, dprk, git, github, vercel, threat-intel

Follow-up to [Incident Report: PolinRider — a Blockchain Dead-Drop Supply-Chain Compromise](/posts/polinrider-blockchain-dead-drop-supply-chain-compromise/), same client, same underlying compromised developer account. As with that post, all client identity, employee names, GitHub handles, and repository names are **redacted or replaced with generic placeholders**. If you haven't read the first post, read the "Background" section there first — it explains what PolinRider is and how the force-push impersonation trick works; this post assumes that context.

|=—[ TL;DR ]

Four months after the first incident, the same compromised developer account force-pushed the same PolinRider malware family into a different client repository — three malicious commits across three branches, in a single burst. This account still had write access to that specific repo because it was missed during the client’s access revocation after the first incident — a repo my engagement never covered and I had no visibility into. This time, the attacker’s own payload had a JavaScript syntax error in it, so every single build attempt failed before any malicious code could run. Production was never touched. Cleanup took under a day.

Date:        August 21–22, 2026
Attacker:    same compromised account as IR-2026-001
Vector:      write access to an out-of-scope repo the client's own
             revocation pass had missed; workstation status unknown
             (my engagement ended after IR-2026-001)
Impact:      0 of 3 malicious builds executed (syntax error); production
             clean throughout
Status:      Closed

|=—[ What happened ]

At 06:10 UTC, three commits were force-pushed in quick succession from the same compromised developer account identified in the earlier incident — still holding collaborator access to this separate, out-of-scope project — across three different branches:

branch A (the developer's own branch): 686c5dc -> 8ba6a1f
branch main:                           97a6fdd -> d81663a
branch B (an unrelated feature branch): 6cdedbc -> 1c37e91

Every one of the three commits injected the same obfuscated payload into a build config file (postcss.config.mjs this time, instead of the Tailwind config file from the first incident — same campaign, different target file, see the IOC list in the first post).

The deploy platform (Vercel) attempted to build all three:

No malicious code executed anywhere. The repo owner noticed the failed checks the same day, reviewed the diff, and removed the payload.

|=—[ The bug that saved them ]

This is worth looking at directly, because it’s a good example of how fragile obfuscated malware can be — one bad find-and-replace during whatever templating step produced this payload build, and the whole delivery mechanism DOAs.

The injected code loads several Node.js built-ins through a createRequire call assigned to an identifier — and then, inside the payload it appended, declares an identifier with that exact same name again in the same scope:

// (illustrative reconstruction of the shape of the bug — the actual
// payload is minified/obfuscated, this is the equivalent unobfuscated form)

const { createRequire } = await import("node:module");
const require = createRequire(import.meta.url);   // first declaration

// ... 380-ish characters of whitespace padding ...

const require = ... ;   // <- SyntaxError: Identifier 'require' has already
                         //    been declared in this scope

Node’s build tooling reported it plainly:

the name 'require' is defined multiple times

export default config; — the file’s real, legitimate content — sat right below the injected block, completely untouched. The attacker’s tooling appended the payload after the legitimate export rather than merging it in, and in doing so declared the same binding twice. Because JavaScript treats a duplicate const/let in the same scope as a parse-time error, not a runtime one, the file never got far enough to execute anything — not the legitimate config, not the malicious payload.

|=—[ Why this matters despite “nothing happened” ]

It’s tempting to read “the payload was broken, so it’s a non-event.” Three reasons that’s the wrong takeaway:

  1. The attacker will fix the bug. This is the same automated campaign from the first incident, redeployed against a second project. The next attempt — or the next victim — may not get a free pass from a typo.
  2. One access-revocation entry got missed, and that was enough. The first incident’s remediation plan explicitly called for revoking the compromised account’s access across every repository and rotating all its credentials — that was the client’s action item to close out, since my engagement ended once that plan was handed off. Four months later this one repo, which was never part of my original scope, still had the account on its collaborator list. A syntax error is not a control; it’s luck. The actual control — full access revocation — existed on paper and simply wasn’t applied everywhere.
  3. Attribution forgery was present again, same trick as the first incident: one of the three malicious commits was crafted to look like a normal merge of an already-open, legitimate pull request — same parent commits, same author name and date as the merge commit it replaced — but with a committer identity and timestamp offset that didn’t match anything else in the repository’s history. Diffing the forged commit against both of its parents showed both parents were clean; a three-way merge of two clean sides cannot legitimately produce a modified result, which is what exposed it as hand-crafted rather than an actual merge.

|=—[ Remediation ]

All of the following was completed the same day the failed builds were noticed:

  1. Reviewed the git history of the targeted file across every branch to identify all injected commits and confirm the state of their parents.

  2. Confirmed the authentic pre-attack commit was still reachable from an unaffected branch.

  3. Reset the local main branch back to that clean commit.

  4. Force-pushed the corrected branch with git push --force-with-lease, replacing the forged commit on the remote.

    `--force-with-lease` instead of a plain `--force`: it refuses to push if the remote branch has moved since you last fetched it, which prevents *your own* cleanup force-push from accidentally clobbering someone else's legitimate work landed in the meantime. Good habit for any force push, not just incident cleanup.

  5. Deleted both malicious branches from the remote entirely.

  6. Removed the compromised account from the repository’s collaborator list.

  7. Verified the restored file byte-for-byte against the known-clean version and confirmed the commit’s committer field matched GitHub’s own merge-bot identity again (the tell for an authentic, non-forged merge commit).

  8. Confirmed via git ls-remote that neither malicious branch remained.

  9. Pushed one content-neutral commit afterward to confirm the deploy pipeline still worked cleanly end to end.

|=—[ Takeaways ]

|=—[ References ]

See the first incident write-up for background on PolinRider, the malware’s obfuscation layers, its blockchain dead-drop C2 mechanism, and full attribution sources.

- - - EOF - - -