Привет, Хабр!

Меня зовут Владислав Кормишкин, я аналитик-исследователь угроз кибербезопасности в компании R-Vision. Сегодня я хотел бы рассказать вам об одном из самых популярных редакторов кода — VSCode, и о том, как злоумышленники могут использовать его в своих целях. На сегодняшний день известно о 229 миллионах установок расширений через встроенный магазин, содержащих вредоносный код. Уязвимости в редакторах кода используются давно, в том числе и APT (Advanced Persistent Threat) с применением социальной инженерии, как это было и с Visual Studio.

В этой статье мы рассмотрим архитектурные особенности VSCode, связанные с безопасностью исполнения кода. Мы также проанализируем уязвимость CVE-2023-46944 и объясним, почему, несмотря на то, что разработчик GitKraken пропатчил её в 2023 году, она всё ещё может быть потенциально опасной из-за особенностей работы с Git. Кроме того, мы расскажем, как именно эта уязвимость была пропатчена, и предложим правило для её обнаружения с использованием языка VRL и плагина R-Object.

Функции безопасности VSCode

Visual Studio Code (VSCode) — мощная и востребованная интегрированная среда разработки (IDE), созданная на базе платформы Electron. Она объединяет в себе возможности Node.js и браузера Chromium, предоставляя разработчикам дополнительную гибкость. VSCode наследует модель процессов от Electron, разделяя функциональность на различные процессы для повышения безопасности. Таким образом, процессы рендеринга, выполняющиеся во встроенном браузере Chromium, имеют ограниченные привилегии, в то время как основной процесс, работающий на Node.js, обладает повышенными привилегиями и может напрямую взаимодействовать с операционной системой. Несмотря на обширный открытый исходный код (почти 800 тысяч строк кода), официальные сборки от Microsoft содержат проприетарные компоненты.

VSCode Workspace Trust — это функция безопасности, представленная в Visual Studio Code в 2021 году. Она помогает предотвращать запуск вредоносного кода и возможных уязвимостей из недоверенной среды. Недоверенной средой считается директория, которая была открыта впервые или для нее не был определен тип доверия. Выбрать его можно во всплывающем окне:

Всплывающее окно Workspace Trust при открытии новой директории

В доверенной рабочей области доступны все функции и расширения приложения, а в ненадёжной — некоторые функции и расширения могут быть недоступны. Ограничения касаются расширений, которые взаимодействуют с файловой системой, запускают сценарии или имеют доступ к сетевым ресурсам. Для предотвращения непреднамеренного выполнения потенциально опасного кода в ненадежных рабочих областях будут ограничены функции отладки, запуска задач и команды терминала. Подразумевается, что, отметив область как недоверенную, можно будет проверить весь находящийся в ней код и после проверки установить значение на доверенное.

Звучит немного идеалистично, не правда ли?

Давайте подробнее рассмотрим, как с этим связана наша уязвимость.

Уязвимость в GitLens (CVE-2023-46944)

В 2023 году была обнаружена уязвимость CVE-2023-46944, в которой используется расширение GitLens для запуска вредоносного кода из недоверенной среды. Но как же так происходит? В документации мы видим, что расширения VSCode могут иметь три типа работы со средами — true, false и limited:

capabilities:
  untrustedWorkspaces:
    { supported: true } |
    { supported: false, description: string } |
    { supported: 'limited', description: string, restrictedConfigurations?: string[] }

Первые два типа настроек либо позволяют использовать расширения в недоверенных средах, либо блокируют их работу. Третий тип настроек предоставляет возможность ограниченного использования расширений. Задается этот тип в файле проекта package.json. При этом определенные параметры могут автоматически ограничиваться с помощью свойства RestrictedConfigurations, где в массиве можно указать Setting ID, и этот параметр будет ограничен в недоверенной среде:

Пример Setting ID

В расширении GitLens определение таких свойств отсутствует, но на скриншоте видно, что поддерживается тип limited, а это значит, что часть функционала будет работать в недоверенных средах:

Файл package.json репозитория Gitlens

В версии GitLens до 14.0.0 отсутствовала проверка на нахождение в недоверенной среде. При открытии репозитория расширение обращалось к Git для инициализации проверки, что позволяло запускать вредоносный код. Начиная с версии Git 2.37.0 была добавлена интересная функция core.fsmonitor. Если мы обратимся к документации с помощью Web Archive и посмотрим описание функции за 2021 год, то увидим следующее:

