One npm account published 141 adware packages in May 2026. npm removed them. Six weeks later, on July 10, the same operator returned and pushed version 2.0.0 updates to every surviving package, refreshing the payload across 36 confirmed names before Amazon Inspector caught the batch on July 13.
The ten packages flagged in this report (MAL-2026-10285 through MAL-2026-10294) are not a new campaign. They are the third wave of a documented operation by the account terminal3airport ([email protected]) that has been running since May 7, 2026.
Campaign Map
The full cluster spans three publishing waves and two distinct sub-families within the May 26-27 window.
| Wave | Date | Version | Package count | Family prefix | Notes |
|---|---|---|---|---|---|
| 1 | May 7, 2026 | 1.1.3 | 2 | changiairportpromax, ilovefemboys | Readable source, Scramjet referenced directly |
| 2a | May 26, 2026 | 1.1.2 | 23 | backup*, whatsadmaidk, named variants | Obfuscated JS, randomized filenames |
| 2b | May 27, 2026 | 1.1.7 | 116 | ratelimitsucks*, backupsitetuff*, ishowfeet*, nottuff*, abuden*, imillegal*, speed*, sixseven*, timmytuffknuckles* | Mass-published via auto-publish.sh in 33 minutes |
| 3 | July 10, 2026 | 2.0.0 | 36+ | All survivors from waves 2a and 2b | Update burst across all 36 packages in 6m27s; detected July 13 |
The ten packages in this report span both wave 2a sub-families: six from the May 26 backup* batch (backup1-gg, backup2-asd, backup3-ff, backup4-gasp, backup5-updated, backupgenuine-updated) and four from the May 27 backupsitetuff* batch (backupsitetuff3, backupsitetuff6, backupsitetuff9, backupsitetuff10).
Triage: What These Names Are
The naming patterns across this cluster are not typosquats and are not dependency confusion probes. They describe nothing. backup1-gg, ishowfeet20, nottuff7, and abuden228 have no relationship to any legitimate npm package, any framework namespace, or any declared dependency in any known public project. We searched GitHub for "backup1-gg" in package.json files and found no results. The names are disposable identifiers chosen for their semantic meaninglessness, which is itself the tell.
The backup* prefix could superficially suggest dependency confusion targeting an internal backup utility. We searched GitHub for "backup1-gg", "backup2-asd", and "backup3-ff" in package.json files. No public organization uses these names internally. This is a spray, not a targeted probe.
Name divergence check: The npm package name, the name field in package.json inside the tarball, and the README heading should match for a legitimate package. In backupsitetuff10 v1.1.7, the npm package name is backupsitetuff10, the manifest name field matches, and there is no README. A package with no README, a semantically meaningless name, and a single publish history is three signals before a single line of code is read.
Version anomaly: The backup* family published at 1.1.2 on May 26. The backupsitetuff* and spray families published at 1.1.7 on May 27. Both families then received 2.0.0 updates on July 10, skipping from minor version space to a major bump with no intermediate history. Nothing about this version arc reflects normal project evolution.
Package Anatomy
Every package in this cluster ships an identical payload. Only package.json’s name field differs between packages. The shared content:
package.json # name field only varies; main: "sw.js", no install hooks
sw.js # Scramjet ServiceWorker
8cfc2/hgshm.js # Scramjet runtime (180 KB, obfuscated)
assets/*.js # 10-12 heavily obfuscated React app bundles
index.html # "Riverbend Tutoring" cover page with adware payload
auto-publish.sh # The operator's own mass-publication script (OPSEC failure)
branding/ # AI provider SVGs (Anthropic, OpenAI, DeepSeek, xAI, Gemini)
.well-known/discord # Discord OAuth verification hashNo preinstall, install, postinstall, or prepare hooks exist. The packages do not execute on npm install. They are static web assets: the npm registry is the hosting infrastructure, not the delivery vector.
The declared main is sw.js. A require('backupsitetuff10') call from Node throws before any code runs: a ServiceWorker file has no meaning in the Node runtime context.
Payload class: this is a Class 4 campaign. The published packages are evidence of registry abuse, not a threat to any machine that installs them. The threat is to browsers that reach the static assets via npm CDN hostnames (unpkg.com, cdn.jsdelivr.net), where the ServiceWorker registers and begins intercepting fetch events.
The OPSEC Failure That Documented the Operation
The auto-publish.sh file left in every v1.1.7 tarball is the most consequential artifact in this campaign. It is the operator’s own publication pipeline, shipped with the payload.
Extracted from backupsitetuff6/package/auto-publish.sh (SHA-256: bc574c406e59c59381239df76b59e7382c6727f29c24149ace0881ebb1ede670):
#!/bin/bash
BASE="backupsitetuff"
TOTAL=10
PARALLEL=3
publish_one() {
NAME="$1"
echo "Building $NAME"
node -e "
const fs = require('fs');
const pkg = require('./package.json');
pkg.name = '$NAME';
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2));
"
npm publish --silent > /dev/null 2>&1
echo "Published: $NAME"
}
count=1
while [ $count -le $TOTAL ]
do
while [ $(jobs -rp | wc -l | tr -d ' ') -ge $PARALLEL ]
do
sleep 0.1
done
publish_one "${BASE}${count}" &
count=$((count + 1))
done
wait
echo "Done publishing all"The script rewrites package.json’s name field to ${BASE}${count} and calls npm publish --silent. The operator ran this script with different BASE values for each family: nottuff produced nottuff1 through nottuff30, ishowfeet produced ishowfeet1 through ishowfeet20, and so on. The hardcoded list inside the v1.7.7 variant of the script enumerates 95+ sibling package names, including the current package’s own name as a literal entry.
The script also documents the rate-limiting the operator hit. The PARALLEL=3 ceiling is a direct response to npm’s publish rate limits. The timestamp spread across the May 27 burst confirms it: 116 packages published across 33 minutes, with the distinct families sequenced in the order the operator switched BASE values.
The Payload
The web proxy running inside these packages is the Scramjet framework by Mercury Workshop, combined with bare-mux for bare server routing. It is a functional web proxy unblocker that intercepts all fetch events in the browser via ServiceWorker and routes traffic through Scramjet’s engine.
sw.js (consistent across all waves):
importScripts('./8cfc2/hgshm.js');
const { ScramjetServiceWorker } = loadWorker();
const proxySw = new ScramjetServiceWorker();
self.addEventListener('install', () => { void self.skipWaiting(); });
self.addEventListener('activate', (event) => {
event.waitUntil(self.clients.claim());
});The injected script in proxied responses hooks window.open, anchor clicks, and form submissions, relaying new-tab navigation to the parent frame via postMessage. The comment TY WAVES + CHATGPT ILY recovered from the ServiceWorker source names the project internally as a collaboration. The GitHub URL github.com/lucideproxy/svg embedded in the React bundle names the project externally: “Lucide Proxy.”
index.html adware layer. The cover page presents as “Riverbend Tutoring | After-School Coaching & Test Prep” with 800+ words of SEO keyword text in a hidden div. The og:url metadata points to 21baseballacademy.com, linking the fake tutoring cover to the ad infrastructure. On the first user interaction (click, keypress, or touch), this inline script fires:
(function () {
var k = 'p7k2x';
var cd = 9e5; // 900,000 ms = 15-minute cooldown
var u = 'https://abdct.com/';
function ok() {
try {
var t = +localStorage.getItem(k) || 0;
return Date.now() - t >= cd;
} catch (_) { return true; }
}
function fire() {
if (!ok()) { done(); return; }
try { localStorage.setItem(k, String(Date.now())); } catch (_) {}
try { window.open(u, '_blank', 'noreferrer'); window.focus(); } catch (_) {}
done();
}
if (ok()) {
['click', 'keydown', 'touchstart'].forEach(function (e) {
document.addEventListener(e, fire, true);
});
}
})();The key p7k2x is consistent across all waves and is the campaign’s strongest persistent browser-side IOC. The popunder target abdct.com returned HTTP 403 during our probe on July 13, 2026. The domain is live.
Additional monetization layers loaded by index.html:
- External scripts from
cdn.21baseballacademy.comandwoofbeginner.com - Google Analytics property
G-0VL3ZSBXDH, shared across all 141 packages - A monetization key endpoint:
https://woofbeginner.com/jivd2xu8?key=c6851a038da578a80eeb201e0588c84c
The Wave 1 (May 7) packages shipped readable React source. The wave 2a (May 26) and wave 2b (May 27) packages ran the React application through a hex variable name obfuscator with randomized file names:
// assets/a3g0q43tbe.js (first 200 chars)
(function(_0x1b35e9,_0x379ecc){const _0x47926c={_0x46fe72:0x7ae,
_0x53a4a0:0xaad,_0xdcea50:0xe56,_0x2a9cda:0xae0,...The obfuscator builds a shuffled string array and resolves literals at runtime through a rotation function and index-offset lookups. sw.js and index.html stayed readable across all waves. The popunder code, external script URLs, and analytics ID sit in plain text.
The branding SVGs in the tarball (branding/) carry logos for Anthropic, OpenAI, DeepSeek, xAI, and Gemini, plus a Roblox shortcut. The proxy’s intended user base is students bypassing school network filters to reach AI chatbots and gaming sites.
Version Timeline
| Package | v1 (Wave 2) | 2.0.0 (Wave 3) | Takedown | Notes |
|---|---|---|---|---|
| backup1-gg | 2026-05-26T14:45:54Z | 2026-07-10T13:02:30Z | 2026-07-13T06:14:44Z | First in wave 2a batch |
| backup2-asd | 2026-05-26T14:46:14Z | 2026-07-10T13:02:33Z | 2026-07-13T06:15:34Z | 20 seconds after backup1-gg |
| backup3-ff | 2026-05-26T14:46:24Z | 2026-07-10T13:02:37Z | 2026-07-13T06:15:44Z | |
| backup4-gasp | 2026-05-26T14:46:40Z | 2026-07-10T13:02:40Z | 2026-07-13T06:15:49Z | |
| backup5-updated | 2026-05-26T14:46:54Z | 2026-07-10T13:02:44Z | 2026-07-13T06:15:29Z | |
| backupgenuine-updated | 2026-05-26T14:47:01Z | 2026-07-10T13:02:47Z | 2026-07-13T06:15:09Z | Last in wave 2a; 67s total for 6 packages |
| backupsitetuff3 | 2026-05-27T04:34:14Z | 2026-07-10T13:04:12Z | 2026-07-13T06:18:17Z | Wave 2b start, 14 hours after wave 2a |
| backupsitetuff6 | 2026-05-27T04:34:22Z | 2026-07-10T13:04:16Z | 2026-07-13T06:22:19Z | |
| backupsitetuff9 | 2026-05-27T04:34:30Z | 2026-07-10T13:04:18Z | 2026-07-13T06:19:16Z | |
| backupsitetuff10 | 2026-05-27T04:34:36Z | 2026-07-10T13:04:22Z | 2026-07-13T06:17:42Z | 22s span for 4 packages |
Wave 3 timing across the full 36-package cluster: backup1-gg received its 2.0.0 update at 13:02:30 UTC on July 10. The final package in the cluster received its update at 13:08:57 UTC. All 36 packages were updated in 6 minutes and 27 seconds. No human operator updates 36 packages manually in under seven minutes; this is a scripted publish loop, the same automation that built the original cluster.
Six packages in the extended cluster (ratelimitsucks2, ratelimitsucks3) received intermediate versions (1.1.4, 1.1.5) between the initial wave 2 publish and the July 10 update, indicating the operator was actively iterating on the payload during the six-week gap.
Infrastructure
abdct.com: Popunder ad network endpoint. Returned HTTP 403 on July 13, 2026. Live.
21baseballacademy.com / cdn.21baseballacademy.com: Linked via og:url in index.html and as a script host. Connects the tutoring cover to the operator’s ad infrastructure.
woofbeginner.com: Secondary monetization script host. Endpoint https://woofbeginner.com/jivd2xu8?key=c6851a038da578a80eeb201e0588c84c was returning HTTP 500 as of May 27 analysis.
Google Analytics G-0VL3ZSBXDH: Shared property ID across all 141 packages. Any session that loads these assets reports to this GA property. The operator receives visit data from every browser that reaches the proxy through any of the 141 package names.
github.com/lucideproxy/svg: Internal project name recovered from the React bundle. The GitHub repository URL is embedded in the hook that intercepts new-tab navigation.
Discord verification: A .well-known/discord file in the v1.1.2/v1.1.7 packages contains the hash bb7ce9f4c508bc87f13889c18031ca42ad8c1bd5. The application uses Discord OAuth for user authentication, sending verification codes via Discord DM.
We searched VirusTotal, urlscan.io (which returned HTTP 403 to our API query), Shodan, and Criminal IP for abdct.com. The domain does not appear in any named threat actor’s prior infrastructure reporting.
Attribution
The npm account terminal3airport ([email protected]) published all 141 packages. SafeDep reported this account on May 27, 2026, covering the wave 2 packages in full. The July 10 wave 3 updates were published from the same account. npm removed the wave 2 packages and replaced them with 0.0.1-security tombstones; the wave 3 updates pushed new malicious versions over the same package names, bypassing the tombstone by republishing under the same version number pattern.
The package names (ilovefemboys, bomboclatwallahi, ishowfeet, imillegal, ratelimitsucks), the TY WAVES + CHATGPT ILY comment in the ServiceWorker, and the student-targeted AI proxy branding all point to a teenage operator running a Scramjet-based unblocker as an ad-monetization side project. The operation is financially motivated, not espionage.
This is not a sophisticated supply chain attack against developer machines. The operator treats npm as free static file hosting and npm CDN hostnames (unpkg.com, cdn.jsdelivr.net) as distribution points that bypass content filters by routing through trusted domains. The harm is to students whose browsers are served popunder ads and third-party tracking scripts from what they believe is a school-safe proxy.
The scale is the finding. One account. 141 packages. A documented return six weeks after removal. A 2.0.0 update batch completed in under seven minutes.
IOC Table
| Indicator | Type | Value | Method |
|---|---|---|---|
| npm account | Publisher | terminal3airport | Pulled from registry metadata during triage across all 10 packages |
| Account identifier | [email protected] | Pulled from npm registry maintainer record | |
| npm package | Malicious package | backup1-gg v1.1.2, v2.0.0 | Confirmed in OSV advisory MAL-2026-10285 / GHSA-5v83-r2g7-g234 |
| npm package | Malicious package | backup2-asd v1.1.2, v2.0.0 | Confirmed in OSV advisory MAL-2026-10286 / GHSA-5wxc-4vm4-55cx |
| npm package | Malicious package | backup3-ff v1.1.2, v2.0.0 | Confirmed in OSV advisory MAL-2026-10287 / GHSA-8w5w-mhwr-m53r |
| npm package | Malicious package | backup4-gasp v1.1.2, v2.0.0 | Confirmed in OSV advisory MAL-2026-10288 / GHSA-rwpw-7393-4jg2 |
| npm package | Malicious package | backup5-updated v1.1.2, v2.0.0 | Confirmed in OSV advisory MAL-2026-10289 / GHSA-cjhm-8r3j-gcvx |
| npm package | Malicious package | backupgenuine-updated v1.1.2, v2.0.0 | Confirmed in OSV advisory MAL-2026-10290 / GHSA-8gjh-hmfj-xqcg |
| npm package | Malicious package | backupsitetuff3 v1.1.7, v2.0.0 | Confirmed in OSV advisory MAL-2026-10292 / GHSA-x8vw-cqjx-99gm; Amazon Inspector IN-MAL-2026-009883, IN-MAL-2026-009884 |
| npm package | Malicious package | backupsitetuff6 v1.1.7, v2.0.0 | Confirmed in OSV advisory MAL-2026-10293 / GHSA-p34p-53j9-759q; Amazon Inspector IN-MAL-2026-009880, IN-MAL-2026-009882 |
| npm package | Malicious package | backupsitetuff9 v1.1.7, v2.0.0 | Confirmed in OSV advisory MAL-2026-10294 / GHSA-rj5f-355v-g5vc; Amazon Inspector IN-MAL-2026-009879, IN-MAL-2026-009881 |
| npm package | Malicious package | backupsitetuff10 v1.1.7, v2.0.0 | Confirmed in OSV advisory MAL-2026-10291 / GHSA-cv9h-3xg8-22j6; Amazon Inspector IN-MAL-2026-009885, IN-MAL-2026-009886 |
| domain | Popunder ad network | abdct.com | Extracted from inline <script> in index.html; confirmed by Amazon Inspector analysis |
| URL | Popunder target | https://abdct.com/ | Extracted from index.html adware script block |
| domain | Ad script host | 21baseballacademy.com / cdn.21baseballacademy.com | Extracted from index.html script src and og:url metadata |
| domain | Monetization script | woofbeginner.com | Extracted from React bundle assets |
| string | Monetization key | c6851a038da578a80eeb201e0588c84c | Extracted from woofbeginner.com request parameter in React bundle |
| string | GA property | G-0VL3ZSBXDH | Extracted from index.html gtag script block; shared across all 141 packages |
| string | localStorage key | p7k2x | Extracted from index.html adware script; rate-limit cooldown key |
| string | Internal project name | lucideproxy | Extracted from window.open.__lucideIntercepted flag in React bundle |
| URL | Project repository | github.com/lucideproxy/svg | Extracted from React bundle string |
| hash | Discord verification | bb7ce9f4c508bc87f13889c18031ca42ad8c1bd5 | Extracted from .well-known/discord file in tarball |
| file | Auto-publication script | auto-publish.sh SHA-256: bc574c406e59c59381239df76b59e7382c6727f29c24149ace0881ebb1ede670 | Recovered from backupsitetuff6 tarball; confirmed in Amazon Inspector advisory |
| file | Scramjet runtime | 8cfc2/hgshm.js SHA-256: 8444505b604b82fa1497c3968d19c35277fa470c5d9088ca4536a6b8ec9c2baa | Confirmed in Amazon Inspector evidence files for backupsitetuff6 |
Extended cluster (127 additional confirmed packages): Full list available in the SafeDep May 27 2026 advisory. All packages from account terminal3airport.
Affected Versions
| Package | Affected versions | Current status | OSV ID |
|---|---|---|---|
| backup1-gg | 1.1.2, 2.0.0 | Replaced by 0.0.1-security tombstone | MAL-2026-10285 |
| backup2-asd | 1.1.2, 2.0.0 | Replaced by 0.0.1-security tombstone | MAL-2026-10286 |
| backup3-ff | 1.1.2, 2.0.0 | Replaced by 0.0.1-security tombstone | MAL-2026-10287 |
| backup4-gasp | 1.1.2, 2.0.0 | Replaced by 0.0.1-security tombstone | MAL-2026-10288 |
| backup5-updated | 1.1.2, 2.0.0 | Replaced by 0.0.1-security tombstone | MAL-2026-10289 |
| backupgenuine-updated | 1.1.2, 2.0.0 | Replaced by 0.0.1-security tombstone | MAL-2026-10290 |
| backupsitetuff3 | 1.1.7, 2.0.0 | Replaced by 0.0.1-security tombstone | MAL-2026-10292 |
| backupsitetuff6 | 1.1.7, 2.0.0 | Replaced by 0.0.1-security tombstone | MAL-2026-10293 |
| backupsitetuff9 | 1.1.7, 2.0.0 | Replaced by 0.0.1-security tombstone | MAL-2026-10294 |
| backupsitetuff10 | 1.1.7, 2.0.0 | Replaced by 0.0.1-security tombstone | MAL-2026-10291 |
Remediation
These packages have no install hooks. A machine that ran npm install backup1-gg did not execute any attacker code at install time. Credential rotation is not required for installing machines.
For registry consumers:
- Run
npm uninstall <package-name>to remove any of the listed packages from local environments. This is hygiene, not incident response. - Audit
package-lock.jsonandnode_modulesfor any of the 141 confirmed package names from accountterminal3airport. The full list is in the SafeDep May 27 advisory. - If your organization uses unpkg.com or cdn.jsdelivr.net to serve npm package assets directly in a browser context, add the package names and the
abdct.com,21baseballacademy.com, andwoofbeginner.comdomains to your content security policy blocklist.
For browser environments:
- Block
abdct.com,cdn.21baseballacademy.com, andwoofbeginner.comat the network edge. These domains serve adware to any browser that reaches the proxy assets. - Check browser ServiceWorker registrations for any origin that served content from these package names via npm CDN hostnames. A registered ServiceWorker survives package removal and continues intercepting fetch events until explicitly unregistered.
- Clear localStorage entries with key
p7k2xto disable the popunder rate-limit state in any browser that visited the proxy pages.
For the publishing side (organizations concerned their build infrastructure was the dropper):
The wave 3 publish at 13:02 UTC on July 10 originated from the terminal3airport account. If your organization’s CI systems published to npm during the July 10 window, review outbound HTTPS logs to registry.npmjs.org for publish traffic from the terminal3airport credential context.
The comment TY WAVES + CHATGPT ILY recovered from sw.js ServiceWorker source is consistent across all waves.
The most useful artifact in this cluster is the auto-publish.sh script. The operator shipped their own publication infrastructure inside the package they were publishing. It documents the campaign’s intended scope (95+ names), its parallelism strategy (3 concurrent publishes), and its rate-limit workaround, all in 39 lines of bash. npm removed 141 packages in May. The operator returned in July with the same tooling and updated 36 survivors. The script will run again.
