Two days after we published our analysis of a trojanized CUDA toolkit linked to DPRK threat actors, we found the same operational cluster has pivoted. The new vector: fake Next.js coding assessments sent to developers during job interviews. We pulled the actual malware loader source code off live staging infrastructure, confirmed two active command-and-control servers, registered a fake agent, and captured the full Stage 2 tasking payload when the C2 pushed it to our session. We deobfuscated the entire chain. The campaign is still running.
Discovery and Acquisition
Microsoft Defender published a report on February 24 identifying malicious repositories on Bitbucket posing as legitimate Next.js projects. We independently acquired artifacts from the live infrastructure and confirmed active C2 servers. Five Vercel-hosted staging domains remained live at time of analysis, including endpoints actively serving malware loader code to anyone who requested it.
The campaign uses recruiting-themed lures. A developer receives a repository framed as a technical assessment or interview project. The repo looks legitimate. It builds. It runs. It also phones home to Pyongyang.
Three Execution Paths, One Outcome
The attackers embedded three independent execution triggers into each repository, ensuring infection regardless of how the developer interacts with the code:
Path 1: VS Code Workspace Trigger
A .vscode/tasks.json file configured with runOn: "folderOpen" executes a Node script the moment the developer opens the project folder in VS Code and clicks Trust. The script fetches a loader from a Vercel staging domain and executes it in memory.
Path 2: Dev Server Trigger
When the developer runs npm run dev, a trojanized JavaScript file disguised as a legitimate library (e.g., jquery.min.js) decodes a base64 URL hidden in its source, fetches a loader from Vercel, and executes it within the running Node.js process.
Path 3: Backend Startup Trigger
The .env file contains a base64-encoded C2 endpoint stored in an innocuous variable like AUTH_API. A backend route file (server/routes/api/auth.js) decodes it on import, transmits the entire process.env to the attacker, and executes whatever JavaScript the server returns using new Function("require", response.data)(require). This path exfiltrates cloud API keys, database credentials, and deployment secrets before the malware even establishes persistence.
The Stage 1 Loader: Hiding RCE in Error Handling
We acquired the complete Stage 1 loader source code directly from two live Vercel endpoints. One served it in cleartext. The other used string array rotation obfuscation. Both implement the same logic. The cleartext variant, pulled from api-web3-auth[.]vercel[.]app/api/auth, is 1,290 bytes of weaponized simplicity:
The loader imports axios and os, then defines two functions. getSystemInfo() collects the victim\'s hostname, every non-null MAC address from all network interfaces, and the OS type, release, and platform. checkServer() sends this profile to the C2 as query parameters alongside an exceptionId (a per-campaign identifier like env101383) and an instanceId that starts at zero.
The C2 response determines what happens next. If status is "ok", the loader stores any returned instanceId for session tracking and loops. If status is "error", the message field is passed to errorFunction(), which calls new Function(\'require\', message) and immediately invokes the result with Node\'s require as the argument.
That single line is the entire RCE mechanism. new Function() compiles an arbitrary string into executable JavaScript at runtime. By passing require as a parameter, the attacker\'s code gains full access to Node\'s module system, meaning it can load child_process to run shell commands, fs to read and write files, net to open sockets, or anything else in the Node.js ecosystem. The server sends code. The victim\'s machine runs it. No files touch disk.
The loader polls every 5 seconds via setInterval(checkServer, 5000). It is patient. The C2 returns {"status":"ok","message":"server connected"} until an operator decides to engage. We confirmed this response from both live C2 servers during our analysis.
The Obfuscated Variant
The second loader, served from oracle-v1-beta[.]vercel[.]app/api/getMoralisData, implements identical logic through string array rotation obfuscation. We extracted the string table and reconstructed the C2 URL from fragments: http: + //163 + .245. + 194.2 + 16:30 + 00 resolves to 163[.]245[.]194[.]216:3000, one of the two C2 servers we confirmed as live. The obfuscation adds anti-debugging wrappers and a self-referencing toString trap, but the core logic, new Function(\'require\', message), remains identical.
Live C2 Infrastructure
We probed all four C2 IP addresses identified in the campaign. Two responded:
| IP | Port | ASN | Location | Status |
|---|---|---|---|---|
147.124.202.208 | 3000 | AS397423 Tier.Net | New York, US | LIVE |
163.245.194.216 | 3000 | AS26666 Interserver | Dallas, US | LIVE |
87.236.177.9 | 3000 | AS207728 EUROHOSTER | Netherlands | DOWN |
66.235.168.136 | 3000 | AS397423 Tier.Net | Oregon, US | DOWN |
Both live servers run Express.js (confirmed via X-Powered-By header) and expose two endpoints: GET /api/errorMessage returns the idle beacon response, and GET /health returns {"ok":true}. The servers accept any instanceId parameter without validation, returning the same static response. POST requests to all endpoints return 404, indicating the tasking endpoints (/api/handleErrors, /api/reportErrors) either use a different HTTP method signature or have been reconfigured since Microsoft\'s initial observation.
The ETag header W/"2c-C+DUpHd4h+gH5EGTBj3pzaIUfSY" is identical across both servers and has remained stable throughout our monitoring window, confirming a shared codebase deployed to separate infrastructure. No TLS is configured on either C2, all communication runs over plaintext HTTP.
Stage 2: Captured Live
After building a fake agent that mimicked the Stage 1 beacon protocol, we registered with the live C2 at 163[.]245[.]194[.]216:3000 and maintained a persistent session. On beacon 38, the server pushed a 19KB obfuscated JavaScript payload via {"status":"error","message":"<code>"}. Over the next two minutes, it delivered the same payload six more times, each with a unique handleCode tracking token that changed the hash on every delivery, per-session polymorphism designed to defeat hash-based detection.
We fully deobfuscated the Stage 2 tasking client by reversing the string array rotation and resolving the concatenation chain. The obfuscated wrapper builds a 4,928-character JavaScript string, then immediately spawns it as a detached, hidden child Node.js process: spawn(process.execPath, ["-e", code], {detached: true, windowsHide: true, stdio: ["pipe", "ignore", "ignore"]}).unref(). The parent process exits. The child persists.
The deobfuscated Stage 2 operates as a polling tasking client. It collects a system profile identical to Stage 1 (hostname, all MACs, OS details) plus the victim\'s public IP address via api[.]ipify[.]org. It then enters an infinite loop, POSTing {agentId, handleCode, sysInfo} to POST /api/handleErrors every 5 seconds with a 10-second timeout race.
The C2 response drives all behavior. A messages[] array contains arbitrary JavaScript strings, each spawned as a separate hidden child process via STDIN pipe, the same new Function() philosophy but now through child_process.spawn. A responseCode field rotates the session token on every exchange. A newAgentId field enables live agent ID rotation. A responseCode of "-1" triggers self-destruct: the client kills all managed child processes via taskkill /T /F on Windows or SIGTERM/SIGKILL on Unix, then exits.
Error telemetry is posted to POST /api/reportErrors with typed categories: deps-address for IP resolution failures, deps-child-main for child spawn errors, deps-running for execution failures, deps-main for C2 communication errors, and deps-theard for main loop crashes. The misspelling of "thread" as theard is a consistent fingerprint confirming a non-native English speaker authored the code.
Microsoft\'s earlier analysis identified additional Stage 2 capabilities including directory browsing through paired enumeration endpoints (/api/hsocketNext and /api/hsocketResult) and staged file exfiltration through a three-phase upload workflow (/upload, /uploadsecond, /uploadend). These endpoints are accessed through the messages[] tasking mechanism we captured, the C2 sends JavaScript that implements these capabilities, and the Stage 2 client blindly executes it. This is targeted espionage infrastructure, not spray-and-pray commodity malware.
The Social Engineering Layer: monobyte
The attackers built a complete fake product to justify the repository\'s existence. monobyte-code[.]vercel[.]app serves a polished landing page for a fictional code editor called monobyte, branded as "THE LAST EDITOR YOU NEED." The site features a retro CRT aesthetic with scanline effects, a lo-fi background music track titled "Neon Haze," and a feature list including "Smart indexing & debugging," "Agent Instructions manager," and "Code review & git GUI."
The page includes a Mailchimp waitlist form connected to an account under the name chaosbyte (Mailchimp datacenter: us9, list ID: a6193b602503875411d9c8e9a). This is likely harvesting developer email addresses for future targeting. The entire frontend is built with Vue.js 3.5.25 and Vite, not Next.js, meaning the attackers used a different framework for their bait site than the one they\'re trojanizing.
Victim Profiling: IP and VPN Detection
Two additional Vercel endpoints serve as reconnaissance modules. ip-check-notification-rkb[.]vercel[.]app/api and ip-check-notification-firebase[.]vercel[.]app/api return JSON responses containing the requesting IP address, VPN detection status, and a reassuring note: "Connection appears normal. For best performance, avoid VPNs or anonymized routes." These modules allow the operators to filter out sandboxes, VPN users, and security researchers before engaging a target with the actual malware payload.
DPRK / Lazarus Attribution
This campaign maps directly to the DPRK threat cluster tracked as Contagious Interview (CL-STA-0240). The overlap with our previous CUDA toolkit analysis and with documented Lazarus tradecraft is structural, not circumstantial:
Recruiting-themed lures targeting developers through fake job assessments are the defining characteristic of Contagious Interview, documented by Palo Alto Unit 42, Group-IB, and multiple government advisories. The use of Node.js new Function() for dynamic code execution mirrors the Python eval() and exec() patterns in BeaverTail/InvisibleFerret. Multiple redundant execution paths (VS Code, dev server, backend) reflect the same defense-in-depth-of-offense philosophy we observed in the CUDA toolkit\'s four persistence mechanisms.
The infrastructure choices align with known DPRK procurement patterns. Budget US VPS providers (Tier.Net at $3-5/month, Interserver) are consistent with DPRK operational infrastructure documented by Mandiant and Recorded Future. The use of Vercel\'s free tier for staging eliminates payment trails. The exceptionId values (env101383, env99*) suggest organized tracking of multiple targeting campaigns or environments, indicating this is not a one-off operation but part of a managed program.
Microsoft references prior DPRK campaigns using VS Code projects in their report. The repository naming conventions (Cryptan, JP-soccer, RoyalJapan, SettleMint) follow patterns identified in earlier Lazarus developer targeting operations. The monobyte/chaosbyte branding and Mailchimp email collection suggest an actor building long-term targeting infrastructure, not conducting opportunistic attacks.
IOC Summary
C2 Servers
| Indicator | Type | Notes |
|---|---|---|
147.124.202.208:3000 | C2 IP | Express.js, LIVE |
163.245.194.216:3000 | C2 IP | Express.js, LIVE |
87.236.177.9:3000 | C2 IP | DOWN |
66.235.168.136:3000 | C2 IP | DOWN |
Staging and Social Engineering Domains
| Indicator | Type | Notes |
|---|---|---|
api-web3-auth[.]vercel[.]app | Staging | Serves cleartext Stage 1 loader |
oracle-v1-beta[.]vercel[.]app | Staging | Serves obfuscated Stage 1 loader |
monobyte-code[.]vercel[.]app | Social Engineering | Fake code editor landing page |
ip-check-notification-rkb[.]vercel[.]app | Recon | IP/VPN profiling |
ip-check-notification-firebase[.]vercel[.]app | Recon | IP/VPN profiling |
price-oracle-v2[.]vercel[.]app | Staging | Taken down |
coredeal2[.]vercel[.]app | Staging | Taken down |
vscodesettingtask[.]vercel[.]app | Staging | Taken down |
ip-checking-notification-kgm[.]vercel[.]app | Recon | Status unknown |
ip-check-notification-03[.]vercel[.]app | Recon | Status unknown |
ip-check-wh[.]vercel[.]app | Recon | Status unknown |
ip-checking-notification-firebase111[.]vercel[.]app | Recon | Status unknown |
ip-check-notification-firebase03[.]vercel[.]app | Recon | Status unknown |
File Hashes (SHA-256)
| Hash | Description |
|---|---|
c15111d9246e845b7d8dcffc6b9c351beb12aaebcf70c1ad980dcf8b1f1965b7 | Stage 1 loader (cleartext, acquired live) |
ae87e37db08525a46be1024068e44521c8a233a23c8069e349f0c2bcfacf634c | Stage 1 loader (obfuscated, acquired live) |
49bc956bd495526b1009172221d15dda75a860d523dbc1a91ebd194e2ddec96a | Stage 2 tasking client (obfuscated, captured live from C2) |
2b3a5abf9aebe5cc5082fef65548c6670f9228b4cd1b682b966a223457ce2b49 | Stage 2 tasking client (deobfuscated) |
ddd43e493cb333c1cc5d7cd50a6a5a61ecd89cfa5f4076f62c2adf96748b87f8 | Malicious repo artifact |
449e2bf57ab4790427a3a7de3d98b6c540e76190a3d844de2f0e7b66be842b19 | Malicious repo artifact |
07ad8525844ce61471e08e8c515b76bf063bac482394152bad814026cd577f69 | Malicious repo artifact |
e4d71aa95be0725c351e9d1d273d35ccdb0a8bdb31a57927c8738431b89788f5 | Malicious repo artifact |
13152dcb3be425e1ce0f085cd733121a4665cf9935cf8867738e3d510a80308a | Malicious repo artifact |
6d59740d0710da370d5c38ddf88d6912487a1799e4ad09b72d764a3d27ed16b3 | Malicious repo artifact |
Network Indicators
| Indicator | Context |
|---|---|
GET /api/errorMessage | Stage 1 C2 beacon endpoint |
POST /api/handleErrors | Stage 2 tasking endpoint (captured live) |
POST /api/reportErrors | Stage 2 error telemetry endpoint |
exceptionId=env101383 | Campaign/target identifier |
chaosbyte.us9.list-manage.com | Mailchimp email harvesting |
deps-theard | Stage 2 error category (attribution fingerprint) |