Страница git-config в web.archive

Подразумевается, что в переменной fsmonitor могут содержаться команды.

Для чего же нужна функция core.fsmonitor и как ее использовать? Для этого давайте разберемся, что из себя представляет Git config, так как в нем содержится функция fsmonitor.

Git config — это файл конфигурации для Git, в котором хранятся настройки, специфичные для конкретного репозитория. Он создается при инициализации репозитория git init, его можно редактировать вручную или с помощью команд. Git поддерживает конфигурацию из трех источников, и каждый уровень заменяет предыдущий: системный /etc/gitconfig, глобальный ~/.gitconfig и локальный репозиторий .git/config. На этом этапе нам интересен файл .git/config.

Функция fsmonitor позволяет при вызовах git status, git add, git diff и других команд, для которых требуется информация о файлах в локальном репозитории, использовать индексацию, чтобы не перебирать все файлы. Это значительно ускоряет работу Git с большими репозиториями. В fsmonitor можно как записать значение типа boolean для включения или отключения функции, так и указать исполняемый файл. Для запуска произвольного файла (калькулятора в примере ниже), .git/config файл должен выглядеть следующим образом:

[core]
    repositoryformatversion = 0
    filemode = true
    bare = false
    logallrefupdates = true
    fsmonitor = calc.exe

Посмотрим, как же работает наша уязвимость.

Эксплуатация уязвимости

Давайте проверим, что произойдет, если в версии VSCode 19.0 запустить нелегитимный код, используя расширение GitLens 13.6.0, как это описано в PoC для уязвимости CVE-2023-46944:

Демонстрация запуска калькулятора в VSCode. Уязвимость CVE-2023-46944.

Возникает вопрос, как же происходит запуск команды из fsmonitor? При открытии рабочей области с файлами и папками в VSCode, Git пытается определить, является ли эта область репозиторием. И если он обнаруживает в ней знакомые папки и файлы, такие как .git, HEAD или config, то пытается ее инициализировать ее и проверить, есть ли изменения в открытом репозитории. То есть при открытии директории в VSCode (в нашем случае нажатие на файл README.md) происходит запуск команд по синхронизации с удаленным репозиторием.

Посмотрим, как это будет выглядеть в событиях с использованием вредоносных команд, а не просто с запуском калькулятора. Для этого возьмем полезную нагрузку типа Fetch Payload, так как нагрузки, которые не используют команды для запуска, в данном случае не работают. В файле .git/config это выглядит следующим образом:

[core]
    repositoryformatversion = 0
    filemode = true
    bare = false
    logallrefupdates = true
    fsmonitor = "powershell -w hidden -nop -e <payload> #

В <payload> используется запуск утилиты curl (инструмент командной строки для передачи данных по различным сетевым протоколам).

Анализ событий

При открытии файла происходит выполнение ряда команд по проверке изменений в открытом репозитории.

Проанализируем последовательность событий с помощью R-Vision SIEM:

Событие из интерфейса R-Vision SIEM

Здесь нас интересует выделенная команда ls-files -- README.md, ее выполнение как раз и триггерит вызов функции fsmonitor. Аналогичные события происходят и в Windows.

Мы видим, что родительским процессом для чтения README.md от Git является процесс code:

Родительский процесс для Git

Событие запуска полезной нагрузки выглядит следующим образом:

Запуск полезной нагрузки

В поле spid (означает родительский процесс) находится значение dpid (PID текущего процесса) из события запуска Git, значит, запуск /bin/sh является дочерним процессом от Git. Теперь мы понимаем, что это выглядит как цепочка процессов.

Это уязвимость 2023 года, и можно подумать, что раз в VSCode все расширения по умолчанию обновляются, то и беспокоиться не о чем. Однако это не совсем так. Посмотрим, как обстоят дела с устранением этой уязвимости.

Патчинг

Патч для уязвимости можно посмотреть в этом коммите. Изменения в проекте были сделаны в сборке 14.0.0. Для устранения уязвимости были добавлены дополнительные проверки на нахождение в недоверенной среде — Untrusted Workspace. Ранее в статье мы выяснили, что в Gitlens не было проверки на тип среды, и расширение запускало Git для инициализации репозитория и выполнения операций индексации файлов.

