Skip to content
KitploitKITPLOIT
ToolsBlog
Einreichen
ToolsBlog
Einreichen

Hacking-, PenTest- und Cybersicherheits-Tools für Ihr Sicherheitsarsenal!

Kitploit ist ein Verzeichnis von Hacking-, Cybersicherheits- und Pentesting-Tools. Entdecken Sie die neuesten Projekt-Updates, um Schwachstellen zu finden, Systeme zu analysieren, Tests zu automatisieren und Ihre Sicherheit zu stärken.

··Feeds·Kontakt·Datenschutz·© 2026 Kitploit

Tool-Verzeichnis

Kategorien

Alle Kategorien anzeigen
Loading categories
CVE-2023-6702 — Chrome Renderer 1day RCE durch Type Confusion im Async Stack Trace (v8ctf-Einreichung) | Kitploit
Tools/GitHubGitHub/kaist-hacking/cve-2023-6702
SchwachstellenanalyseExploitationWebanwendungs-ExploitationCTFLernen & BildungBinary-Exploitation
GitHubkaist-hacking/cve-2023-6702

CVE-2023-6702

Chrome Renderer 1day RCE durch Type Confusion im Async Stack Trace (v8ctf-Einreichung)

Repository anzeigen
869vor 2 JahrenVon Kitploit geprüft

Beliebteste

Alle anzeigen →

Entdecken Sie die meistgenutzten Tools unserer Community.

Alle Tools erkunden

Durchsuchen Sie unsere Tool-Sammlung

Alle Tools anzeigen →
Teilen

Chrome Renderer 1-Day-RCE durch Typverwechslung im asynchronen Stack-Trace (CVE-2023-6702)

Zusammenfassung

Diese Schwachstelle erlaubte es einem entfernten Angreifer, beliebigen Code im Chrome-Renderer-Prozess auszuführen.

In der Verarbeitung der asynchronen Stack-Traces fehlte eine ausreichende Typprüfung. Dies führt zu einer Typverwechslung zwischen FunctionContext und NativeContext, die einen unerlaubten Zugriff auf den JSGlobalProxy->hash-Wert verursacht. Durch Heap-Spraying konnte der Angreifer einen gefälschten asynchronen Stack-Frame einschleusen und die fakeobj-Primitive konstruieren. Mit der fakeobj-Primitive konnte der Angreifer beliebigen Code im Chrome-Renderer-Prozess ausführen.

Du kannst dir unsere TyphoonCon-2024-Folien ansehen.

Hersteller / Produkt / Version

  • Google Chrome
  • Betroffene Versionen: vor 120.0.6099.109
  • Behobene Version: 120.0.6099.109

Zeitleiste

  • 2020-05-13: Bug eingeführt - [Promise.any] Implment async stack traces for Promise.any
  • 2023-11-10: Bug-Report - Security: V8 Debug check failed: LAST_TYPE >= value
  • 2023-11-15: Patch - [promises, async stack traces] Fix the case when the closure has run
  • 2023-12-12: Sicherheitshinweis - https://chromereleases.googleblog.com/2023/12/stable-channel-update-for-desktop_12.html
  • 2024-01-12: v8CTF-Einreichung <-- Der Zeitpunkt, an dem wir an dieser Schwachstelle gearbeitet haben
  • 2024-02-23: Bug-Report offengelegt

Hintergrund

Asynchroner Stack-Trace

Asynchrone Programmierung ist eines der wichtigsten Features in JavaScript. Früher war es schwierig, asynchronen Code mit dem Fehler-Stack zu debuggen, da asynchrone Funktionen nicht im Fehler-Stack erfasst wurden. Suspendierte asynchrone Funktionen werden in der Callback-Queue der Ereignisschleife gespeichert, nicht im Aufrufstapel, daher enthält der Fehler-Stack die asynchrone Funktion nicht. Um dieses Problem zu lösen, bietet V8 die Funktion „async stack trace“ (standardmäßig seit V8 v7.3), um asynchrone Funktionen im Fehler-Stack zu erfassen. (v8-Blog, v8-Dokumentation)

Promise.all Resolve Element Closure

„Promise.all Resolve Element Closure“ ist eine Hilfsfunktion zum Auflösen der eingehenden Promises in der Promise.all-Funktion. Die Promise.all-Funktion nimmt ein Array von Promises entgegen und gibt ein Promise zurück, das aufgelöst wird, wenn alle eingegebenen Promises aufgelöst sind. „Promise.all Resolve Element Closure“ ist ein Resolve-Handler für jedes eingehende Promise in der Promise.all-Funktion. Die Funktion hat die Aufgabe, das eingehende Promise aufzulösen und den Erfüllungswert im Ergebnis-Array zu speichern.

