Skip to content
KitploitKITPLOIT
ToolsBlog
Log in
Submit
ToolsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
CVE-2023-6702 — Chrome Renderer 1day RCE via Type Confusion in Async Stack Trace (v8ctf submission) | Kitploit
Tools/GitHubGitHub/kaist-hacking/cve-2023-6702
Vulnerability AnalysisExploitationWeb Application ExploitationCTFLearning & EducationBinary Exploitation
GitHubkaist-hacking/cve-2023-6702

CVE-2023-6702

Chrome Renderer 1day RCE via Type Confusion in Async Stack Trace (v8ctf submission)

View Repository
86972 years agoReviewed by Kitploit

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

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

Summary

This vulnerability allowed a remote attacker to execute arbitrary code inside the Chrome renderer process.

There was an insufficient type check in the async stack trace handling code. It leads to a type confusion between FunctionContext and NativeContext, causing illegal access to the JSGlobalProxy->hash value. With heap spraying, the attacker was able to inject a fake async stack frame, and construct the fakeobj primitive. Using the fakeobj primitive, the attacker was able to achieve arbitrary code execution in the Chrome renderer process.

You can check our TyphoonCon 2024 slides.

Vendor / Product / Version

  • Google Chrome
  • Affected Versions: pre 120.0.6099.109
  • Fixed Version: 120.0.6099.109

Timeline

  • 2020-05-13: Bug Introduced - [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: Advisory - https://chromereleases.googleblog.com/2023/12/stable-channel-update-for-desktop_12.html
  • 2024-01-12: v8CTF submission <-- The time we worked on this vulnerability
  • 2024-02-23: Bug report disclosed

Background

Async Stack Trace

Asynchronous is one of the most important feature in JavaScript. In the past, it was difficult to debug asynchronous code with error stack because async functions are not captured in the error stack. Suspended async functions are stored in the callback queue of the event loop not the call stack, so the error stack does not contain the async function. To resolve this issue, V8 provides "async stack trace" feature (by default since V8 v7.3) to capture async function in the error stack. ([v8 blog], [v8 docs])

Promise.all Resolve Element Closure

"Promise.all Resolve Element Closure" is a helper function to resolve the input promises in the Promise.all function. Promise.all function takes an array of promises and returns a promise that resolves when all of the input promises are resolved. "Promise.all Resolve Element Closure" is a resolve handler of each input promise in the Promise.all function. The role of the function is to resolve the input promise and store the fulfillment value in the result array.

There are 2 points to note about the function:

  1. It is a intrinsic builtin function and it is not directly accessible from the JavaScript code.
  2. The context of the function is used as a marker to check whether the function has been executed or not. It has FunctionContext until it was called, and then it has NativeContext after it was called. (v8 code)

The Vulnerability

Bug class: Type confusion between FunctionContext and NativeContext

Vulnerability details:

The vulnerability can be triggered by capturing an async stack trace with the already executed "Promise.all Resolve Element Closure" function or similar intrinsic builtin functions. In this exploit, I used the "Promise.all Resolve Element Closure" function as an example.

When an error is thrown in the JavaScript code, V8 captures the error stack from the stack and appends the async stack frames from the current microtask [1].

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

CaptureAsyncStackTrace function [[2]] looks up the promise chain and appends the async stack frame according to the async call type (e.g., await, Promise.all, Promise.any).

Below is the snippet of CaptureAsyncStackTrace function which handles the Promise.all case:

} 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 (

While looking up the promise chain, if reaction->fulfill_handler is "Promise.all Resolve Element Closure" builtin function, it appends the async promise combinator frame to the error stack. Then, it moves to the next promise by accessing function->context->capability->promise.

The issue is that the function assumes the "Promise.all Resolve Element Closure" function has not been executed yet. If the "Promise.all Resolve Element Closure" function has already been executed, the context is changed from FunctionContext to NativeContext. It leads to a type confusion between FunctionContext and NativeContext in the CaptureAsyncStackTrace function.

Making the PoC:

The strategy to trigger the vulnerability is as follows:

  1. Get the "Promise.all Resolve Element Closure" function which is an intrinsic builtin function.
  2. Explicitly call the "Promise.all Resolve Element Closure" function to change the context from FunctionContext to NativeContext.
  3. Set the "Promise.all Resolve Element Closure" function as a fulfill handler of a promise with a new promise chain.
  4. Throw an error in the promise chain and capture the async stack trace.

I used the synchronous promise resolving pattern for Promise.all to get the "Promise.all Resolve Element Closure" function at the JS script level. I borrowed the pattern from the test262 test cases.

After explicitly calling the function, to trigger the vulnerability, I used the sample code in the [zero-cost async stack trace document][v8 docs] to prepare a new promise chain and set the intrinsic builtin function as a fulfill handler of one of the promises.

Finally, when the error is thrown, the async stack trace is captured with the already executed "Promise.all Resolve Element Closure" function as a fulfill handler, leading to a type confusion between FunctionContext and NativeContext.

Here is the PoC code: poc.js

The Exploit

(The terms exploit primitive, exploit strategy, exploit technique, and exploit flow are defined here.)

Exploit primitive: fakeobj primitive

Exploit strategy: To build fakeobj primitive from the type confusion bug, I used the following strategy:

Download Tool