В файл src/git/errors.ts был добавлен экспортируемый класс WorkspaceUntrustedError. Он представляет собой тип ошибки, который можно генерировать и перехватывать специально для обработки случаев, когда операции Git не разрешены из-за ненадежной рабочей области:

Добавление обработки ошибок для закрытия уязвимости

Класс WorkspaceUntrustedError также используется в src/env/node/git/git.ts. Этот файл отвечает за взаимодействие расширения GitLens с самим Git. В классе Git, который используется для обработки и запуска команд, добавлена проверка типа рабочей области. Проверка происходит с помощью API функции VSCode isTrusted, которая используется в расширениях со значением limited. В начале статьи мы узнали, что значение limited позволяет реализовать работу отдельных функций расширения в недоверенной среде (подробнее в документации).

Добавлена проверка типа рабочей области

Файл src/git/gitProviderService.ts абстрагирует взаимодействие с Git, предоставляя согласованный API для остального расширения GitLens и взаимодействия с репозиториями, независимо от конкретной реализации или среды. В него были добавлены следующие проверки:

Добавлены проверки изменения типа рабочей области на доверенную

Алгоритм проверок следующий:

  1. Изначально любая рабочая область является недоверенной, только если ранее не было выбрано обратное.

  2. С помощью выражения !workspace.isTrusted проводится проверка на принадлежность рабочей области к недоверенной.

  3. Функция onDidGrantWorkspaceTrust ожидает изменения состояния доверия рабочей области.

  4. Если рабочая область является доверенной и содержит папки для инициализации репозитория, то она инициирует обнаружение репозитория.

  5. Если рабочая область является доверенной и в рабочей области нет папок для инициализации репозитория, она возвращает значение emptyDisposable, которое не вызывает никаких операций и является заглушкой.

Пустая функция emptyDisposable

Таким образом, в GitLens добавили проверку на тип активного Workspace. Ну вот, вроде бы все проверки добавлены. Но как обстоят дела сейчас?

Если мы попробуем открыть наш репозиторий с вредоносным кодом в обновленном GitLens, то код и правда не запустится, но только при выборе недоверенной среды. Если мы выберем среду как доверенную, код у нас благополучно исполнится.

Давайте честно, как часто при скачивании репозитория вы проверяете все его файлы на подозрительные строки? Если использовать git clone <repository>, вы не скачаете .git/config файл и, скорее всего, не столкнетесь именно с этой уязвимостью. Но что делать, если это будет репозиторий, который был получен из другого источника?

Откроем вредоносный репозиторий в VSCode без плагина GitLens, но в доверенной среде:

Демонстрация запуска Reverse-Shell

У нас снова происходит запуск вредоносного кода:

  • слева видно, что запуск происходит при выборе недоверенной среды во всплывающем окне;

  • после открытия файла README.md в правом нижнем окне происходит запуск sh.exe и командной строки с powershell.exe;

  • в правом верхнем окне происходит получение оболочки reverse-shell, в данном случае cmd/windows/http/x64/shell_reverse_tcp.

В продемонстрированном PoC используется аналогичная команда запуска кода с помощью PowerShell.

Содержимое файла .git/config:

[core]
    repositoryformatversion = 0
    filemode = true
    bare = false
    logallrefupdates = true
    fsmonitor = "powershell -w hidden -nop -e <payload> #"

Детектирование

В связи с тем, что для предотвращения этой активности нет какого-то простого решения, можно использовать правила корреляции для Windows и Linux, построенные на детектировании цепочки событий, приведенной ранее.

Построим детект следующей цепочки событий:

  1. Первое событие — запуск VSCode. Он является родительским для процесса Git.

  2. Второе событие — запуск Git с командой перечисления файлов ls-files.

  3. Третье событие — запуск оболочки sh с исполнением команд через -c.

На примере R-Vision SIEM посмотрим, как наши правила детектирования обнаруживают такую вредоносную активность:

Windows

Правило корреляции для Windows, написанное на языке R-Object в R-Vision SIEM:

Правило корреляции для Windows
id: e65f4f66-e0ee-4eef-8d98-513d184fae84
name: Эксплуатация уязвимости CVE-2023-46944 в расширении GitLens для VSCode на Windows
version: 1.0.0
date: 2024-05-29
author: Vladislav Kormishkin, R-Vision
status: stable

