Arang.dev
搜索

利用最新 ECMAScript 功能实现的 XSS Filtering Bypass

利用 ECMAScript 新功能的 XSS 过滤绕过技术及原理解析

#web-security#xss#ecmascript#bypass
🌐 已翻译

本文由 AI 从韩语机器翻译,部分表达可能与原文有所不同。

阅读原文(韩语)

随着现代浏览器的 JavaScript 引擎更新到 ecma script 2019(es10),新增了许多元素和功能。因此,基于最新 ecma script 标准(而非传统 javascript)的 xss filtering bypass 技术也在不断涌现。我想在本文中分享这些内容,并简要讲解其原理。


XSS(Cross-Site Scripting) 漏洞主要是指:用户的 unvalid input 被插入到浏览器的 dom 内部,或者由于某种 javascript 行为,使得攻击者能够任意执行其想要的代码,或改变代码执行流程的漏洞。

大多数情况下,为了防御 XSS 漏洞,会设置如下防御机制。


  1. 代码层面的过滤

当需要接收用户传入的参数并将其输出到 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", ""); %>这样进行替换,或者在找到特定字符串时将参数清空等。

  1. 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 性质略有不同的数据类型。详细内容请参考下面的博客。

关于 Symbol 数据类型的参考博客

[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')//&param1=foo&param2=bar这样,然后触发了 xss,

location.search 的值就会变成?0:alert('xss')//&param1=foo&param2=bar, 整个字符串则会变成javascript:0?0:alert('xss')//&param1=foo&param2=bar。

0?0:alert('xss') 之后的字符串会因为注释而消失,由于这是三元运算的表达式,所以会变成 false?false:alert('xss'),最终攻击者想要执行的 javascript 语句就会被执行。


除了上面所写的这些语句之外,还有无数种方法可以绕过过滤。如果您想到了新的绕过技术,欢迎分享,非常感谢。

感谢您的阅读。

Reference* xss-cheatsheet_posix

评论

正在加载评论...
利用最新 ECMAScript 功能实现的 XSS Filtering Bypass | Arang