Skip to content
KitploitKITPLOIT
HerramientasBlog
Enviar
HerramientasBlog
Enviar

¡Herramientas de Hacking, PenTest y Ciberseguridad para tu Arsenal de Seguridad!

Kitploit es un directorio de herramientas de hacking, ciberseguridad y pentesting. Descubre las últimas actualizaciones de proyectos para encontrar vulnerabilidades, analizar sistemas, automatizar pruebas y fortalecer tu seguridad.

··Feeds·Contacto·Privacidad·© 2026 Kitploit

Directorio de Herramientas

Categorías

Ver todas las categorías
Loading categories
CVE-2023-6702 — Chrome Renderer 1day RCE mediante Type Confusion en Async Stack Trace (v8ctf submission) | Kitploit
Herramientas/GitHubGitHub/kaist-hacking/cve-2023-6702
Análisis de VulnerabilidadesExplotaciónExplotación de Aplicaciones WebCTFAprendizaje y EducaciónExplotación de Binarios
GitHubkaist-hacking/cve-2023-6702

CVE-2023-6702

Chrome Renderer 1day RCE mediante Type Confusion en Async Stack Trace (v8ctf submission)

Ver Repositorio
8692hace 2 añosRevisado por Kitploit

Más Populares

Ver todos →

Descubre las herramientas más usadas por nuestra comunidad.

Explora todas las herramientas

Explora nuestra colección de herramientas

Ver todas las herramientas →
Compartir

Chrome Renderer 1day RCE via Type Confusion in Async Stack Trace (CVE-2023-6702)

Resumen

Esta vulnerabilidad permitía a un atacante remoto ejecutar código arbitrario dentro del proceso renderizador de Chrome.

Había una comprobación de tipo insuficiente en el código de manejo de trazas de pila asíncronas. Conduce a una confusión de tipos entre FunctionContext y NativeContext, causando un acceso ilegal al valor JSGlobalProxy->hash. Mediante heap spraying, el atacante pudo inyectar un marco de pila asíncrono falso y construir la primitiva fakeobj. Usando la primitiva fakeobj, el atacante pudo lograr la ejecución de código arbitrario en el proceso renderizador de Chrome.

Puedes consultar nuestras diapositivas de TyphoonCon 2024.

Proveedor / Producto / Versión

  • Google Chrome
  • Versiones afectadas: anteriores a 120.0.6099.109
  • Versión corregida: 120.0.6099.109

Cronología

  • 2020-05-13: Bug introducido - [Promise.any] Implementar trazas de pila asíncronas para Promise.any
  • 2023-11-10: Reporte del bug - Seguridad: V8 Debug check failed: LAST_TYPE >= value
  • 2023-11-15: Parche - [promises, async stack traces] Corregir el caso cuando el closure se ha ejecutado
  • 2023-12-12: Advisory - https://chromereleases.googleblog.com/2023/12/stable-channel-update-for-desktop_12.html
  • 2024-01-12: Envío a v8CTF <-- El momento en que trabajamos en esta vulnerabilidad
  • 2024-02-23: Reporte del bug divulgado

Contexto

Traza de pila asíncrona

La asincronía es una de las características más importantes de JavaScript. En el pasado, era difícil depurar código asíncrono con la pila de errores porque las funciones asíncronas no se capturaban en la pila de errores. Las funciones asíncronas suspendidas se almacenan en la cola de callbacks del bucle de eventos, no en la pila de llamadas, por lo que la pila de errores no contenía la función asíncrona. Para resolver este problema, V8 ofrece la funcionalidad de "traza de pila asíncrona" (por defecto desde V8 v7.3) para capturar funciones asíncronas en la pila de errores. (v8 blog, v8 docs)

Closure de resolución de elementos de Promise.all

El "Closure de resolución de elementos de Promise.all" es una función auxiliar para resolver las promesas de entrada en la función Promise.all. La función Promise.all toma un array de promesas y devuelve una promesa que se resuelve cuando todas las promesas de entrada se han resuelto. El "Closure de resolución de elementos de Promise.all" es un manejador de resolución de cada promesa de entrada en la función Promise.all. Su función es resolver la promesa de entrada y almacenar el valor de cumplimiento en el array de resultados.

Hay 2 puntos a tener en cuenta sobre la función:

  1. Es una función intrínseca integrada y no es directamente accesible desde el código JavaScript.
  2. El contexto de la función se utiliza como un marcador para comprobar si la función ha sido ejecutada o no. Tiene FunctionContext hasta que se llama, y luego tiene NativeContext después de ser llamada. (código v8)

La vulnerabilidad

Clase de bug: Confusión de tipos entre FunctionContext y NativeContext

