浅析XSSI漏洞
/0x00 背景
XSSI漏洞是个老东西了,最近看了下有点新东西,就记下笔记。
为了有效防御XSS,业界推出了CSP即内容安全策略,即使用白名单机制,对网站加载或执行的资源进行安全策略的控制。这种情形下,除非CSP配置存在问题,不然XSS一般是难以再被深入挖掘利用了。
但在某些场景中,开发者看你们会将敏感信息存放在某些文件中,当无法对XSS进行利用时,此时就可以尝试挖掘下是否存在XSSI漏洞。
0x01 XSSI原理
XSSI(全称Cross Site Script Inclusion)跨站脚本包含,是一种通过嵌入script标签的src属性来加载外部数据来实现绕过边界窃取敏感信息的漏洞。
例如:
1 | <!-- attacker's page loads external data with SCRIPT tag --> |
XSSI的核心原理就是绕过SOP(同源策略)来跨域包含含有敏感信息的外域文件。我们知道,script标签是允许跨域加载资源的,如果某个网站的动态脚本、文件或响应中包含某些敏感信息(比如唯一标识符、个人资料、防御CSRF的Token),便有信息泄露的风险。XSSI就是利用的script标签允许跨域加载资源的特性来实现跨域包含资源的,这正是通过JSONP的技术来实现的。注意,大多数的XSSI都是针对动态JS文件进行攻击利用的。
传统的XSSI攻击场景如下:恶意页面B使用script标签包含了目标网站A用来储存敏感数据的信息源C(可能是动态脚本、文件或响应),当攻击者引导受害者访问B时,由于受害者此时在A处于登录态,B可以轻松获取C中包含的受害者的敏感信息。
如图:
0x02 XSSI与XSS、CSRF的区别
相同点:三者均为Web前端安全漏洞,即针对客户端攻击的漏洞。
不同点:
- XSS是在受害者页面中注入恶意代码执行恶意操作,例如窃取已登录用户的cookie信息;
- CSRF是通过诱使受害者访问恶意页面导致向目标页面发起请求,在受害者已登录的目标页面中执行恶意动作,例如提交修改用户密码的表单操作;
- XSSI是通过script标签的src属性来跨域包含含有敏感数据的文件来窃取敏感信息的;
0x03 XSSI攻击利用
XSSI的攻击利用根据包含敏感信息的目标文件类型主要分两种场景,即JS文件和其他文件。这里JS文件的攻击利用最为简单直接,因为JS文件其内容本身就是JS代码,加载到script标签中刚好符合JS格式语法;而其他类型的文件的利用相比之下较为复杂。
我们可以对XSSI的攻击利用进行个简单的分类:
- 针对JavaScript类型文件
- 静态的JavaScript文件
- 静态的JavaScript文件,但仅在认证后可访问
- 动态的JavaScript文件
- 针对非JavaScript类型文件
- CSV文件
- JSON文件/响应
针对JavaScript类型文件
在某些JS文件中,可能会保存着一些敏感信息。当然,静态和动态的JS文件它们之间的利用是存在区别的。
窃取JS全局变量的值
这里假设目标服务端存在静态的secret.js文件,里面保存着token敏感信息:
1 | var token = "mi1k7ea_token_192ufh189u2erjsjoif189u"; |
攻击者编写的xssi.html文件:
1 | <!--恶意页面--> |
改文件即XSSI的攻击实现。通过script标签的src属性来跨域加载目标服务端的secret.js文件进来,然后将窃取到的token信息显示在页面上:
当然,这只是个模拟的简单的不行的演示。因为这个JS文件时静态的,直接访问都是可以的,此时利用XSSI进行攻击都是多此一举的。一般的,XSSI是针对动态的JS文件来攻击利用的,这是因为动态的JS文件通常在用户处于登录态时容易包含敏感信息。
那么如何快速分辨一个JS文件是否为动态JS文件?——当有Cookie和无Cookie时请求所响应的文件内容不同时,即可确定这是一个动态JS文件了,当然并不是每一个动态JS文件都可以被利用。
我们可以使用Burpsuite的插件DetectDynamicJS来完成这项工作,此插件已在github上开源,链接地址:https://github.com/portswigger/detect-dynamic-js
前面的敏感信息是保存在JS文件的全局变量中,获取的时候直接读取该全局变量的值即可,十分方便。下面看下敏感信息保存在JS文件的局部变量中如何来获取。
重写函数窃取数据
一般情况下,网站都会将一些基本的功能函数写入一个JS文件中,以便后面的各项业务中能够很方便地重用这个功能函数。
Demo1
这里假设目标服务端含有敏感token信息的动态JS文件,getToken()函数中的session是动态获取的,此时敏感信息保存在局部变量中,并且调用doSomeThing()函数对该敏感信息进行处理:
1 | (function(){ |
XSSI利用页面,此时没法像前面那样直接通过全局变量就获取到敏感数据,因为这里敏感数据是保存在局部变量中的,并且调用了doSomeThing()方法对该变量进行处理,因此这里通过重写doSomeThing()函数来窃取token数据:
1 | <!--恶意页面--> |
此时就能成功窃取到敏感信息token了,这是因为将目标服务端的JS文件加载进来后,在调用doSomeThing()函数处理敏感数据时,是直接调用的恶意页面上实现的doSomeThing()函数,从而执行了攻击者自己编写的doSomeThing()函数的代码被窃取到了数据:
Demo2
secret.js,这次传递给函数的数据是一个包含了我们想要的数据的回调函数:
1 | (function(){ |
xssi.html,攻击者可以通过使用toString方法来获得回调函数中的这些数据:
1 | <!--恶意页面--> |
这里就能获取到包括敏感信息在内的整个回调函数的内容:
如果想精准输出内容,可用正则:
1 | ... |
重写原型链窃取数据
如果目标JS文件中并未调用相应的函数对敏感数据进行处理,换句话说,就是我们没办法重写函数来窃取敏感数据,此时我们可以考虑通过重写原型链来实现。这种方式的关键点在于,JS中调用了敏感数据所属类型的内置方法,比如String类型的内置方法split()/trim()/search()…等。
Demo1
这里假设目标服务端含有敏感token信息的动态JS文件,getToken()函数中的session是动态获取的,此时敏感信息保存在局部变量中,和前一小节的区别在于,并未调用函数对敏感数据进行处理,而是调用了String类型的内置方法trim()来处理该敏感数据:
1 | (function(){ |
XSSI利用页面,重写String类型的内置方法trim()的原型链来实现窃取敏感数据,这是由于token是String类型的,在调用trim()方法时会调用String原型链中的trim()方法:
1 | <!--恶意页面--> |
此时能够成功窃取到数据:
原型链分析
我们到浏览器的Console试下就知道。先看下String类型的原型链确实存在trim()这个内置函数,当然,如果含有敏感信息的String类型的局部变量调用了其他如下列出的内置方法我们都可以直接同理利用:
接着看下trim()内置方法的实现:
OK,现在我们知道目标JS中该String类型变量会调用trim()方法,那么我们就可以通过在我们的恶意页面来污染String原型链的trim()方法为我们自定义实现的方法,从而来窃取数据:
Demo2
较Demo1,使用了一个函数和toString方法。
secret.js:
1 | (function(){ |
xssi.html,通过prototype将所有Functiorn的call方法重写,然后再调用.toString()方法将函数转换为字符串类型再读取内容,最后再引入产生数据泄露的脚本文件执行:
1 | <!--恶意页面--> |
这里就不通过Jquery往标签中写内容了,因为这里污染了Function的原型链会让Jquery在执行时报错。修改成弹框显示就好:
针对非JavaScript类型文件
某些场景下,目标文件并未JS类型,包含敏感信息的文件,其中的内容并不能直接作为一个JS变量的值读取,或者文件内容是多行的,这些都会使得XSSI的信息窃取变得很困难。
下面先看下之前wooyun的文章中说到的方法,但都有些历史了,这里本地测试下看看是否还能成功。其中涉及到的文件类型包括CSV、JSON等。
IE bug导致错误信息泄漏
这种方法的局限性在于ie的版本要小于10,且目前的Chrome和Firefox都不能成功利用。除此之外,获取的CSV中的内容只能获取第一行、第二列的内容,并不能全部获取得到。
为了防止js错误信息跨域泄漏,对于外部加载的js文件,现在主流的浏览器只有固定的错误信息,比如“script error”,但是在ie9与ie10,情况不一定如此。
一般来说,在外部js发生语法错误的情况下,浏览器只会提供固定的错误信息,但是当在runtime发生错误的情况下,浏览器会提供详细的错误信息。比如”foo 未定义”之类的,某些浏览器一旦允许外域js回复详细的错误信息,就会导致信息泄漏。
就是说,当某个网页的内容能被js识别为javascript格式的话,那么就可能通过错误信息获取到目标的内容。
假设目标服务端存在包含敏感信息的a.csv:
1 | name,age,address |
用软件打开就是这样的:
xssi.html,设置window.error的错误显示:
1 | <!--恶意页面--> |
在ie 8上攻击利用,可以看到会显示CSV文件的第一行、第二列的内容:
出现这种情况的原因在于,浏览器将目标CSV文件内容识别为JavaScript,其中age被识别为某个未定义的JS变量。当为这种情况的时候,浏览器就允许页面捕捉来自不同网页的错误信息。
在Chrome和Firefox,以及10版本以上的ie都不能成功。比如Firefox中并不会弹框,而是直接被浏览器拦截了获取CSV类型的响应:
UTF-16编码窃取敏感信息
这种方法突破了前面只能对CSV信息进行窃取的尴尬局面,但局限性和前面的方法一样,仅在ie < 10的版本下才能成功利用,因为ie 10会拒绝将没有空字节活着bom的编码为UTF-16。
这种方法的原理如下:
使用
script
标签的charset
属性将包含的文件编码为UTF-16,其目的在于强制文件的所有内容连为一体,变为一个未定义的Javascript变量。然后通过在window域内使用onerror
捕获错误信息(此错误信息一定为已编码的文件内容 is not defined
),再进行解码即可。此举其实是为了防止符号会引起Javascript出现其他异常,例如英文逗号会截断文件内容,报错只会显示逗号前的内容未定义;而中文逗号则会直接提示非法字符,从而获取不到任何敏感信息。
a.json,假设的目标服务端保存着敏感数据的Json文件:
1 | {"username":"admin", "token":"89uki4gk9iu9213trju"} |
xssi.html,在script标签中加入charset=”UTF-16BE”,同时通过window.error来捕获错误信息并弹框显示出来:
1 | <!--恶意页面--> |
本地使用ie 8进行测试,弹框显示一段乱码内容,并识别该乱码为JS变量未定义:
乱码内容如下:
1 | 笢畳敲湡浥∺≡摭楮∬•瑯步渢㨢㠹畫椴杫㥩甹㈱㍴牪產 |
将这堆乱码再进行UTF-16BE编码等操作即可获取到原始内容·:
注意,这种方式成功的前提在于浏览器是否会将编码后的内容识别为JS的变量,若不能则无法利用成功。除了IE 10以上的版本,在当前的Chrome和Firefox中都是不能成功的,这是因为浏览器并未将编码后的内容识别为JS变量,自然而然地也就无法从window.error中捕获到乱码信息了:
下图是引自wooyun文章,能够被浏览器认定为有效的JS变量,当字符编码为UTF-16的时候的数字字母组合,ie 9将其99.3%认为是有效的js标示符,高于Chrome和Firefox:
Harmony proxy bug in Firefox / Chrome
Harmony是一个ECMAScript 6中的新功能,类似于Java的反射类,其中定义了对于对象属性的查找、分配、函数调用,在我们针对这些新特性的研究过程中发现该功能可以用于XSSI的攻击中。
注意,这种方法在当前较新版本的Chrome和Firefox中都以失效。
和前面一样的a.csv:
1 | name,age,address |
xssi.html,其中window.__proto__
定义了一个代理对象,当访问一个未定义的全局变量,就会出发handler进行处理:
1 | <!--恶意页面--> |
在本地测试没成功,显示不能给该对象设置prototype:
穷举搜索
穷举搜索简单地说就是在客户端使用某些方法来暴力破解目标文件中的敏感数据,分为下面三种类型。
定义变量穷举CSV内容
此方法适用于当前所有IE版本。
在前面IE Bug小节中说到了利用IE的Bug可以获取CSV中的第一行第二列的内容,这是因为IE浏览器将该内容当成JS变量然后报错显示错误信息导致信息泄露。现在这种方法的原理在于,我们在恶意页面中就穷举定义CSV中可能存在的项的内容,如果穷举成功、定义了该CSV项的值的变量,那么浏览器就不会报错,证明我们穷举成功。
在前面的基础上,我们可以通过定义变量的方式来穷举CSV中的所有内容(这里数值无法定义变量也就无法穷举出其值),同时也解决了一般情况下浏览器不提供详细的外部错误信息的问题,这样,即使在最新版的IE 11也能够成功进行利用。
假设a.csv如下:
1 | name,age,address |
xssi.html,先啥变量都不定义:
1 | <!--恶意页面--> |
此时会报age未定义,也就是CSV中的第一行第二列的内容,和前面的一样:
接着,修改xssi.html,添加age变量的定义:
1 | ... |
此时发现和前面的报错是一样的。没关系,我们直接修改xssi.html,添加age前面的name的变量的定义,发现报错信息就会往后识别了:
1 | ... |
根据这个原理,我们就可以对CSV文件中的每一项的内容进行穷举,直至浏览器无报错时即穷举完成:
1 | ... |
值得注意的是,CSV中的项若为数值,则无需我们定义该数值的变量、也无法定义该变量,换句话说,这种方式除了CSV中的数值项、其他的全部内容都能够被成功穷举出来。
JS getter穷举CSV内容
原理和上一小节定义变量大同小异,只是借助了JavaScript的getter方法来实现。
此方法同样适用于当前所有IE版本。
a.csv如上。
xssi.html,穷举过程和前面的类似,这里先对CVS中的几项进行定义穷举:
1 | <!--恶意页面--> |
要是文件中每一项的内容被定义成功,则会逐个弹框显示出来,并且浏览器最后会报错有些像还未定义,当然显不显示具体的未定义的项的内容得看浏览器:
接着和前面小节一样,继续通过JS的getter来穷举出CSV文件内容中的其他项即可:
1 | ... |
此时穷举完成,浏览器也没有报错,说明CSV文件中的每一项均已被定义穷举(当然,数值项是无法通过定义穷举到的,这个缺点和前面小节是一样的):
结合VBScript穷举JSON数组
注意:VBScript只适用于IE中。此方法在我本地的IE 11中测试并不成功,而在IE 8上OK。
假设服务端存在包含敏感信息的文件a.json:
1 | ["admin","this_is_password"] |
xssi.html,这里script标签添加language属性值为vbscript,其中模拟穷举JSON数组内容进行暴力破解:
1 | <!--恶意页面--> |
在IE 8中成功利用VBScript穷举出JSON数组中的敏感信息:
利用敏感文件中的可控字段窃取数据
CSV with quotations thef
简单地说,就是CSV文件中的敏感内容被双引号括起来了,这样的话前面针对CSV的操作就没用了。但是如果我们能够控制CSV文件中某些项的值,那么还是可以进行信息窃取的,而且一般我们进行CSV文件导出之前、我们是可以设置我们想导出的项以及内容的。
注意,该方法目前仅适用于全版本的IE,不适用于Chrome和Firefox。
假设CSV文件如下,其中有两处地方可控:
1 | 1,"可控","123@123.com","03-0000-0001" |
此时,我们可以通过可控的项来构造如下的CSV文件,先注入个双引号闭合掉前面的引号,然后使用mi1k7ea=function() {/*…*/}
来解决多行的问题:
1 | 1,"a",mi1k7ea=function(){/*","123@123.com","03-0000-0001" |
xssi.html,调用mi1k7ea.toString()获取函数源码来达到攻击目标数据的目的:
1 | <!--恶意页面--> |
在IE 11中测试成功:
为啥能成功执行?我们将CSV文件内容直接放到IE的Console中运行,浏览器是能够成功识别为JS代码并定义了一个名为mi1k7ea的JS函数:
也就是说,通过可控字段构造CSV文件的目的就是为了让文件内容使浏览器能够正常识别为JS代码,并且将中间的敏感信息都注释掉,然后通过调用toString()函数来获取JS函数源码从而成功窃取敏感信息。
其实前面那段CSV内容放到Chrome和Firefox中都能成功识别为JS的,结果和IE的一样,但为啥不能在XSSI中利用呢?——原因在于目前较新版本的Chrome和Firefox都已经将MIME类型为text/csv
的响应都自动拦截掉了,因此无法成功利用:
利用ECMAScript6特性——‘`’反引号
业界研究的这种技巧,是针对Chrome和Firefox的利用的,因为,但是在目前的Chrome和Firefox中已经不再能成功利用,原因如前面所说,Chrome和Firefox都已经将MIME类型为text/csv
的响应都自动拦截掉了。
但是我们还是来看下这个原理和利用吧。
假设目标CSV文件如下,其中’Sample report’这个字段是我们可以控制的:
1 | report_id,report_title,program_name,total_amount,amount,bonus_amount,currency,awarded_at,status |
然后我们构造如下,将’Sample’作为JS变量,变量内容未反引号括起来的内容,最后需要注释掉后面部分的内容才能使浏览器识别JS不会报错:
1 | report_id,report_title,program_name,total_amount,amount,bonus_amount,currency,awarded_at,status |
xssi.html,注意定义CSV中第一行的项为变量:
1 | <!--恶意页面--> |
在Chrome或Firefox上测试是不成功的,原因同上:
但是这段CSV在Firefox和Chrome上的Console上是能成功识别并执行的,也就是说构造本身是没问题的:
利用ECMAScript6特性——箭头函数
a.csv,假设num和b项可控:
1 | name,num,team |
构造如下:
1 | name,num=i=>/*,team |
xssi.html:
1 | <!--恶意页面--> |
在Chrome和Firefox上运行会报错,原因同上。
但是这段CSV在Firefox和Chrome上的Console上是能成功识别并执行的,也就是说构造本身是没问题的:
窃取非JS、非CSV类型文件敏感信息
前面三种方法,对于现在较新版本的Chrome和Firefox都是失效,但是如果目标文件类型并非是JS/CSV的话,就能够通过可控字段来构造文件内容实现在Chrome和Firefox上的执行,这是因为响应的MIME类型并非text/csv
,也就不会被浏览器所拦截了。
比如前面的三种方法中,将a.csv改为a.txt,在Chrome和Firefox上能成功执行:
在前面的第二种方法中,还有一种构造形式,即将Sample后面的=等号去掉,也就是说直接将Sample当成函数进行定义,a.txt如下:
1 | report_id,report_title,program_name,total_amount,amount,bonus_amount,currency,awarded_at,status |
xssi.html修改如下,直接获取Sample函数的参数内容:
1 | <!--恶意页面--> |
这样同样能利用成功。
参照这些例子,同理去挖掘即可。
网上的案例
Yahoo的XSSI,窃取动态JS文件中的session值,进而窃取受害者账号的具体信息:挖洞经验 | 看我如何发现雅虎XSSi漏洞实现用户信息窃取
Hackerone的XSSI,CSV文件内容的窃取以及非JS非CSV类型文件内容的窃取:【技术分享】hackerone漏洞:如何利用XSSI窃取多行字符串(含演示视频)
0x04 防御
- 开发者永远也不要把敏感数据放在JavaScript文件中, 也不要放在JSONP中;
- 请求敏感文件/响应的尽量改为POST方式;
- 使用类似于CSRF-Token机制;
- 设置响应头为
X-Content-Type-Options: nosniff
,此时浏览器就会拒绝加载JS类型的数据;