North Korean Hackers Compromise Go and PHP Packages

6mile1 pts1 comments

PolinRider Jumps the Fence to Go, Packagist, npm, PyPI | OpenSourceMalware

PolinRider is a DPRK-linked supply-chain campaign, part of the Lazarus Group / Contagious Interview activity cluster, that I first identified and named earlier this year. Rather than Contagious Interview's classic playbook of conducting fake interviews accompanied by typosquatting or publishing throwaway malicious packages, PolinRider takes over legitimate GitHub accounts and quietly injects an obfuscated JavaScript loader into repositories the maintainer already owns.<br>When I first documented PolinRider earlier this spring, it was 100% a GitHub problem. A North Korean actor was quietly appending obfuscated JavaScript to config files inside legitimate repositories. The payload hides in config files, fake .woff2 font files, and .vscode/tasks.json triggers, then uses blockchain dead-drops to download the Lazarus stealer toolkit. A Windows script rewrites git history to hide the tampering, making it look like a normal commit. Our first report caught 675 compromised repositories. Our follow-up a few weeks later put the count at 1,951 repos across 1,047 owners.<br>Then a few weeks ago, the OpenSourceMalware and Socket teams observed PolinRider publishing payloads to multiple package registries including Packagist and Go. Almost 200 malicious release artifacts tied to 111 unique packages and extensions: more than 80 Go modules, 10 Packagist packages, a cluster of NPM libraries, and a couple of Chrome extensions thrown in for good measure. The campaign didn’t have to get more sophisticated to make this jump. It got the jump for free, because of how these ecosystems are built.<br>Why ecosystem jumping was inevitable<br>There are two ways a package manager can turn a name like github.com/user/thing into code on your disk.<br>Model A — the registry hosts its own artifact<br>NPM and PyPI host their own artifacts. To publish, a maintainer builds a specific artifact file, typically a tarball or a wheel, and uploads it to the registry’s storage using a publishing token. The registry keeps its own copy. When you install, you pull that copy from the registry — not from GitHub. GitHub is, at most, where the source happens to live. The registry credential is a completely separate key from the GitHub credential.<br>Model B — the registry resolves from the Git repo<br>Go modules and Packagist resolve from the Git repo: There is no separate “upload” step and, in Go’s case, no artifact-hosting registry at all.<br>Go: A module’s identity is its repository path, and a version is a git tag. go get github.com/user/thing@v1.4.0 asks the module proxy (proxy.golang.org) for that tag, and the proxy fetches whatever is in that git commit. There is no publish token. If you can push a tag to the repo, you have published a Go module version.

Packagist: The registry stores metadata that points at your GitHub repo. A GitHub webhook tells Packagist to re-read the repo whenever you push, and composer require downloads the code as a zipball of the tagged commit straight from GitHub. Again: no separate artifact, no separate upload credential. The repo is the source of truth.

For PolinRider, this collapses two attacks into one. The moment the actor takes over a GitHub account and pushes a malicious commit — which is exactly the capability the campaign already had at scale — every Go module and every Composer package backed by that repo is automatically compromised too. Repository access and publishing access are the same thing in these ecosystems. No stolen NPM token required. No PyPI credential required. The registry does the distribution for you.<br>The evidence, ecosystem-by-ecosystem<br>Packagist / Composer<br>The PHP ecosystem is particularly vulnerable to these force push style attacks. So far we have identified 10 packages in the Packagist registry that are part of the PolinRider campaign. Each is distributed through Composer’s normal require flow, because Composer trusted the linked GitHub repo.<br>thiio/kubernetes-php-sdk - GitHub compromised commit

arsl/optima-class - GitHub compromised commit

olc/olc-php - GitHub compromised commit

sevenspan/laravel-whatsapp - GitHub compromised commit

adxio/twig-hmvc - GitHub compromised commit

sevenspan/code-generator - GitHub compromised commit

lambda-platform/moqup - GitHub compromised commit

roberts/leads - GitHub compromised commit

sevenspan/laravel-chat - GitHub compromised commit

plusinfolab/logstation - GitHub compromised commit

The sevenspan Packagist namespace shows how this worked in practice. The affected Composer packages were backed by GitHub repositories maintained by the 7span organization. Once the attacker had write access to those repos, they could add the PolinRider payload directly to the source code. In the example below from 7span/react-list, the malicious JavaScript was appended to vite.config.js. The commit timeline shows the campaign’s signature evasion: malicious payload is added to the vite.config.js file via a force push...

github commit compromised registry packagist polinrider

Related Articles