Detalles de la vulnerabilidad:

La vulnerabilidad se puede desencadenar capturando una traza de pila asíncrona con la función "Closure de resolución de elementos de Promise.all" ya ejecutada o funciones integradas intrínsecas similares. En este exploit, utilicé la función "Closure de resolución de elementos de Promise.all" como ejemplo.

Cuando se lanza un error en el código JavaScript, V8 captura la pila de errores de la pila y añade los marcos de pila asíncronos de la microtarea actual [1].

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

// Si --async-stack-traces está habilitado y la "microtarea actual" es un
// PromiseReactionJobTask, intentamos enriquecer la traza de pila con marcos
// asíncronos.
if (v8_flags.async_stack_traces) {
    CaptureAsyncStackTrace(isolate, &builder);
}

La función CaptureAsyncStackTrace [2] recorre la cadena de promesas y añade el marco de pila asíncrono según el tipo de llamada asíncrona (p. ej., await, Promise.all, Promise.any).

A continuación se muestra el fragmento de la función CaptureAsyncStackTrace que maneja el caso de 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);

    // Ahora inspeccionamos el contexto del elemento de resolución de Promise.all()
    // para encontrar la capacidad de promesa que se está resolviendo cuando todas
    // las promesas concurrentes se resuelven.
    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 (

Mientras recorre la cadena de promesas, si reaction->fulfill_handler es la función integrada "Closure de resolución de elementos de Promise.all", añade el marco de combinador de promesas asíncrono a la pila de errores. Luego, pasa a la siguiente promesa accediendo a function->context->capability->promise.

El problema es que la función asume que la función "Closure de resolución de elementos de Promise.all" no se ha ejecutado todavía. Si la función "Closure de resolución de elementos de Promise.all" ya se ha ejecutado, el contexto cambia de FunctionContext a NativeContext. Esto lleva a una confusión de tipos entre FunctionContext y NativeContext en la función CaptureAsyncStackTrace.

Creación del PoC:

La estrategia para desencadenar la vulnerabilidad es la siguiente:

  1. Obtener la función "Closure de resolución de elementos de Promise.all", que es una función intrínseca integrada.
  2. Llamar explícitamente a la función "Closure de resolución de elementos de Promise.all" para cambiar el contexto de FunctionContext a NativeContext.
  3. Establecer la función "Closure de resolución de elementos de Promise.all" como manejador de cumplimiento de una promesa con una nueva cadena de promesas.
  4. Lanzar un error en la cadena de promesas y capturar la traza de pila asíncrona.

Usé el patrón de resolución de promesas síncrono para Promise.all con el fin de obtener la función "Closure de resolución de elementos de Promise.all" a nivel de script JS. Tomé prestado el patrón de los casos de prueba de test262.

Después de llamar explícitamente a la función, para desencadenar la vulnerabilidad, usé el código de ejemplo del documento de traza de pila asíncrona de costo cero para preparar una nueva cadena de promesas y establecer la función intrínseca integrada como manejador de cumplimiento de una de las promesas.

Finalmente, cuando se lanza el error, la traza de pila asíncrona se captura con la función "Closure de resolución de elementos de Promise.all" ya ejecutada como manejador de cumplimiento, lo que lleva a una confusión de tipos entre FunctionContext y NativeContext.

Aquí está el código del PoC: poc.js

El exploit

(Los términos primitiva de exploit, estrategia de exploit, técnica de exploit y flujo de exploit están definidos aquí.)

Primitiva de exploit: primitiva fakeobj

Estrategia de exploit: Para construir la primitiva fakeobj a partir del bug de confusión de tipos, usé la siguiente estrategia:

  1. Hacer heap spraying con objetos JSPromise para que coincida el número hash aleatorio con un puntero de objeto JSPromise válido.
  2. Usar el valor hash como el puntero de objeto JSPromise válido e inyectar el marco de pila asíncrono falso.
  3. Usar Error.prepareStackTrace con el método getThis para recuperar el objeto falso.

El bug lleva a una confusión de tipos entre FunctionContext y NativeContext en la función CaptureAsyncStackTrace. Accede a Context->PromiseCapability->JSPromise para construir el siguiente marco de pila asíncrono. Cuando se desencadena el bug, accede a NativeContext->JSGlobalProxy->hash. Para explotar el bug, usé el valor hash como puntero de objeto JSPromise.

Podemos verificar que el valor hash tiene un rango de (0, 0xfffff) a partir de la siguiente función generadora de 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

El valor hash tiene la etiqueta SMI, por lo que en la memoria se almacenará como hash << 1. Por lo tanto, el valor en la memoria estará en el rango de (0, 0xfffff << 1) con números pares.

Para hacer coincidir el número hash aleatorio con un puntero de objeto JSPromise válido, tenemos 2 restricciones:

  1. La dirección del puntero interpretado debe ser un número impar.
  2. Tenemos que hacer spray en el heap en el rango (0, 0xfffff << 1).

Siguiendo las restricciones, hice spray en el heap con objetos JSPromise desplazados a la izquierda 8 bits para que la dirección sea impar, y usé bucles for pequeños para encajar en el rango (0, 0xfffff << 1).

Hacer coincidir el número hash aleatorio con un puntero de objeto válido parece tener una probabilidad bastante baja. Para aumentar la fiabilidad, utilicé la técnica de iframe. Las páginas de diferentes sitios web se ejecutan en procesos diferentes debido al aislamiento de sitios en Chrome. Así que creé un iframe con un dominio diferente y ejecuté el exploit en el iframe para evitar el bloqueo del proceso principal.

Después de pasar a la siguiente promesa en la cadena de promesas, el programa verifica la validez de la promesa e intenta añadir el marco de pila asíncrono según el tipo de llamada asíncrona.

root@kitploit:~
  while (!builder->Full()) {
    // Verificar que la {promesa} no esté resuelta.
    if (promise->status() != Promise::kPending) return;

    // Verificar que tengamos exactamente una PromiseReaction en la {promesa}.
    if (!IsPromiseReaction(promise->reactions())) return;
    Handle<PromiseReaction> reaction(
        PromiseReaction::cast(promise->reactions()), isolate);
    if (!IsSmi(reaction->next())) return;

    // Verificar si la {reacción} tiene una de las conocidas continuaciones de
    // función asíncrona o generador asíncrono como su manejador de cumplimiento.
    if (IsBuiltinFunction(isolate, reaction->fulfill_handler(),
                          Builtin::kAsyncFunctionAwaitResolveClosure) ||
        IsBuiltinFunction(isolate, reaction->fulfill_handler(),
                          Builtin::kAsyncGeneratorAwaitResolveClosure) ||
        IsBuiltinFunction(
            isolate, reaction->fulfill_handler(),
            Builtin::kAsyncGeneratorYieldWithAwaitResolveClosure)) {
      // Ahora inspeccionamos el AwaitContext de los manejadores para llegar
      // al JSGeneratorObject de la función asíncrona.
      Handle<Context> context(
          JSFunction::cast(reaction->fulfill_handler())->context(), isolate);
      Handle<JSGeneratorObject> generator_object(
          JSGeneratorObject::cast(context->extension()), isolate);
      CHECK(generator_object->is_suspended());

      // Añadir marco asíncrono correspondiente al {generator_object}.
      builder->AppendAsyncFrame(generator_object);

Elegimos el caso kAsyncFunctionAwaitResolveClosure porque el parámetro de la función AppendAsyncFrame, generator_object, es totalmente controlable.

Al establecer objetos falsos apropiados como PromiseReaction, Function, Context, JSGeneratorObject para pasar las condiciones, podemos inyectar nuestro marco asíncrono falso llamando a builder->AppendAsyncFrame(generator_object). Podemos verificar el marco asíncrono falso inyectado desde la terminal.

root@kitploit:~
Error: Vamos a echar un vistazo...
    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)

Aquí está el código de fake_frame.js.

Después de inyectar el marco asíncrono falso, usé Error.prepareStackTrace con el método getThis para obtener el receiver del objeto de error (en este caso, es JSGeneratorObject). Con el receiver, podemos recuperar el objeto falso del heap (primitiva fakeobj).

Flujo de exploit: Usé el flujo de explotación típico para exploits de V8.

  1. Usando la primitiva fakeobj, planté y recuperé un fake array OOB.
  2. Usando el fake array OOB, construí primitivas caged_read/caged_write.
  3. Para lograr RCE, me referí a la técnica compartida desde Google CTF 2023. Para escapar del sandbox de V8, corrompí el objeto BytecodeArray para ejecutar bytecode arbitrario. Usando instrucciones Ldar/Star con acceso fuera de límites, podemos leer/escribir la pila. Para filtrar la dirección base del binario de Chrome, leí una dirección de retorno de la pila para filtrar los 32 bits bajos de la dirección base, y leí un puntero del heap de libc para obtener los 16 bits altos de la dirección. Luego, corrompí el puntero de marco (frame pointer) para realizar stack pivoting y ejecutar la cadena ROP para lograr RCE.

Aquí está el código completo del exploit: index.html y exploit.html Fue probado en Chrome 118.0.5993.70, que era la versión objetivo de v8CTF M118.

Créditos

Haein Lee de KAIST Hacking Lab

Descargar herramienta