Es gibt 2 Punkte, die man bei dieser Funktion beachten sollte:

  1. Es ist eine intrinsische Built-in-Funktion und aus dem JavaScript-Code nicht direkt zugänglich.
  2. Der Kontext der Funktion wird als Marker verwendet, um zu prüfen, ob die Funktion bereits ausgeführt wurde. Sie hat FunctionContext, bis sie aufgerufen wurde, und nach dem Aufruf hat sie NativeContext. (v8-Code)

Die Schwachstelle

Bug-Klasse: Typverwechslung zwischen FunctionContext und NativeContext

Details zur Schwachstelle:

Die Schwachstelle kann ausgelöst werden, indem ein asynchroner Stack-Trace mit der bereits ausgeführten Funktion „Promise.all Resolve Element Closure“ oder ähnlichen intrinsischen Built-in-Funktionen erfasst wird. In diesem Exploit habe ich die Funktion „Promise.all Resolve Element Closure“ als Beispiel verwendet.

Wenn im JavaScript-Code ein Fehler ausgelöst wird, erfasst V8 den Fehler-Stack aus dem Aufrufstapel und hängt die asynchronen Stack-Frames aus der aktuellen Mikrotask an [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);
}

Die Funktion CaptureAsyncStackTrace [2] durchsucht die Promise-Kette und hängt den asynchronen Stack-Frame gemäß dem asynchronen Aufruftyp an (z. B. await, Promise.all, Promise.any).

Unten ist der Ausschnitt der Funktion CaptureAsyncStackTrace, der den Fall Promise.all behandelt:

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 (

Wenn beim Durchsuchen der Promise-Kette reaction->fulfill_handler die Built-in-Funktion „Promise.all Resolve Element Closure“ ist, wird der asynchrone Promise-Combinator-Frame an den Fehler-Stack angehängt. Danach wechselt es zum nächsten Promise, indem es auf function->context->capability->promise zugreift.

Das Problem ist, dass die Funktion davon ausgeht, dass die „Promise.all Resolve Element Closure“-Funktion noch nicht ausgeführt wurde. Wenn die „Promise.all Resolve Element Closure“-Funktion bereits ausgeführt wurde, ändert sich der Kontext von FunctionContext zu NativeContext. Das führt zu einer Typverwechslung zwischen FunctionContext und NativeContext in der Funktion CaptureAsyncStackTrace.

Erstellung des PoC:

Die Strategie zum Auslösen der Schwachstelle ist wie folgt:

  1. Die Funktion „Promise.all Resolve Element Closure“ besorgen, eine intrinsische Built-in-Funktion.
  2. Die Funktion „Promise.all Resolve Element Closure“ explizit aufrufen, um den Kontext von FunctionContext zu NativeContext zu ändern.
  3. Die Funktion „Promise.all Resolve Element Closure“ als Fulfill-Handler eines Promises mit einer neuen Promise-Kette festlegen.
  4. Einen Fehler in der Promise-Kette auslösen und den asynchronen Stack-Trace erfassen.

Ich habe das synchrone Promise-Auflösungsmuster für Promise.all verwendet, um die Funktion „Promise.all Resolve Element Closure“ auf JS-Skriptebene zu erhalten. Das Muster habe ich aus den test262-Testfällen übernommen.

Nachdem ich die Funktion explizit aufgerufen hatte, habe ich zum Auslösen der Schwachstelle den Beispielcode aus der Dokumentation zu Zero-Cost-Async-Stack-Traces verwendet, um eine neue Promise-Kette vorzubereiten, und die intrinsische Built-in-Funktion als Fulfill-Handler eines der Promises festgelegt.

Wenn schließlich der Fehler ausgelöst wird, wird der asynchrone Stack-Trace mit der bereits ausgeführten Funktion „Promise.all Resolve Element Closure“ als Fulfill-Handler erfasst, was zu einer Typverwechslung zwischen FunctionContext und NativeContext führt.

Hier ist der PoC-Code: poc.js

Der Exploit

(Die Begriffe Exploit-Primitive, Exploit-Strategie, Exploit-Technik und Exploit-Ablauf werden hier definiert.)

Exploit-Primitive: fakeobj-Primitive

Exploit-Strategie: Um aus dem Typverwechslungs-Bug eine fakeobj-Primitive zu bauen, habe ich die folgende Strategie verwendet:

  1. Heap-Spraying mit JSPromise-Objekten, um die zufällige Hash-Nummer auf einen gültigen JSPromise-Objektzeiger abzubilden.
  2. Den Hash-Wert als gültigen JSPromise-Objektzeiger verwenden und den gefälschten asynchronen Stack-Frame injizieren.
  3. Error.prepareStackTrace mit der getThis-Methode verwenden, um das gefälschte Objekt abzurufen.

Der Bug führt zu einer Typverwechslung zwischen FunctionContext und NativeContext in der Funktion CaptureAsyncStackTrace. Sie greift auf Context->PromiseCapability->JSPromise zu, um den nächsten asynchronen Stack-Frame zu erstellen. Wenn der Bug ausgelöst wird, greift sie auf NativeContext->JSGlobalProxy->hash zu. Um den Bug auszunutzen, habe ich den Hash-Wert als JSPromise-Objektzeiger verwendet.

Wir können der folgenden Hash-Erzeugungsfunktion entnehmen, dass der Hash-Wert einen Bereich von (0, 0xfffff) hat:

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

Der Hash-Wert ist SMI-getaggt, daher wird er im Speicher als hash << 1 gespeichert. Folglich liegt der Wert im Speicher mit einer geraden Zahl im Bereich von (0, 0xfffff << 1).

Um die zufällige Hash-Nummer auf einen gültigen JSPromise-Objektzeiger abzubilden, haben wir 2 Randbedingungen:

  1. Die interpretierte Zeigeradresse sollte eine ungerade Zahl sein.
  2. Wir müssen den Heap im Bereich (0, 0xfffff << 1) besprühen.

Unter Beachtung dieser Randbedingungen habe ich den Heap mit JSPromise-Objekten besprüht, die um 8 Bit nach links verschoben wurden, um die Adresse ungerade zu machen, und kleine for-Schleifen verwendet, um in den Bereich (0, 0xfffff << 1) zu passen.

Das Abgleichen der zufälligen Hash-Nummer mit einem gültigen Objektzeiger scheint hier eine ziemlich geringe Chance zu haben. Um die Zuverlässigkeit zu erhöhen, habe ich die iframe-Technik verwendet. Seiten von verschiedenen Websites laufen aufgrund der Site-Isolation in Chrome in verschiedenen Prozessen. Also habe ich ein iframe mit einer anderen Domain erstellt und den Exploit im iframe ausgeführt, um den Absturz des Hauptprozesses zu vermeiden.

Nach dem Wechsel zum nächsten Promise in der Promise-Kette prüft das Programm die Gültigkeit des Promises und versucht, den asynchronen Stack-Frame gemäß dem asynchronen Aufruftyp anzuhängen.

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);

