Skip to content
KitploitKITPLOIT
StrumentiBlog
Invia
StrumentiBlog
Invia

Strumenti di Hacking, PenTest e Cybersecurity per il tuo Arsenale di Sicurezza!

Kitploit è una directory di strumenti di hacking, cybersecurity e pentesting. Scopri gli ultimi aggiornamenti dei progetti per trovare vulnerabilità, analizzare sistemi, automatizzare i test e rafforzare la tua sicurezza.

··Feed·Contatto·Privacy·© 2026 Kitploit

Directory degli strumenti

Categorie

Vedi tutte le categorie
Loading categories
CVE-2023-6702 — Chrome Renderer 1day RCE tramite Type Confusion nello Stack Trace Asincrono (v8ctf submission) | Kitploit
Strumenti/GitHubGitHub/kaist-hacking/cve-2023-6702
Analisi delle VulnerabilitàExploitSfruttamento di Applicazioni WebCTFApprendimento e FormazioneBinary Exploitation
GitHubkaist-hacking/cve-2023-6702

CVE-2023-6702

Chrome Renderer 1day RCE tramite Type Confusion nello Stack Trace Asincrono (v8ctf submission)

Vedi Repository
8692 anni faRevisionato da Kitploit

Più Popolari

Vedi tutti →

Scopri gli strumenti più utilizzati dalla nostra community.

Esplora tutti gli strumenti

Sfoglia la nostra collezione di strumenti

Vedi tutti gli strumenti →
Condividi

Chrome Renderer 1day RCE tramite Confusione di Tipo in Async Stack Trace (CVE-2023-6702)

Riepilogo

Questa vulnerabilità permetteva a un attaccante remoto di eseguire codice arbitrario all'interno del processo renderer di Chrome.

C'era un controllo di tipo insufficiente nel codice di gestione dello stack trace asincrono. Porta a una confusione di tipo tra FunctionContext e NativeContext, causando un accesso illegale al valore JSGlobalProxy->hash. Con l'heap spraying, l'attaccante è stato in grado di iniettare un finto frame dello stack asincrono e costruire la primitiva fakeobj. Usando la primitiva fakeobj, l'attaccante è riuscito a ottenere l'esecuzione di codice arbitrario nel processo renderer di Chrome.

Puoi consultare le nostre slide di TyphoonCon 2024.

Vendor / Prodotto / Versione

  • Google Chrome
  • Versioni Affette: pre 120.0.6099.109
  • Versione Corretta: 120.0.6099.109

Cronologia

  • 2020-05-13: Bug Introdotto - [Promise.any] Implementazione degli stack trace asincroni per Promise.any
  • 2023-11-10: Segnalazione Bug - Security: V8 Debug check failed: LAST_TYPE >= value
  • 2023-11-15: Patch - [promises, async stack traces] Correggi il caso in cui la closure è stata eseguita
  • 2023-12-12: Advisory - https://chromereleases.googleblog.com/2023/12/stable-channel-update-for-desktop_12.html
  • 2024-01-12: Sottomissione v8CTF <-- Il periodo in cui abbiamo lavorato su questa vulnerabilità
  • 2024-02-23: Segnalazione bug divulgata

Contesto

Async Stack Trace

L'asincronia è una delle caratteristiche più importanti in JavaScript. In passato, era difficile eseguire il debug del codice asincrono con lo stack di errori perché le funzioni asincrone non venivano catturate nello stack di errori. Le funzioni asincrone sospese sono memorizzate nella coda di callback del ciclo di eventi, non nello stack di chiamate, quindi lo stack di errori non contiene la funzione asincrona. Per risolvere questo problema, V8 fornisce la funzionalità "async stack trace" (per impostazione predefinita da V8 v7.3) per catturare la funzione asincrona nello stack di errori. (v8 blog, v8 docs)

Promise.all Resolve Element Closure

"Promise.all Resolve Element Closure" è una funzione helper per risolvere le promise di input nella funzione Promise.all. La funzione Promise.all prende un array di promise e restituisce una promise che si risolve quando tutte le promise di input sono risolte. "Promise.all Resolve Element Closure" è un gestore di risoluzione per ogni promise di input nella funzione Promise.all. Il ruolo della funzione è risolvere la promise di input e memorizzare il valore di completamento nell'array dei risultati.

Ci sono 2 punti da notare riguardo alla funzione:

  1. È una funzione builtin intrinseca e non è direttamente accessibile dal codice JavaScript.
  2. Il contesto della funzione è usato come marcatore per verificare se la funzione è stata eseguita o meno. Ha FunctionContext fino a quando non viene chiamata, e poi ha NativeContext dopo essere stata chiamata. (v8 code)

