Skip to content
KitploitKITPLOIT
OutilsBlog
Soumettre
OutilsBlog
Soumettre

Outils de Hacking, PenTest et Cybersécurité pour votre Arsenal de Sécurité !

Kitploit est un répertoire d'outils de hacking, de cybersécurité et de pentesting. Découvrez les dernières mises à jour des projets pour trouver des vulnérabilités, analyser des systèmes, automatiser les tests et renforcer votre sécurité.

··Flux·Contact·Confidentialité·© 2026 Kitploit

Répertoire d'outils

Catégories

Voir toutes les catégories
Loading categories
CVE-2023-6702 — Chrome Renderer RCE 1-day via confusion de type dans la trace de pile asynchrone (soumission v8ctf) | Kitploit
Outils/GitHubGitHub/kaist-hacking/cve-2023-6702
Analyse des VulnérabilitésExploitationExploitation d'Applications WebCTFApprentissage et ÉducationExploitation de Binaires
GitHubkaist-hacking/cve-2023-6702

CVE-2023-6702

Chrome Renderer RCE 1-day via confusion de type dans la trace de pile asynchrone (soumission v8ctf)

Voir le dépôt
8692il y a 2 ansVérifié par Kitploit

Populaires

Voir tout →

Découvrez les outils les plus utilisés par notre communauté.

Explorer tous les outils

Parcourez notre collection d'outils

Voir tous les outils →
Partager

RCE 1day du Renderer Chrome via une confusion de type dans la trace de pile asynchrone (CVE-2023-6702)

Résumé

Cette vulnérabilité permettait à un attaquant distant d'exécuter du code arbitraire dans le processus de rendu Chrome.

Il y avait une vérification de type insuffisante dans le code de gestion des traces de pile asynchrones. Cela conduit à une confusion de type entre FunctionContext et NativeContext, provoquant un accès illégal à la valeur JSGlobalProxy->hash. Avec du heap spraying, l'attaquant a pu injecter une fausse trame de pile asynchrone et construire le primitif fakeobj. En utilisant le primitif fakeobj, l'attaquant a pu réaliser une exécution de code arbitraire dans le processus de rendu Chrome.

Vous pouvez consulter nos slides TyphoonCon 2024.

Fournisseur / Produit / Version

  • Google Chrome
  • Versions affectées : pre 120.0.6099.109
  • Version corrigée : 120.0.6099.109

Chronologie

  • 2020-05-13 : Bogue introduit - [Promise.any] Implémenter les traces de pile asynchrones pour Promise.any
  • 2023-11-10 : Rapport de bogue - Security: V8 Debug check failed: LAST_TYPE >= value
  • 2023-11-15 : Correctif - [promises, async stack traces] Fix the case when the closure has run
  • 2023-12-12 : Avis - https://chromereleases.googleblog.com/2023/12/stable-channel-update-for-desktop_12.html
  • 2024-01-12 : Soumission v8CTF <-- La période où nous avons travaillé sur cette vulnérabilité
  • 2024-02-23 : Rapport de bogue divulgué

Contexte

Trace de pile asynchrone

L'asynchrone est l'une des fonctionnalités les plus importantes de JavaScript. Dans le passé, il était difficile de déboguer du code asynchrone avec la pile d'erreur car les fonctions asynchrones ne sont pas capturées dans la pile d'erreur. Les fonctions asynchrones suspendues sont stockées dans la file d'attente de rappels de la boucle d'événements, pas dans la pile d'appels, donc la pile d'erreur ne contient pas la fonction asynchrone. Pour résoudre ce problème, V8 fournit la fonctionnalité "trace de pile asynchrone" (par défaut depuis V8 v7.3) pour capturer les fonctions asynchrones dans la pile d'erreur. (v8 blog, v8 docs)

Fermeture de résolution d'élément Promise.all

"Fermeture de résolution d'élément Promise.all" est une fonction auxiliaire pour résoudre les promesses d'entrée dans la fonction Promise.all. La fonction Promise.all prend un tableau de promesses et retourne une promesse qui se résout lorsque toutes les promesses d'entrée sont résolues. "Fermeture de résolution d'élément Promise.all" est un gestionnaire de résolution de chaque promesse d'entrée dans la fonction Promise.all. Son rôle est de résoudre la promesse d'entrée et de stocker la valeur d'accomplissement dans le tableau de résultats.