type: correlation_rule
severity: high
description: Правило срабатывает при открытии Git репозитория в VSCode, как в доверенной, так и в недоверенной среде, где в файле .git/config в функции fsmonitor указана вредоносная команда. Данная активность относится к уязвимой версии расширения GitLens до 13.6.0 включительно, ей присвоен номер CVE-2023-46944. Также данная активность может возникнуть при открытии вредоносного Git репозитория в доверенной среде, но с пропатченой версией GitLens или вообще без установленного расширения, в версиях редактора старше 1.63.1.

reference:
  - https://packetstormsecurity.com/files/178227/GitLens-Git-Local-Configuration-Execution.html

tags:
  - Execution
  - attack.T1203

data_source:
  - Windows
  - Security
    - EventID_4688
  - Sysmon_Operational
    - EventID_1

known_false_positives:
  - "Пока неизвестно"

group_by:
  - dvchost

filter: !vrl |
  .dvendor == "Microsoft" && 
  includes(["1", "4688"], .externalId) 

aliases:
  event_code:
    filter: !vrl |
      oldFileName = downcase(to_string(.oldFileName) ?? "-")
      sproc = downcase(to_string(.sproc) ?? "-")
      flag = false

      if ends_with(sproc, "\\code.exe") ||
          oldFileName == "electron.exe" {
              flag = true 
      }
      flag

  event_git:
    filter: !vrl |
      cmd = downcase(to_string(.cmd) ?? "-")
      oldFileName = downcase(to_string(.oldFileName) ?? "-")
      dproc = downcase(to_string(.dproc) ?? "-")
      sproc = downcase(to_string(.sproc) ?? "-")
      flag = false

      if (ends_with(sproc, "\\git.exe") ||
             oldFileName == "git.exe") &&
              contains(cmd, "ls-files")  {
                  flag = true
      }
      flag

  event_sh:
    filter: !vrl |
      cmd = downcase(to_string(.cmd) ?? "-")
      oldFileName = downcase(to_string(.oldFileName) ?? "-")
      dproc = downcase(to_string(.dproc) ?? "-")
      sproc = downcase(to_string(.sproc) ?? "-")
      flag = false

      if ((ends_with(sproc, "\\git.exe") ||
           oldFileName == "git.exe") &&
              ends_with(dproc, "\\sh.exe") &&
              starts_with(cmd, "sh -c"))  {
                  flag = true 
      }
      flag
  
select:
    alias: event_code
    join:
      alias: event_git
      on:
        - eq: {event_code: .dpid, event_git: .spid}
      join:
          alias: event_sh
          on:
            - eq: {event_git: .dpid, event_sh: .spid}

ttl: 60

on_correlate: !vrl |
  . |= compact({
    "rt" : %event_sh.rt,
    "dvendor" : %event_sh.dvendor,
    "dversion" : %event_sh.dversion,
    "dhost" : %event_sh.dhost,
    "dproc" : %event_sh.dproc,
    "dvchost" : %event_sh.dvchost,
    "oldFilePath" : %event_sh.oldFilePath,
    "duser" : %event_sh.duser,
    "suser" : %event_sh.suser,
    "sntdom" : %event_sh.sntdom,
    "sproc" : %event_sh.sproc,
    "accessMask" : %event_sh.accessMask,
    "externalId" : %event_sh.externalId,
    "oldFileName" : %event_sh.oldFileName,
    "fname": %event_sh.fname,
    "dntdom" : %event_sh.dntdom,
    "cmd" : %event_sh.cmd,
    "sourceServiceName" : %event_sh.sourceServiceName,
    })
    
    .msg = "На узле " + (to_string(.dvchost) ?? "-") + " пользователем " + (to_string(.duser) ?? "-") + " домена " + (to_string(.dntdom) ?? "-") + " от процесса Git с родительским процессом VSCode запущена команда " + (to_string(.cmd) ?? "-") + ", данное поведение может указывать на эксплуатацию уязвимости CVE-2023-46944"

Linux

Правило корреляции для Linux, написанное на языке R-Object в R-Vision SIEM:

Правило корреляции для Linux
id: 62697bd1-1731-4437-bc31-572815389f29
name: Эксплуатация уязвимости CVE-2023-46944 в расширении GitLens для VSCode на Linux
version: 1.0.0
date: 2024-05-30
author: Vladislav Kormishkin, R-Vision
status: stable