Wir haben den Fall kAsyncFunctionAwaitResolveClosure gewählt, weil der Parameter der Funktion AppendAsyncFrame, generator_object, vollständig kontrollierbar ist.

Indem wir passende gefälschte Objekte wie PromiseReaction, Function, Context, JSGeneratorObject setzen, um die Bedingungen zu erfüllen, können wir unseren gefälschten asynchronen Frame durch den Aufruf von builder->AppendAsyncFrame(generator_object) injizieren. Wir können den injizierten gefälschten asynchronen Frame vom Terminal aus überprüfen.

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)

Hier ist der Code von fake_frame.js.

Nach dem Injizieren des gefälschten asynchronen Frames habe ich Error.prepareStackTrace mit der getThis-Methode verwendet, um den receiver des Fehlerobjekts zu erhalten (in diesem Fall ist es JSGeneratorObject). Mit dem receiver können wir das gefälschte Objekt aus dem Heap abrufen (fakeobj-Primitive).

Exploit-Ablauf: Ich habe den typischen Exploit-Ablauf für V8-Exploits verwendet.

  1. Mit der fakeobj-Primitive habe ich ein gefälschtes OOB-Array platziert und abgerufen.
  2. Mit dem gefälschten OOB-Array habe ich caged_read/caged_write-Primitives konstruiert.
  3. Für den RCE habe ich auf die Technik zurückgegriffen, die beim Google CTF 2023 geteilt wurde. Um die V8-Sandbox zu verlassen, habe ich das BytecodeArray-Objekt korrumpiert, um beliebigen Bytecode auszuführen. Mit Ldar/Star-Instruktionen und Out-of-Bounds-Zugriff können wir den Stack lesen/schreiben. Um die Basisadresse der Chrome-Binary zu leaken, habe ich eine Rücksprungadresse vom Stack gelesen, um die unteren 32 Bits der Basisadresse zu leaken, und einen libc-Heap-Pointer gelesen, um die oberen 16 Bits der Adresse zu erhalten. Dann habe ich den Frame-Pointer für Stack-Pivoting korrumpiert und die ROP-Kette ausgeführt, um RCE zu erreichen.

Hier ist der vollständige Exploit-Code: index.html und exploit.html Er wurde auf Chrome 118.0.5993.70 getestet, der Zielversion des v8CTF M118.

Danksagungen

Haein Lee vom KAIST Hacking Lab

Tool herunterladen