Il y a 2 points à noter à propos de cette fonction :

  1. C'est une fonction native intrinsèque et elle n'est pas directement accessible depuis le code JavaScript.
  2. Le contexte de la fonction est utilisé comme marqueur pour vérifier si la fonction a déjà été exécutée ou non. Elle a FunctionContext jusqu'à ce qu'elle soit appelée, puis elle a NativeContext après avoir été appelée. (code v8)

La vulnérabilité

Classe de bogue : Confusion de type entre FunctionContext et NativeContext

Détails de la vulnérabilité :

La vulnérabilité peut être déclenchée en capturant une trace de pile asynchrone avec la fonction "Fermeture de résolution d'élément Promise.all" déjà exécutée ou une fonction native intrinsèque similaire. Dans cet exploit, j'ai utilisé la fonction "Fermeture de résolution d'élément Promise.all" comme exemple.

Lorsqu'une erreur est levée dans le code JavaScript, V8 capture la pile d'erreur depuis la pile et ajoute les trames de pile asynchrones depuis la microtâche courante [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 fonction CaptureAsyncStackTrace [2] parcourt la chaîne de promesses et ajoute la trame de pile asynchrone en fonction du type d'appel asynchrone (par exemple, await, Promise.all, Promise.any).

Voici l'extrait de la fonction CaptureAsyncStackTrace qui gère le cas 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 (

En parcourant la chaîne de promesses, si reaction->fulfill_handler est la fonction native "Fermeture de résolution d'élément Promise.all", elle ajoute la trame de combinaison de promesses asynchrones à la pile d'erreur. Ensuite, elle passe à la promesse suivante en accédant à function->context->capability->promise.

Le problème est que la fonction suppose que la fonction "Fermeture de résolution d'élément Promise.all" n'a pas encore été exécutée. Si la fonction "Fermeture de résolution d'élément Promise.all" a déjà été exécutée, le contexte passe de FunctionContext à NativeContext. Cela conduit à une confusion de type entre FunctionContext et NativeContext dans la fonction CaptureAsyncStackTrace.

Réalisation du PoC :

La stratégie pour déclencher la vulnérabilité est la suivante :

  1. Obtenir la fonction "Fermeture de résolution d'élément Promise.all" qui est une fonction native intrinsèque.
  2. Appeler explicitement la fonction "Fermeture de résolution d'élément Promise.all" pour changer le contexte de FunctionContext à NativeContext.
  3. Définir la fonction "Fermeture de résolution d'élément Promise.all" comme gestionnaire d'accomplissement d'une promesse avec une nouvelle chaîne de promesses.
  4. Lever une erreur dans la chaîne de promesses et capturer la trace de pile asynchrone.

J'ai utilisé le motif de résolution synchrone de promesses pour Promise.all afin d'obtenir la fonction "Fermeture de résolution d'élément Promise.all" au niveau du script JS. J'ai emprunté le motif des cas de test test262.

Après avoir appelé explicitement la fonction, pour déclencher la vulnérabilité, j'ai utilisé l'exemple de code dans le document sur la trace de pile asynchrone à coût zéro pour préparer une nouvelle chaîne de promesses et définir la fonction native intrinsèque comme gestionnaire d'accomplissement de l'une des promesses.

Enfin, lorsque l'erreur est levée, la trace de pile asynchrone est capturée avec la fonction "Fermeture de résolution d'élément Promise.all" déjà exécutée comme gestionnaire d'accomplissement, conduisant à une confusion de type entre FunctionContext et NativeContext.

Voici le code du PoC : poc.js

L'exploit

(Les termes primitif d'exploitation, stratégie d'exploitation, technique d'exploitation et flux d'exploitation sont définis ici.)

Primitif d'exploitation : Primitif fakeobj

Stratégie d'exploitation : Pour construire le primitif fakeobj à partir du bogue de confusion de type, j'ai utilisé la stratégie suivante :

  1. Heap spray avec des objets JSPromise pour faire correspondre le nombre de hachage aléatoire à un pointeur d'objet JSPromise valide.
  2. Utiliser la valeur de hachage comme pointeur d'objet JSPromise valide et injecter la fausse trame de pile asynchrone.
  3. Utiliser Error.prepareStackTrace avec la méthode getThis pour récupérer le faux objet.

Le bogue conduit à une confusion de type entre FunctionContext et NativeContext dans la fonction CaptureAsyncStackTrace. Il accède à Context->PromiseCapability->JSPromise pour construire la trame de pile asynchrone suivante. Lorsque le bogue est déclenché, il accède à NativeContext->JSGlobalProxy->hash. Pour exploiter le bogue, j'ai utilisé la valeur de hachage comme pointeur d'objet JSPromise.

Nous pouvons vérifier que la valeur de hachage a une plage de (0, 0xfffff) à partir de la fonction de génération de hachage suivante :

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

La valeur de hachage est marquée SMI, donc en mémoire, elle sera stockée comme hash << 1. Ainsi, la valeur en mémoire sera dans la plage (0, 0xfffff << 1) avec un nombre pair.

Pour faire correspondre le nombre de hachage aléatoire à un pointeur d'objet JSPromise valide, nous avons 2 contraintes :

  1. L'adresse du pointeur interprétée doit être un nombre impair.
  2. Nous devons sprayer le heap dans la plage (0, 0xfffff << 1).

En suivant les contraintes, j'ai sprayé le heap avec des objets JSPromise avec un décalage de 8 bits vers la gauche pour rendre l'adresse impaire, et j'ai utilisé de petites boucles for pour rentrer dans la plage (0, 0xfffff << 1).

Ici, faire correspondre le nombre de hachage aléatoire à un pointeur d'objet valide semble avoir une faible probabilité. Pour augmenter la fiabilité, j'ai utilisé la technique iframe. Les pages de différents sites web s'exécutent dans des processus différents grâce à l'isolation de site dans Chrome. J'ai donc créé une iframe avec un domaine différent, et j'ai exécuté l'exploit dans l'iframe pour éviter le crash du processus principal.

Après être passé à la promesse suivante dans la chaîne de promesses, le programme vérifie la validité de la promesse et tente d'ajouter la trame de pile asynchrone selon le type d'appel asynchrone.

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

Nous avons choisi le cas kAsyncFunctionAwaitResolveClosure car le paramètre de la fonction AppendAsyncFrame, generator_object, est entièrement contrôlable.

En définissant des faux objets appropriés tels que PromiseReaction, Function, Context, JSGeneratorObject pour passer les conditions, nous pouvons injecter notre fausse trame asynchrone en appelant builder->AppendAsyncFrame(generator_object). Nous pouvons vérifier la fausse trame asynchrone injectée depuis le terminal.

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)

