Skip to content
KitploitKITPLOIT
ИнструментыБлог
Отправить
ИнструментыБлог
Отправить

Инструменты для хакинга, пентеста и кибербезопасности — ваш арсенал защиты!

Kitploit — это каталог инструментов для хакинга, кибербезопасности и пентестинга. Находите последние обновления проектов для поиска уязвимостей, анализа систем, автоматизации тестирования и усиления вашей безопасности.

··Ленты·Контакты·Конфиденциальность·© 2026 Kitploit

Каталог инструментов

Категории

Все категории
Loading categories
CVE-2023-6702 — Уязвимость 1-day в Chrome Renderer: RCE через путаницу типов в асинхронном стеке вызовов (участие v8ctf) | Kitploit
Инструменты/GitHubGitHub/kaist-hacking/cve-2023-6702
Анализ уязвимостейЭксплуатацияЭксплуатация веб-приложенийCTFОбучение и ОбразованиеЭксплуатация Бинарных Файлов
GitHubkaist-hacking/cve-2023-6702

CVE-2023-6702

Уязвимость 1-day в Chrome Renderer: RCE через путаницу типов в асинхронном стеке вызовов (участие v8ctf)

Репозиторий
86922 лет назадПроверено Kitploit

Популярное

Смотреть все →

Откройте для себя самые используемые инструменты нашего сообщества.

Изучить все инструменты

Просмотрите нашу коллекцию инструментов

Смотреть все инструменты →
Поделиться

Chrome Renderer 1day RCE через Type Confusion в Async Stack Trace (CVE-2023-6702)

Краткое описание

Эта уязвимость позволяла удаленному злоумышленнику выполнить произвольный код в процессе рендеринга Chrome.

В коде обработки асинхронных стековых трасс была недостаточная проверка типа. Это приводило к путанице типов (type confusion) между FunctionContext и NativeContext, что вызывало незаконный доступ к значению JSGlobalProxy->hash. С помощью heap spraying злоумышленник мог внедрить поддельный асинхронный кадр стека и построить примитив fakeobj. Используя примитив fakeobj, злоумышленник мог добиться произвольного выполнения кода в процессе рендеринга Chrome.

Вы можете ознакомиться с нашими слайдами с TyphoonCon 2024.

Поставщик / Продукт / Версия

  • Google Chrome
  • Уязвимые версии: до 120.0.6099.109
  • Исправленная версия: 120.0.6099.109

Хронология

  • 2020-05-13: Внесена ошибка - [Promise.any] Implment async stack traces for Promise.any
  • 2023-11-10: Сообщение об ошибке - Security: V8 Debug check failed: LAST_TYPE >= value
  • 2023-11-15: Исправление - [promises, async stack traces] Fix the case when the closure has run
  • 2023-12-12: Уведомление - https://chromereleases.googleblog.com/2023/12/stable-channel-update-for-desktop_12.html
  • 2024-01-12: Отправка v8CTF <-- Время, когда мы работали над этой уязвимостью
  • 2024-02-23: Раскрыт отчет об ошибке

Предыстория

Async Stack Trace

Асинхронность — одна из важнейших функций JavaScript. В прошлом было трудно отлаживать асинхронный код с помощью стеков ошибок, потому что асинхронные функции не захватывались в стеке ошибок. Приостановленные асинхронные функции хранятся в очереди обратных вызовов цикла событий, а не в стеке вызовов, поэтому стек ошибок не содержит асинхронной функции. Чтобы решить эту проблему, V8 предоставляет функцию "async stack trace" (по умолчанию начиная с V8 v7.3) для захвата асинхронной функции в стеке ошибок. (v8 blog, v8 docs)

Promise.all Resolve Element Closure

"Promise.all Resolve Element Closure" — это вспомогательная функция для разрешения входных промисов в функции Promise.all. Функция Promise.all принимает массив промисов и возвращает промис, который разрешается, когда все входные промисы будут разрешены. "Promise.all Resolve Element Closure" является обработчиком разрешения каждого входного промиса в функции Promise.all. Роль этой функции — разрешить входной промис и сохранить значение выполнения в результирующем массиве.

Есть два важных момента об этой функции:

  1. Это встроенная функция (intrinsic builtin), и она недоступна напрямую из JavaScript-кода.
  2. Её контекст используется как маркер для проверки, была ли функция выполнена. До вызова она имеет FunctionContext, а после вызова — NativeContext. (код V8)

Уязвимость

Класс ошибки: Путаница типов (type confusion) между FunctionContext и NativeContext

Детали уязвимости:

Уязвимость можно вызвать, захватив асинхронный стек вызовов с уже выполненной функцией "Promise.all Resolve Element Closure" или аналогичной встроенной функцией. В этом эксплойте в качестве примера используется функция "Promise.all Resolve Element Closure".

Когда в JavaScript-коде возникает ошибка, V8 захватывает стек ошибки из стека и добавляет асинхронные кадры стека из текущей микрозадачи. [1]