La Vulnerabilità

Classe di bug: Confusione di tipo tra FunctionContext e NativeContext

Dettagli della vulnerabilità:

La vulnerabilità può essere attivata catturando uno stack trace asincrono con la funzione "Promise.all Resolve Element Closure" già eseguita o funzioni builtin intrinseche simili. In questo exploit, ho usato la funzione "Promise.all Resolve Element Closure" come esempio.

Quando un errore viene lanciato nel codice JavaScript, V8 cattura lo stack di errori dallo stack e aggiunge i frame dello stack asincrono dal microtask corrente [1].

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

// If --async-stack-traces are enabled and the "current microtask" is a
// PromiseReactionJobTask, we try to enrich the stack trace with async
// frames.
if (v8_flags.async_stack_traces) {
    CaptureAsyncStackTrace(isolate, &builder);
}

La funzione CaptureAsyncStackTrace [2] cerca nella catena di promise e aggiunge il frame dello stack asincrono in base al tipo di chiamata asincrona (es., await, Promise.all, Promise.any).

Di seguito è riportato lo snippet della funzione CaptureAsyncStackTrace che gestisce il caso 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);

    // Now peak into the Promise.all() resolve element context to
    // find the promise capability that's being resolved when all
    // the concurrent promises resolve.
    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 (

Durante la ricerca nella catena di promise, se reaction->fulfill_handler è la funzione builtin "Promise.all Resolve Element Closure", aggiunge il frame combinatorio della promise asincrona allo stack di errori. Poi, passa alla promise successiva accedendo a function->context->capability->promise.

Il problema è che la funzione presuppone che la funzione "Promise.all Resolve Element Closure" non sia stata ancora eseguita. Se la funzione "Promise.all Resolve Element Closure" è già stata eseguita, il contesto viene cambiato da FunctionContext a NativeContext. Porta a una confusione di tipo tra FunctionContext e NativeContext nella funzione CaptureAsyncStackTrace.

Realizzazione del PoC:

La strategia per attivare la vulnerabilità è la seguente:

  1. Ottenere la funzione "Promise.all Resolve Element Closure" che è una funzione builtin intrinseca.
  2. Chiamare esplicitamente la funzione "Promise.all Resolve Element Closure" per cambiare il contesto da FunctionContext a NativeContext.
  3. Impostare la funzione "Promise.all Resolve Element Closure" come gestore di completamento di una promise con una nuova catena di promise.
  4. Lanciare un errore nella catena di promise e catturare lo stack trace asincrono.

Ho usato il pattern di risoluzione sincrona delle promise per Promise.all per ottenere la funzione "Promise.all Resolve Element Closure" a livello di script JS. Ho preso in prestito il pattern dai casi di test test262.

Dopo aver chiamato esplicitamente la funzione, per attivare la vulnerabilità, ho usato il codice di esempio nel documento sullo stack trace asincrono a costo zero per preparare una nuova catena di promise e impostare la funzione builtin intrinseca come gestore di completamento di una delle promise.

Infine, quando l'errore viene lanciato, lo stack trace asincrono viene catturato con la funzione "Promise.all Resolve Element Closure" già eseguita come gestore di completamento, portando a una confusione di tipo tra FunctionContext e NativeContext.

Ecco il codice del PoC: poc.js

L'Exploit

(I termini primitiva di exploit, strategia di exploit, tecnica di exploit e flusso di exploit sono definiti qui.)

Primitiva di exploit: primitiva fakeobj

Strategia di exploit: Per costruire la primitiva fakeobj dal bug di confusione di tipo, ho usato la seguente strategia:

  1. Heap spray con oggetti JSPromise per far corrispondere il numero hash casuale a un puntatore valido di oggetto JSPromise.
  2. Usare il valore hash come puntatore valido di oggetto JSPromise e iniettare il finto frame dello stack asincrono.
  3. Usare Error.prepareStackTrace con il metodo getThis per recuperare l'oggetto finto.

Il bug porta a una confusione di tipo tra FunctionContext e NativeContext nella funzione CaptureAsyncStackTrace. Accede a Context->PromiseCapability->JSPromise per costruire il prossimo frame dello stack asincrono. Quando il bug viene attivato, accede a NativeContext->JSGlobalProxy->hash. Per sfruttare il bug, ho usato il valore hash come puntatore di oggetto JSPromise.

Possiamo verificare che il valore hash ha un intervallo di (0, 0xfffff) dalla seguente funzione di generazione hash:

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

Il valore hash è con tag SMI, quindi in memoria, sarà memorizzato come hash << 1. Quindi, il valore in memoria sarà nell'intervallo di (0, 0xfffff << 1) con numeri pari.

Per far corrispondere il numero hash casuale a un puntatore valido di oggetto JSPromise, abbiamo 2 vincoli:

  1. L'indirizzo del puntatore interpretato dovrebbe essere un numero dispari.
  2. Dobbiamo fare heap spray nell'intervallo (0, 0xfffff << 1).

Seguendo i vincoli, ho fatto heap spray con oggetti JSPromise con uno shift a sinistra di 8 bit per rendere l'indirizzo dispari, e ho usato piccoli cicli for per rientrare nell'intervallo (0, 0xfffff << 1).

Qui far corrispondere il numero hash casuale a un puntatore valido di oggetto sembra avere una probabilità piuttosto bassa. Per aumentare l'affidabilità, ho usato la tecnica iframe. Le pagine di siti web diversi vengono eseguite in processi diversi a causa dell'isolamento dei siti in Chrome. Così, ho creato un iframe con un dominio diverso e ho eseguito l'exploit nell'iframe per evitare il crash del processo principale.

Dopo essere passato alla promise successiva nella catena di promise, il programma verifica la validità della promise e tenta di aggiungere il frame dello stack asincrono in base al tipo di chiamata asincrona.

root@kitploit:~
  while (!builder->Full()) {
    // Check that the {promise} is not settled.
    if (promise->status() != Promise::kPending) return;

    // Check that we have exactly one PromiseReaction on the {promise}.
    if (!IsPromiseReaction(promise->reactions())) return;
    Handle<PromiseReaction> reaction(
        PromiseReaction::cast(promise->reactions()), isolate);
    if (!IsSmi(reaction->next())) return;

    // Check if the {reaction} has one of the known async function or
    // async generator continuations as its fulfill handler.
    if (IsBuiltinFunction(isolate, reaction->fulfill_handler(),
                          Builtin::kAsyncFunctionAwaitResolveClosure) ||
        IsBuiltinFunction(isolate, reaction->fulfill_handler(),
                          Builtin::kAsyncGeneratorAwaitResolveClosure) ||
        IsBuiltinFunction(
            isolate, reaction->fulfill_handler(),
            Builtin::kAsyncGeneratorYieldWithAwaitResolveClosure)) {
      // Now peek into the handlers' AwaitContext to get to
      // the JSGeneratorObject for the async function.
      Handle<Context> context(
          JSFunction::cast(reaction->fulfill_handler())->context(), isolate);
      Handle<JSGeneratorObject> generator_object(
          JSGeneratorObject::cast(context->extension()), isolate);
      CHECK(generator_object->is_suspended());

      // Append async frame corresponding to the {generator_object}.
      builder->AppendAsyncFrame(generator_object);

Abbiamo scelto il caso kAsyncFunctionAwaitResolveClosure perché il parametro della funzione AppendAsyncFrame, generator_object, è completamente controllabile.

Impostando oggetti finti appropriati come PromiseReaction, Function, Context, JSGeneratorObject per superare le condizioni, possiamo iniettare il nostro finto frame asincrono chiamando builder->AppendAsyncFrame(generator_object). Possiamo verificare il finto frame asincrono iniettato dal terminale.

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)

Ecco il codice fake_frame.js.

Dopo aver iniettato il finto frame asincrono, ho usato Error.prepareStackTrace con il metodo getThis per ottenere il receiver dell'oggetto errore (in questo caso, è JSGeneratorObject). Con il receiver, possiamo recuperare l'oggetto finto dall'heap (primitiva fakeobj).

Flusso dell'exploit: Ho usato il flusso tipico di exploit per gli exploit V8.

  1. Usando la primitiva fakeobj, ho piantato e recuperato il finto array OOB.
  2. Usando il finto array OOB, ho costruito le primitive caged_read/caged_write.
  3. Verso la RCE, mi sono riferito alla tecnica condivisa dal Google CTF 2023. Per evadere dalla sandbox di V8, ho corrotto l'oggetto BytecodeArray per eseguire bytecode arbitrario. Usando le istruzioni Ldar/Star con accesso out-of-bounds, possiamo leggere/scrivere lo stack. Per fare leak dell'indirizzo base del binario di Chrome, ho letto un indirizzo di ritorno dallo stack per fare leak dei 32 bit inferiori dell'indirizzo base, e ho letto un puntatore heap di libc per ottenere i 16 bit superiori dell'indirizzo. Poi, ho corrotto il frame pointer per lo stack pivoting ed eseguito la catena ROP per ottenere RCE.

Ecco il codice completo dell'exploit: index.html e exploit.html È stato testato su Chrome 118.0.5993.70 che era la versione target del v8CTF M118.

Crediti

Haein Lee del KAIST Hacking Lab

Scarica lo strumento