type: correlation_rule
severity: high
description: Правило срабатывает при открытии Git репозитория в VSCode, как в доверенной, так и в недоверенной среде, где файле .git/config в функции fsmonitor указана вредоносная команда. Данная активность относится к уязвимой версии расширения GitLens до 13.6.0 включительно, ей присвоен номер CVE-2023-46944. Также данная активность может возникнуть при открытии вредоносного Git репозитория в доверенной среде, но с пропатченой версией GitLens или вообще без установленного расширения, в версиях редактора старше 1.63.1.

reference:
  - https://packetstormsecurity.com/files/178227/GitLens-Git-Local-Configuration-Execution.html

tags:
  - Execution
  - attack.T1203

data_source:
  - Linux
    - Auditd
      - Execve

known_false_positives:
  - "Пока неизвестно"

group_by:
  - dvchost

filter: !vrl |
  .dvendor == "Linux" && 
  .cs4 == "execve" 

aliases:
  event_code:
    filter: !vrl |
      dproc = downcase(to_string(.dproc) ?? "-")
      flag = false

      if ends_with(dproc, "/code/code") {
            flag = true 
      }
      flag

  event_git:
    filter: !vrl |
      cmd = downcase(to_string(.cmd) ?? "-")
      dproc = downcase(to_string(.dproc) ?? "-")
      flag = false

      if ends_with(dproc, "/bin/git") &&
          contains(cmd, "ls-files")  { #перечисление файлов
              flag = true 
      }
      flag
  
  event_sh:
    filter: !vrl |
      filePath = downcase(to_string(.filePath) ?? "-")
      dproc = downcase(to_string(.dproc) ?? "-")
      cmd = downcase(to_string(.cmd) ?? "-")
      flag = false

      if contains(filePath, "/bin/sh") &&
          contains(cmd, "-c"){
              flag = true 
      }
      flag

select:
    alias: event_code
    join:
      alias: event_git
      on:
        - eq: {event_code: .spid, event_git: .dvcpid}
      join:
          alias: event_sh
          on:
            - eq: {event_git: .spid, event_sh: .dvcpid}

ttl: 60

on_correlate: !vrl |
  . |= compact({
    "rt" : %event_sh.rt,
    "dvendor" : %event_sh.dvendor,
    "dversion" : %event_sh.dversion,
    "dhost" : %event_sh.dhost,
    "dproc" : %event_sh.dproc,
    "dvchost" : %event_sh.dvchost,
    "oldFilePath" : %event_sh.oldFilePath,
    "duser" : %event_sh.duser,
    "suser" : %event_sh.suser,
    "sntdom" : %event_sh.sntdom,
    "sproc" : %event_sh.sproc,
    "accessMask" : %event_sh.accessMask,
    "externalId" : %event_sh.externalId,
    "oldFileName" : %event_sh.oldFileName,
    "fname": %event_sh.fname,
    "dntdom" : %event_sh.dntdom,
    "cmd" : %event_sh.cmd,
    "sourceServiceName" : %event_sh.sourceServiceName,
    })
    
    .msg = "На узле " + (to_string(.dvchost) ?? "-") + " пользователем " + (to_string(.suser) ?? "-") + " от процесса Git с родительским процессом VSCode запущена команда " + (to_string(.cmd) ?? "-") + ", данное поведение может указывать на эксплуатацию уязвимости CVE-2023-46944"

Заключение

В этой статье мы рассмотрели возможности Workspace Trust и способы предотвращения выполнения потенциально вредоносного кода. Также мы обсудили причины возникновения уязвимости CVE-2023-46944.

Мы выяснили, что если открывать вредоносные репозитории в доверенной среде, то патч не поможет, так как эта активность связана с функционалом интегрированного расширения Git. Для ее отслеживания можно использовать правила корреляции на основе цепочки событий.

К сожалению, в этой ситуации нет простого решения, чтобы полностью обезопасить себя. Однако мы можем выделить основные рекомендации:

  • проверять все, что вы открываете в редакторе кода, если это было скачано из неизвестного или ненадежного источника;

  • отключить по умолчанию интеграцию с Git.

Буду рад, если статья оказалась для вас полезной! Задавайте вопросы и пишите комментарии.

Автор: Владислав Кормишкин (@Watislove), аналитик-исследователь угроз кибербезопасности R-Vision.