root@kitploit:~
CallSiteBuilder builder(isolate, mode, limit, caller);
VisitStack(isolate, &builder);

// Если --async-stack-traces включены и "текущая микрозадача" является
// PromiseReactionJobTask, мы пытаемся обогатить стек ошибок асинхронными кадрами.
if (v8_flags.async_stack_traces) {
    CaptureAsyncStackTrace(isolate, &builder);
}

Функция CaptureAsyncStackTrace [2] просматривает цепочку промисов и добавляет асинхронный кадр стека в соответствии с типом асинхронного вызова (например, await, Promise.all, Promise.any).

Ниже приведён фрагмент функции CaptureAsyncStackTrace, который обрабатывает случай Promise.all:

root@kitploit:~
} else if (IsBuiltinFunction(isolate, reaction->fulfill_handler(),
                                Builtin::kPromiseAllResolveElementClosure)) {
    Handle<JSFunction> function(JSFunction::cast(reaction->fulfill_handler()),
                                isolate);
    Handle<Context> context(function->context(), isolate);
    Handle<JSFunction> combinator(context->native_context()->promise_all(),
                                isolate);
    builder->AppendPromiseCombinatorFrame(function, combinator);

    // Теперь заглядываем в контекст resolve element функции Promise.all(),
    // чтобы найти promise capability, который разрешается, когда все
    // одновременные промисы будут разрешены.
    int const index =
        PromiseBuiltins::kPromiseAllResolveElementCapabilitySlot;
    Handle<PromiseCapability> capability(
        PromiseCapability::cast(context->get(index)), isolate);
    if (!IsJSPromise(capability->promise())) return;
    promise = handle(JSPromise::cast(capability->promise()), isolate);
} else if (

При просмотре цепочки промисов, если reaction->fulfill_handler является встроенной функцией "Promise.all Resolve Element Closure", она добавляет кадр асинхронного комбинатора промисов в стек ошибок. Затем она переходит к следующему промису, обращаясь к function->context->capability->promise.

Проблема в том, что функция предполагает, что "Promise.all Resolve Element Closure" ещё не была выполнена. Если эта функция уже выполнена, контекст меняется с FunctionContext на NativeContext. Это приводит к путанице типов между FunctionContext и NativeContext в функции CaptureAsyncStackTrace.

Создание PoC:

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

  1. Получить функцию "Promise.all Resolve Element Closure", которая является встроенной функцией.
  2. Явно вызвать функцию "Promise.all Resolve Element Closure", чтобы изменить контекст с FunctionContext на NativeContext.
  3. Установить функцию "Promise.all Resolve Element Closure" как обработчик выполнения промиса с новой цепочкой промисов.
  4. Вызвать ошибку в цепочке промисов и захватить асинхронный стек.

Для получения функции "Promise.all Resolve Element Closure" на уровне JS-скрипта я использовал синхронный паттерн разрешения промисов для Promise.all. Я позаимствовал этот паттерн из тестовых случаев test262.

После явного вызова функции, чтобы вызвать уязвимость, я использовал пример кода из документации по zero-cost async stack trace для подготовки новой цепочки промисов и установил встроенную функцию как обработчик выполнения одного из промисов.

Наконец, когда возникает ошибка, асинхронный стек захватывается с уже выполненной функцией "Promise.all Resolve Element Closure" в качестве обработчика выполнения, что приводит к путанице типов между FunctionContext и NativeContext.

Вот код PoC: poc.js

Эксплойт

(Термины «примитив эксплойта», «стратегия эксплойта», «техника эксплойта» и «поток эксплойта» определены здесь.)

Примитив эксплойта: примитив fakeobj

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

  1. Heap spray с объектами JSPromise для сопоставления случайного хеш-номера с корректным указателем на объект JSPromise.
  2. Использовать хеш-значение как корректный указатель на объект JSPromise и внедрить поддельный асинхронный кадр стека.
  3. Использовать Error.prepareStackTrace с методом getThis для получения поддельного объекта.

Ошибка приводит к путанице типов между FunctionContext и NativeContext в функции CaptureAsyncStackTrace. Она обращается к Context->PromiseCapability->JSPromise для построения следующего асинхронного кадра стека. При срабатывании ошибки она обращается к NativeContext->JSGlobalProxy->hash. Для эксплуатации ошибки я использовал хеш-значение как указатель на объект JSPromise.

Мы можем проверить, что хеш-значение находится в диапазоне (0, 0xfffff) из следующей функции генерации хеша:

root@kitploit:~
int Isolate::GenerateIdentityHash(uint32_t mask) {
  int hash;
  int attempts = 0;
  do {
    hash = random_number_generator()->NextInt() & mask;
  } while (hash == 0 && attempts++ < 30);
  return hash != 0 ? hash : 1;
}
root@kitploit:~
pwndbg> p/x mask
$1 = 0xfffff

Хеш-значение помечено как SMI, поэтому в памяти оно будет храниться как hash << 1. Следовательно, значение в памяти будет в диапазоне (0, 0xfffff << 1) и чётным.

Чтобы сопоставить случайный хеш-номер с корректным указателем на JSPromise, есть два ограничения:

  1. Интерпретируемый адрес указателя должен быть нечётным.
  2. Необходимо распылить кучу в диапазоне (0, 0xfffff << 1).

Следуя этим ограничениям, я распылил кучу объектами JSPromise со сдвигом влево на 8 бит, чтобы сделать адрес нечётным, и использовал маленькие циклы for, чтобы уместиться в диапазон (0, 0xfffff << 1).

Здесь сопоставление случайного хеш-номера с корректным указателем на объект выглядит как довольно низкая вероятность. Чтобы повысить надёжность, я использовал технику iframe. Страницы с разных веб-сайтов выполняются в разных процессах из-за изоляции сайтов в Chrome. Поэтому я создал iframe с другим доменом и запустил эксплойт в iframe, чтобы избежать краха основного процесса.

После перехода к следующему промису в цепочке программа проверяет корректность промиса и пытается добавить асинхронный кадр стека в соответствии с типом асинхронного вызова.

root@kitploit:~
  while (!builder->Full()) {
    // Проверяем, что {promise} не выполнен.
    if (promise->status() != Promise::kPending) return;

    // Проверяем, что у {promise} ровно одна PromiseReaction.
    if (!IsPromiseReaction(promise->reactions())) return;
    Handle<PromiseReaction> reaction(
        PromiseReaction::cast(promise->reactions()), isolate);
    if (!IsSmi(reaction->next())) return;

    // Проверяем, что {reaction} имеет одно из известных асинхронных
    // продолжений функции или асинхронного генератора в качестве обработчика выполнения.
    if (IsBuiltinFunction(isolate, reaction->fulfill_handler(),
                          Builtin::kAsyncFunctionAwaitResolveClosure) ||
        IsBuiltinFunction(isolate, reaction->fulfill_handler(),
                          Builtin::kAsyncGeneratorAwaitResolveClosure) ||
        IsBuiltinFunction(
            isolate, reaction->fulfill_handler(),
            Builtin::kAsyncGeneratorYieldWithAwaitResolveClosure)) {
      // Теперь заглядываем в AwaitContext обработчиков, чтобы получить
      // JSGeneratorObject для асинхронной функции.
      Handle<Context> context(
          JSFunction::cast(reaction->fulfill_handler())->context(), isolate);
      Handle<JSGeneratorObject> generator_object(
          JSGeneratorObject::cast(context->extension()), isolate);
      CHECK(generator_object->is_suspended());

      // Добавляем асинхронный кадр, соответствующий {generator_object}.
      builder->AppendAsyncFrame(generator_object);

Мы выбрали случай kAsyncFunctionAwaitResolveClosure, потому что параметр функции AppendAsyncFrame, generator_object, полностью контролируем.

Устанавливая соответствующие поддельные объекты, такие как PromiseReaction, Function, Context, JSGeneratorObject, чтобы пройти условия, мы можем внедрить наш поддельный асинхронный кадр, вызвав builder->AppendAsyncFrame(generator_object). Мы можем проверить внедрённый поддельный асинхронный кадр из терминала.

root@kitploit:~
Error: Let's have a look...
    at bar (../../../../fake_frame.js:168:15)
    at async foo (../../../../fake_frame.js:163:9)
    at async Promise.all (index 0)
    at async Array.sloppy_func (../../../../fake_frame.js:1:1)

Вот код fake_frame.js.

После внедрения поддельного асинхронного кадра я использовал Error.prepareStackTrace с методом getThis, чтобы получить receiver объекта ошибки (в данном случае это JSGeneratorObject). С помощью receiver можно извлечь поддельный объект из кучи (примитив fakeobj).

Поток эксплойта: Я использовал типичный поток эксплуатации для эксплойтов V8.

  1. Используя примитив fakeobj, я внедрил и извлёк поддельный массив OOB (out-of-bounds).
  2. Используя поддельный массив OOB, я построил примитивы caged_read/caged_write.
  3. Для достижения RCE я обратился к технике, опубликованной на Google CTF 2023. Чтобы выйти из песочницы V8, я повредил объект BytecodeArray для выполнения произвольного байт-кода. Используя инструкции Ldar/Star с доступом за пределами границ, можно читать/записывать стек. Чтобы утечки базовый адрес бинарника Chrome, я прочитал адрес возврата из стека и утёк младшие 32 бита базового адреса, а также прочитал указатель на кучу libc, чтобы получить старшие 16 бит адреса. Затем я повредил указатель кадра для переключения стека и выполнил ROP-цепочку для достижения RCE.

Вот полный код эксплойта: index.html и exploit.html Он протестирован на Chrome 118.0.5993.70, который был целевой версией v8CTF M118.

Благодарности

Хэин Ли (Haein Lee) из KAIST Hacking Lab

Скачать инструмент