Voici le code fake_frame.js.

Après avoir injecté la fausse trame asynchrone, j'ai utilisé Error.prepareStackTrace avec la méthode getThis pour obtenir le receiver de l'objet d'erreur (dans ce cas, c'est JSGeneratorObject). Avec le receiver, nous pouvons récupérer le faux objet depuis le heap (primitif fakeobj).

Flux d'exploitation : J'ai utilisé le flux d'exploitation typique pour les exploits V8.

  1. En utilisant le primitif fakeobj, j'ai planté et récupéré le faux tableau OOB (out-of-bounds).
  2. En utilisant le faux tableau OOB, j'ai construit les primitives caged_read/caged_write.
  3. Pour atteindre la RCE, je me suis référé à la technique partagée lors du Google CTF 2023. Pour échapper au sandbox V8, j'ai corrompu l'objet BytecodeArray pour exécuter un bytecode arbitraire. En utilisant les instructions Ldar/Star avec un accès hors limites, nous pouvons lire/écrire la pile. Pour fuiter l'adresse de base du binaire Chrome, j'ai lu une adresse de retour depuis la pile pour fuiter les 32 bits inférieurs de l'adresse de base, et lu un pointeur de heap libc pour obtenir les 16 bits supérieurs de l'adresse. Ensuite, j'ai corrompu le pointeur de cadre pour un pivot de pile et exécuté la chaîne ROP pour atteindre la RCE.

Voici le code complet de l'exploit : index.html et exploit.html Il a été testé sur Chrome 118.0.5993.70 qui était la version cible du v8CTF M118.

Crédits

Haein Lee of KAIST Hacking Lab

Télécharger l’outil