利用最新 ECMAScript 功能实现的 XSS Filtering Bypass
利用 ECMAScript 新功能的 XSS 过滤绕过技术及原理解析
本文由 AI 从韩语机器翻译,部分表达可能与原文有所不同。
阅读原文(韩语)随着现代浏览器的 JavaScript 引擎更新到 ecma script 2019(es10),新增了许多元素和功能。因此,基于最新 ecma script 标准(而非传统 javascript)的 xss filtering bypass 技术也在不断涌现。我想在本文中分享这些内容,并简要讲解其原理。
XSS(Cross-Site Scripting) 漏洞主要是指:用户的 unvalid input 被插入到浏览器的 dom 内部,或者由于某种 javascript 行为,使得攻击者能够任意执行其想要的代码,或改变代码执行流程的漏洞。
大多数情况下,为了防御 XSS 漏洞,会设置如下防御机制。
- 代码层面的过滤
当需要接收用户传入的参数并将其输出到 dom 时,会对用户的参数进行验证(validation),如果其中包含用于 XSS 攻击的词语或字符,则拒绝该请求或删除相应的词语/字符。
典型的过滤对象包括"(Double Quote)、'(Single Quote)、`(Back Quote)、<(Left Angle Bracket)、>(Right Angle Bracket) 等字符,以及 script、alert、eval、onerror 等 HTML Tag 和 Attribute。
过滤可以通过 XSS 过滤解决方案来完成,或者通过自行实现的 XSS Filter Function 来完成。
例如,对于 Java Web 服务器,可以实现各种过滤操作,比如像<% String param1 = request.getParameter("param1").replaceAll("script", ""); %>这样进行替换,或者在找到特定字符串时将参数清空等。
- Web 应用防火墙(WAF) 层面的过滤
与代码层面的过滤相同,当存在特定词语或字符时,拒绝用户的请求。
Web 应用防火墙层面的过滤相比代码层面,会全局性地应用于该服务器的页面,因此其特点通常是:相比于对单个字符的过滤,对 HTML Tag 或 Attribute 这类词语的过滤更为严格。(根据经验)
它似乎几乎不执行 replace 之类的操作,而是通过 block 来阻止攻击。
对于最近防御较好的站点,几乎绝大部分 HTML Tag 和 Attribute 都被纳入了过滤列表。在这种情况下,被称为 DOM XSS 的、需要像?param="><script>alert('xss');</script>这样直接插入 HTML Tag 的 XSS,大多会被过滤拦截而无法执行。(此外,在这种需要直接输入 HTML TAG 的情况下,还会被客户端浏览器的 XSS Auditor 检测到,除少数特殊情况外脚本不会执行。在这种情况下,要实现用于执行实际攻击的 weaponizing 是很困难的。)
因此,在这种过滤非常严格的情况下,发生 XSS 漏洞的情形之一,就是用户输入被"无限制地"插入到<script>标签区域内部的情况。这里所说的"无限制",是指即使 Web 应用防火墙层面存在过滤,但代码层面不存在过滤或过滤不完善,从而能够构造出我想要的代码流程的情况。下面结合示例进行说明。
XSS 漏洞代码示例
<script>
var username = "<%=request.getParameter("username")%>";
alert(username+"님 로그인 하셨습니다.");
</script>在上述情况下,通过在username参数中插入"(Double Quote) 字符,原本被插入到 script 标签内字符串中的参数就会脱离该字符串,从而能够执行任意脚本。
但这是在完全没有任何过滤的情况下;如果存在代码层面的过滤或 Web 应用防火墙层面的过滤,就必须在不使用相应字符和词语的前提下控制代码流程。
本文将介绍几种在这种情况下可以使用的、实用的过滤绕过语句。
使用正则表达式过滤 alert 等 javascript 内置函数执行的情况
a=alert; a(document.domain);javascript 中的函数(Function) 是 Object 的一种。javascript 中的 Object 是对象,可以将该 Object 赋值给变量。
因此,可以将 alert(内置函数)赋值给变量a,再通过调用它,像上面那样来表达 alert(document.domain)。
如果通过正则表达式(例如 /alert(.+)/)来阻止特定函数的执行,就可以像上面这样进行绕过。
以下内容与上面的说明相同
a=eval; b="aler"; c="t(documen"; d="t.domai"; e="n)"; a(b+c+d+e);a=eval; a(atob("YWxlcnQoZG9jdW1lbnQuZG9tYWluKQ=="));无法使用 Quote(引号) 的情况
eval(8680439..toString(30)+String.fromCharCode(40,49,41))Int..toString(Int) 会把前面的数字转换为字符串形式,并以参数的形式传入进制。也就是说,这里是让它按 30 进制解释并转换为字符串的命令。
String.fromCharCode(int, int) 会将以参数形式传入的 decimal integer 格式的数字转换为字符,并拼接(concatenation) 后返回。
因此,上面的命令等同于 eval("alert(1)")。
eval(/aler/.source+/t(1)/.source)在 javascript 中,位于 / 之间的字符串是正则表达式。正则表达式是 Object 类型的一种,在其下属函数中,存在可以将 type 转换为字符串的 .source attribute。
因此,在不使用 Quote(Single、Double、Back) 的情况下,就可以借助 .source attribute,将以字符形式写出的正则表达式转换为字符串,从而得到该正则表达式对应的字符串。
无法使用括号的情况
Set.constructor`alert\x28document.domain\x29```Set:Javascript 内置函数,这里使用了 Set,但换成其他任何内置函数都是可以的
constructor:设置 Set 这个函数的构造函数。
```:Back Quote 可以在向函数传递参数的同时执行该函数。若把上面的语句用括号和引号来表示,
则等同于Set.constructor("alert(document.domain)")()。
\x28、\x29:Ecma Script 对 Hex Ascii String、Unicode String 和 Octal String 支持 Auto Typecasting。
\x28 == \u0028 == \50
因此,将 Javascript 的内置函数 Set 的构造函数的行为设置为 alert(document.domain),然后通过执行(``) 该 Set 函数来让构造函数运行,从而使攻击者能够执行其想要的任意脚本。
以下内容与上面的说明相同
Set.constructor`alert\`document.domain\````Set.constructor`alert\u0028document.domain\u0029```setTimeout`alert\x28document.domain\x29`setInterval`alert\x28document.domain\x29`词语过滤 + Quote 过滤 + 括号过滤
_=URL+0,/aler/.source+/t/.source+_[12]+/documen/.source+/t.domai/.source+/n/.source+_[13]instanceof{[Symbol.hasInstance]:eval}我们从前往后依次分析。
URL 是内置函数。函数(Function) 在 javascript 中是 Object。当 Object 与 String 类型之间发生加法运算时,会先执行 Object.toString() 方法,将 Object 本身 Type Casting 为字符串,然后再与 String 字符串相加。
因此,就会像上图那样变成字符串。之所以执行这样的操作,是因为括号被过滤了,为了在不使用括号的情况下获得括号字符。
该字符串被赋值给了_变量。_[12]==(,_[13]==)
正如上面所看到的,可以利用正则表达式来获取并拼接字符串。通过这样的运算,我们得到了 "alert(document.domain)" 这个字符串。
在最新的 Ecma Script 中,除了原有的 String、Integer 等数据类型外,新增了一种名为 Symbol 的数据类型。
在这个 Symbol 数据类型的属性中,存在一个名为 hasInstance 的属性。它是一个可以判断该 Symbol 对象是否为 instance,并据此重新定义后续行为的属性。
我们把上面的代码表达得更易于理解一些。
"alert(document.domain)" instanceof { [Symbol.hasInstance] : eval }
{ } 是 Object。我们将这个 Object 的 Symbol.hasInstance 属性(当该对象是 instance 时将要执行的代码)定义为了 eval。
然后,通过 instanceof 运算符,用"alert(document.domain)"字符串来询问后面的对象 {} 是否为 instance;如果这个对象(Object) 是 instance,就会把该字符串作为上面定义的 eval 的参数传入并调用,然后返回其结果。
因此,最终就变成了 eval("alert(document.domain)")。
基于类似原理实现的代码如下。
_=URL+0,Array.prototype[Symbol.hasInstance]=eval,/alert/.source+_[12]+1+_[13]instanceof[]_=URL+!0+!1,Array.prototype[Symbol.hasInstance]=eval,_[19]+_[38]+_[40]+_[33]+_[4]+_[12]+1+_[13]instanceof[]Event.prototype[Symbol.toPrimitive]=x=>/javascript:0/.source+location.search,onload=open上面的代码中混入了一些不同的内容,因此我会加上注释说明。
location.search 指定的是 uri 中?之后的部分。
如果通过javascript:scheme 将页面跳转到该 scheme,之后就可以执行任意 javascript 代码。
解析上面的代码,
通过函数提升(hoisting) 和 Arrow Function,将 Event 内置函数的 prototype(构造函数)的 Symbol.toPrimitive 的返回值设置为/javascript:0/.source+location.search。
正如上面所说,原有的 Ecma Script 数据类型有 String、Integer、Undefined、Null、Boolean 这 5 种。
从 Ecma Script6 开始,把这些称为 Primitive Type,并在此基础上新增了一种名为 Symbol 的数据类型。
Symbol 是一种与原有 Primitive Type 性质略有不同的数据类型。详细内容请参考下面的博客。
[ES6] 8. Symbol关于前端开发的博客jaeyeophan.github.io
当 Symbol 类型与原有的 primitive type 匹配时,用于将其转换过去的 attribute method 就是 Symbol.toPrimitive method。
Symbol.toPrimitive 方法的返回值是通过 Arrow Function 和函数提升来设置的。"javascript:0"+location.search
通过onload=open语句,将 document.onload 的行为指定为 open 函数。
由于 onload 是 Event 属性,所以 Event 内置函数会被执行,设置在其 prototype 上的 Symbol.toPrimitive 也会随之执行。
open 内置函数经过 toPrimitive method,
open(x => /javascript:0/.source+location.search)
->x = function(x){return /javascript:0/.source+location.search}; open(x);
->open(/javascript:0/.source+location.search),会变成如上这样的语句。
如果攻击者将 uri 的链接设置为https://ar9ang3.com/test.html?0:alert('xss')//¶m1=foo¶m2=bar这样,然后触发了 xss,
location.search 的值就会变成?0:alert('xss')//¶m1=foo¶m2=bar,
整个字符串则会变成javascript:0?0:alert('xss')//¶m1=foo¶m2=bar。
0?0:alert('xss') 之后的字符串会因为注释而消失,由于这是三元运算的表达式,所以会变成 false?false:alert('xss'),最终攻击者想要执行的 javascript 语句就会被执行。
除了上面所写的这些语句之外,还有无数种方法可以绕过过滤。如果您想到了新的绕过技术,欢迎分享,非常感谢。
感谢您的阅读。
Reference* xss-cheatsheet_posix
评论