戰(zhàn)繞過與防御)
在實(shí)際業(yè)務(wù)里摸爬滾打過一段時(shí)間后我對CSRF這個(gè)老牌漏洞有了些新的理解。今天不聊那些嘴上說爛的“Token校驗(yàn)、SameSite”空話而是用一篇完整的教程從底層機(jī)制到實(shí)際場景把CSRF這個(gè)看似簡單的漏洞講透徹。無論你是剛?cè)胄械男氯诉€是寫過幾年業(yè)務(wù)代碼的開發(fā)我相信這篇文章都能給你一些不一樣的啟發(fā)。1. 內(nèi)容整體設(shè)計(jì)與思路拆解1.1 為什么說CSRF“入門容易精通難”CSRFCross-Site Request Forgery跨站請求偽造在各類漏洞排行里一直沒掉出過前排但很多零基礎(chǔ)的安全愛好者在學(xué)習(xí)它時(shí)會經(jīng)歷這樣一個(gè)過程剛看教程時(shí)覺得“這還不簡單”無非就是誘導(dǎo)用戶點(diǎn)擊一個(gè)鏈接就能偽造請求。真正上手測試后才發(fā)現(xiàn)實(shí)際情況復(fù)雜得多——CSRF漏洞的利用成功與否往往取決于你對目標(biāo)站點(diǎn)的業(yè)務(wù)邏輯理解有多深而不是僅僅會構(gòu)造一個(gè)表單就完事。我記得團(tuán)隊(duì)里一個(gè)新人A同學(xué)第一次接觸CSRF時(shí)對照網(wǎng)上模版寫了一個(gè)惡意的提交表單興沖沖地跑去測試某個(gè)內(nèi)部系統(tǒng)。結(jié)果顯而易見請求被攔截了。他當(dāng)時(shí)疑惑地問我“是不是目標(biāo)系統(tǒng)沒這個(gè)漏洞”我讓他去抓包看下實(shí)際請求里有沒有Token參數(shù)再看看Referer頭部是怎么處理的。結(jié)果發(fā)現(xiàn)目標(biāo)系統(tǒng)不僅校驗(yàn)了Token還把SameSite屬性設(shè)置成了Lax。這個(gè)案例很典型讓我意識到CSRF的“簡單”只停留在教科書層面真實(shí)環(huán)境下CSRF最難的其實(shí)是“繞過”與“防御”之間的博弈。1.2 這期教程的價(jià)值與適合人群這篇文章的定位不是一篇“讀起來很爽”的漏洞列表而是一篇具備實(shí)操參考價(jià)值的挖掘指南。我會從零開始拆解CSRF的觸發(fā)鏈路逐層分析“同源策略”“表單提交”“JSON跨域”這些看似基礎(chǔ)但綜合起來能決定漏洞是否存在的因素。如果你正在學(xué)習(xí)Web安全或者你是后端開發(fā)、全棧工程師想從“防御者視角”反向理解為什么CSRF會讓開發(fā)團(tuán)隊(duì)頭疼這篇文章很適合你。我會用自己的實(shí)戰(zhàn)經(jīng)歷、模擬測試環(huán)境和測試中踩過的坑給你展示一個(gè)符合真實(shí)業(yè)務(wù)場景的CSRF測試全流程。提示所有測試操作均應(yīng)在具備合法授權(quán)的前提下進(jìn)行。不建議對未經(jīng)許可的目標(biāo)發(fā)起任何安全測試否則后果自負(fù)。2. 核心機(jī)制解析CSRF為什么能夠存在2.1 先從“會話”聊起Cookie與身份認(rèn)證想要理解CSRF必須先理解Web應(yīng)用是如何識別一個(gè)用戶的。HTTP協(xié)議本身是無狀態(tài)的服務(wù)器無法直接知道“當(dāng)前發(fā)起請求的人是誰”。為了讓用戶不需要每次請求都重新輸入用戶名密碼Web應(yīng)用引入了“會話Session”機(jī)制通常通過Cookie在客戶端保存一個(gè)“會話標(biāo)識”Session ID。你登錄了一個(gè)網(wǎng)站服務(wù)器生成一個(gè)Session ID存在你瀏覽器的Cookie里并把它和你的用戶身份綁定在一起。之后你的每次請求帶上這個(gè)Cookie服務(wù)器確認(rèn)無誤后就認(rèn)為“這是本人在操作”。這個(gè)機(jī)制本身沒有問題但它催生了一個(gè)盲區(qū)瀏覽器自動攜帶Cookie的特性。2.2 攻擊的核心邏輯瀏覽器的“無腦”行為CSRF的攻擊邏輯是建立在瀏覽器“自動攜帶身份憑據(jù)”這一行為之上的。攻擊者構(gòu)造一個(gè)惡意頁面頁面里“藏”著一個(gè)指向目標(biāo)網(wǎng)站的請求。當(dāng)受害者訪問這個(gè)惡意頁面時(shí)瀏覽器會忠實(shí)地替受害者發(fā)出那個(gè)請求并且只要受害者在目標(biāo)網(wǎng)站處于登錄狀態(tài)瀏覽器就會自動附帶上他的Cookie。舉個(gè)最常見的例子假設(shè)你在某個(gè)論壇保持了登錄狀態(tài)黑客在某個(gè)人氣很旺的圖片分享帖里嵌入了一個(gè)隱藏表單表單的提交地址是那個(gè)論壇的“修改郵箱”接口。當(dāng)你打開這個(gè)帖子時(shí)隱藏表單自動提交你的郵箱就被修改成了黑客的郵箱。整個(gè)過程你完全無感知請求已經(jīng)發(fā)出并且攜帶了你的合法身份憑證。這就是CSRF最核心的模型。2.3 關(guān)鍵誤區(qū)CSRF與XSS的本質(zhì)區(qū)別很多零基礎(chǔ)同學(xué)會把CSRF和XSS跨站腳本弄混這里我用一句話說清楚XSS利用的是“用戶對網(wǎng)站的信任”核心是“在目標(biāo)網(wǎng)站內(nèi)執(zhí)行惡意腳本”。CSRF利用的是“網(wǎng)站對用戶瀏覽器的信任”核心是“在受害者的瀏覽器里發(fā)起帶有身份的偽造請求”。在CSRF攻擊中攻擊者完全不需要拿到目標(biāo)網(wǎng)站的Cookie、Session ID或其他任何身份憑證攻擊者只需要知道請求的目標(biāo)URL和參數(shù)格式。2.4 CSRF利用成功的條件缺一不可在實(shí)際測試中一個(gè)CSRF漏洞想要利用成功通常需要滿足以下三個(gè)條件之一或全部目標(biāo)請求必須使用Cookie或?yàn)g覽器自動攜帶的身份進(jìn)行鑒權(quán)。如果目標(biāo)接口改用Token放在請求頭比如Authorization: Bearer那么純CSRF攻擊基本行不通。目標(biāo)請求沒有防偽校驗(yàn)。比如沒有CSRF Token、沒有校驗(yàn)Referer/Origin或者校驗(yàn)方式可以被繞過。攻擊者能夠構(gòu)造一個(gè)“命中”的請求。這意味著攻擊者要精確知道目標(biāo)接口的URL、請求方法POST/GET、必要的參數(shù)名和參數(shù)值。理解了這個(gè)鏈路你才能明白為什么CSRF漏洞的高發(fā)區(qū)往往是**“非核心業(yè)務(wù)但涉及狀態(tài)變更”**的接口比如修改個(gè)人資料、綁定手機(jī)號、修改密碼一定條件下、點(diǎn)贊、關(guān)注、發(fā)表評論等。這些接口往往安全防護(hù)松懈但又真實(shí)地改變了用戶數(shù)據(jù)。3. 實(shí)操要點(diǎn)從零構(gòu)造一個(gè)CSRF驗(yàn)證環(huán)境以“教”帶“學(xué)”是最快的成長路徑。想要真正掌握CSRF漏洞的利用和防御自己動手搭一個(gè)靶場環(huán)境是必經(jīng)之路。我分享一下我平時(shí)測試用的最小化環(huán)境方案簡單、直接、不依賴外部網(wǎng)絡(luò)。3.1 環(huán)境準(zhǔn)備三個(gè)組件缺一不可你需要準(zhǔn)備這三個(gè)角色一個(gè)目標(biāo)站點(diǎn)我建議用本地PHPStudy或者直接寫一個(gè)最簡單的PHP/Flask/Express服務(wù)提供登錄鑒權(quán)接口和“修改個(gè)人信息”接口。一個(gè)攻擊者站點(diǎn)同樣可以放在本地或者使用本地文件file://協(xié)議模擬跨域注意文件協(xié)議在某些瀏覽器里Cookie行為不同建議還是起一個(gè)本地HTTP服務(wù)。一個(gè)受害者瀏覽器推薦使用無痕窗口方便模擬登錄態(tài)。3.2 核心靶場代碼目標(biāo)接口設(shè)計(jì)我以Python Flask為例寫一個(gè)最典型的“修改昵稱”接口用于演示。這個(gè)接口僅依賴Cookie進(jìn)行身份識別沒有CSRF防護(hù)為了模擬真實(shí)性我特意讓它使用POST請求內(nèi)容編碼格式為表單application/x-www-form-urlencodedfrom flask import Flask, request, make_response, session app Flask(__name__) app.secret_key test-secret-key # 僅演示用 users { admin: {password: 123456, nickname: Admin}, } # 登錄接口設(shè)置Session app.route(/login, methods[POST]) def login(): username request.form.get(username) password request.form.get(password) user users.get(username) if user and user[password] password: session[username] username return make_response(login ok) return make_response(login failed, 403) # 修改昵稱接口 —— 僅檢查登錄狀態(tài)無CSRF Token app.route(/update_nickname, methods[POST]) def update_nickname(): if username not in session: return make_response(not logged in, 401) new_nickname request.form.get(nickname) # 實(shí)際業(yè)務(wù)在這里更新數(shù)據(jù)庫 users[session[username]][nickname] new_nickname return make_response(fnickname updated to {new_nickname}) if __name__ __main__: app.run(host0.0.0.0, port8000)這段代碼里update_nickname接口沒有Token校驗(yàn)邏輯極其簡單——只判斷當(dāng)前瀏覽器是否攜帶了有效Session Cookie。3.3 攻擊者頁面隱藏表單與自動提交接下來是攻擊者的陣地。我需要構(gòu)造一個(gè)“惡意頁面”這個(gè)頁面放在另一個(gè)本地端口比如8001上。關(guān)鍵點(diǎn)是這個(gè)頁面里“藏”的表單提交地址是目標(biāo)站點(diǎn)127.0.0.1:8000的接口并且由于表單提交天然支持跨域?yàn)g覽器不會攔截這種“頁面發(fā)起跨域請求”的行為。html body h1一個(gè)正常的私人博客頁面/h1 p如果你看到了這篇文章說明你已經(jīng)中招了。/p form idcsrf-form methodPOST actionhttp://127.0.0.1:8000/update_nickname enctypeapplication/x-www-form-urlencoded input typehidden namenickname valueHacked-By-CSRF / /form script // 頁面加載完成后立即提交 window.onload function() { document.getElementById(csrf-form).submit(); } /script /body /html頁面看起來人畜無害但實(shí)際上是一個(gè)“請求發(fā)射器”。任何登錄了目標(biāo)站點(diǎn)的用戶只要訪問了這個(gè)惡意頁面瀏覽器就會向目標(biāo)接口發(fā)出一個(gè)POST請求并把受害者的Session Cookie一并帶上。3.4 完整復(fù)現(xiàn)與現(xiàn)象解釋操作步驟很簡單我按順序演示一遍打開無痕窗口A訪問http://127.0.0.1:8000/login提交用戶名和密碼admin/123456確認(rèn)登錄成功。登錄后訪問任意頁面確認(rèn)Cookie已被設(shè)置此時(shí)瀏覽器對目標(biāo)站點(diǎn)是“已認(rèn)證”狀態(tài)。另開一個(gè)無痕窗口B確保和目標(biāo)站點(diǎn)沒有直接關(guān)聯(lián)訪問攻擊者控制的惡意頁面http://127.0.0.1:8001/evil.html。觀察窗口A或者直接查詢數(shù)據(jù)庫中的nickname字段你會發(fā)現(xiàn)即使沒用目標(biāo)站點(diǎn)的Cookie、沒有輸入密碼用戶的昵稱也已經(jīng)被修改成了Hacked-By-CSRF?,F(xiàn)象背后發(fā)生了什么窗口B的瀏覽器向目標(biāo)服務(wù)器發(fā)送了一個(gè)攜帶了SessionID的合法POST請求目標(biāo)服務(wù)器只認(rèn)Cookie不關(guān)心這個(gè)請求到底是不是用戶本人主動發(fā)出的于是直接更新了數(shù)據(jù)。受害者完全沒有感知。這里補(bǔ)充一個(gè)容易被忽略的細(xì)節(jié)為什么表單提交能跨域因?yàn)?HTML 表單form的action屬性可以指向任意域名的地址瀏覽器出于歷史原因老式的Web交互模式允許這種POST跨域行為。而XMLHttpRequest / fetch 則受同源策略限制無法直接跨域發(fā)送“可攜帶Cookie”的請求需要額外配置CORS。這也是CSRF攻擊最傳統(tǒng)的載體是“表單”的根本原因。4. 常見問題與排查技巧為什么我的測試不成功實(shí)際測試中不可能每次CSRF利用都一擊即中。我把近幾年測試中遇到的高頻失敗原因做了匯總你對照著排查基本能解決95%的問題。4.1 防偽Token校驗(yàn)導(dǎo)致的失敗現(xiàn)象提交惡意表單后對方返回“請求無效”或“參數(shù)校驗(yàn)失敗”。排查思路這是最常見的攔截手段。目標(biāo)接口在渲染時(shí)生成了一個(gè)一次性Token存入隱藏字段或自定義Header下一次提交時(shí)對比是否一致。這里有個(gè)關(guān)鍵測試思路不要一上來就說“這里防御了沒法繞過”。你要在合法業(yè)務(wù)里找“Token是否被復(fù)用、是否可預(yù)測、是否在特定接口缺失”的場景。比如注冊接口的新用戶Token是否固定某些接口如退訂郵件、消息已讀上報(bào)是否因?yàn)闅v史原因漏掉了TokenGET類型請求也能觸發(fā)狀態(tài)變更且無Token時(shí)能否利用圖片標(biāo)簽img srchttp://target.com/delete?id123直接觸發(fā)4.2 SameSite Cookie策略導(dǎo)致的失敗現(xiàn)象寫好的利用頁面請求始終不攜帶Cookie抓包看Cookie字段是空的。排查思路這是現(xiàn)代瀏覽器抵御CSRF的標(biāo)準(zhǔn)手段之一。SameSite屬性有Strict、Lax、None三個(gè)值簡單說Strict完全禁止第三方Cookie隨跨站請求發(fā)送。Lax只有“頂層導(dǎo)航”的GET請求會攜帶Cookie表單POST和異步請求都不會攜帶。None允許所有請求攜帶Cookie但需要在HTTPS環(huán)境配置。如果目標(biāo)站點(diǎn)的Cookie設(shè)置的是SameSiteLax或Strict傳統(tǒng)的表單自動提交方式大概率失效。這時(shí)候你得轉(zhuǎn)換測試思路嘗試尋找“子域漏洞Cookie域配置寬松”的組合或者看看是否有“JSONP”“CORS配置錯(cuò)誤”可以利用的服務(wù)端邏輯。4.3 Referer/Origin校驗(yàn)帶來的挑戰(zhàn)現(xiàn)象目標(biāo)服務(wù)器校驗(yàn)了Referer或Origin頭發(fā)現(xiàn)請求來源不是自身域名直接拒絕。排查思路如果Referer校驗(yàn)是通過“包含匹配”例如檢查請求頭中是否包含了某個(gè)域名片段那么就存在繞過空間。舉個(gè)例子目標(biāo)校驗(yàn)Referer中是否包含target.com那攻擊者可以把惡意頁面放在target.com.attacker.com域名下不同子域名Referer就會變成http://target.com.attacker.com/evil-page包含目標(biāo)域名但實(shí)際是攻擊者控制的。不過這種方式需要攻擊者擁有一個(gè)可控域名的子域解析在真實(shí)SRC挖掘中屬于高危接口可利用邊緣場景的結(jié)合。4.4 JSON接口與Content-Type校驗(yàn)現(xiàn)象目標(biāo)接口要求Content-Type: application/json而HTML表單只能發(fā)送text/plain、application/x-www-form-urlencoded、multipart/form-data這三種編碼無法直接提交JSON數(shù)據(jù)。排查思路碰到這種情況傳統(tǒng)的form提交失效??梢栽囋囘@兩個(gè)方向JSONP接口如果目標(biāo)站點(diǎn)存在返回JSON數(shù)據(jù)的Callback接口老系統(tǒng)常見可以構(gòu)造script src...跨域加載并執(zhí)行特定函數(shù)間接向接口發(fā)送請求。Flash / 表單跨域漏洞2010年前后的老系統(tǒng)內(nèi)部處理函數(shù)有的會把text/plain主體按JSON解析因?yàn)閞equest.getParameter無法讀取但request.getInputStream是通的這種情況純CSRF攻擊也有可能成立。4.5 調(diào)整請求方式GET型CSRF別忽略現(xiàn)象目標(biāo)接口只支持GET請求但按照“傳統(tǒng)CSRF教程”好像都需要POST表單。排查思路如果GET請求直接攜帶了敏感操作參數(shù)例如DELETE /index.php?id2或者POST 接口支持 GET 方式訪問很多開發(fā)框架為了方便調(diào)試默認(rèn)允許這種場景下用img、link、script標(biāo)簽就能觸發(fā)跨站請求根本不需要表單。為什么不建議只測POST因?yàn)椤澳苡肎ET絕不用POST”的老一代開發(fā)習(xí)慣以及RESTful接口的普遍化導(dǎo)致“GET方法修改狀態(tài)”的案例至今沒有斷絕。這是一個(gè)非常典型的“低投入高回報(bào)”測試點(diǎn)。5. 防御方案與有效性對比說完了攻擊側(cè)和測試側(cè)我站在開發(fā)者的視角聊幾套CSRF防御方案的實(shí)際效果。記住沒有銀彈安全的本質(zhì)是“成本與收益的博弈”。5.1 同步令牌模式Synchronizer Token Pattern思路服務(wù)器生成隨機(jī)Token寫入表單隱藏字段或響應(yīng)Header中并在Session中保存一份。每次請求提交時(shí)比對兩個(gè)值是否一致。有效性與缺點(diǎn)這是最經(jīng)典的CSRF防御機(jī)制有效防止“無腦表單提交”。但它的防護(hù)強(qiáng)度取決于Token是否綁定用戶身份全局Token在“登錄前功能”上容易被繞過Token是否在每次請求后刷新一次性Token比固定Token安全得多Token的隨機(jī)性生成算法是否安全實(shí)際踩坑時(shí)你會發(fā)現(xiàn)很多系統(tǒng)中的Token只做了“存在性校驗(yàn)”沒做“一致性校驗(yàn)”或者雖然做了校驗(yàn)但Token同時(shí)存在于Cookie中導(dǎo)致可以配合XSS缺陷繞過。5.2 雙重提交CookieDouble Submit Cookie思路服務(wù)器要求前端提交一個(gè)Token這個(gè)Token在客戶端以Cookie形式存在。服務(wù)器校驗(yàn)“提交的Token”和“Cookie里的Token”是否一致由于跨站請求無法設(shè)置目標(biāo)域名的Cookie所以認(rèn)為安全的。有效性與缺點(diǎn)這個(gè)方案在“純CSRF”場景下很有效但如果目標(biāo)環(huán)境存在子域任一處XSS或者Cookie由于某些配置可以被子域影響例如Cookie Domain設(shè)置太寬攻擊者可以在攻擊者域名下寫入惡意Cookie來偽造提交Token導(dǎo)致繞過。5.3 SameSite Cookie 屬性前面已經(jīng)提到過這是瀏覽器層面提供的原生防護(hù)。強(qiáng)烈建議開發(fā)團(tuán)隊(duì)將所有“不要求跨站攜帶”的Cookie一律設(shè)置為SameSiteLax必要時(shí)用Strict。在Chrome和Firefox高版本中這基本能把傳統(tǒng)CSRF的90%問題堵在門外。這也是2020年以后CSRF漏洞數(shù)量陡降的主因之一。5.4 Origin/Referer校驗(yàn)思路如果請求頭中的Origin不是本站域名則拒絕。這種方案部署成本低但依然存在“空Origin”通過比如沙箱頁面、內(nèi)嵌頁面、某些舊版瀏覽器的情況。建議配合Token方案做雙保險(xiǎn)。5.5 防御方案對比速查表防御方案CSRF防護(hù)強(qiáng)度部署成本繞過難度備注Token同步令牌高中高需注意Token生成算法和失效機(jī)制雙重提交Cookie中低中對子域XSS較敏感SameSite屬性高極低高主流瀏覽器不能完全替代服務(wù)端防御兼容性問題Referer/Origin校驗(yàn)中低中需關(guān)注空值和子域解析問題驗(yàn)證碼/二次確認(rèn)極高高極高常用于高敏感操作如改密、轉(zhuǎn)賬我的建議是核心業(yè)務(wù)采取“TokenSameSite”組合重要操作修改密碼、綁定手機(jī)、轉(zhuǎn)賬額外增加二次驗(yàn)證。在成本和體驗(yàn)之間尋求一個(gè)可控的平衡點(diǎn)。6. 從漏洞挖掘角度看CSRF的行業(yè)變化6.1 “降級”為高危鏈里的一環(huán)這兩年CSRF在獨(dú)立漏洞挖掘中的地位有所下降原因正如前面提到的瀏覽器對Cookie的跨站限制越來越嚴(yán)格開發(fā)者框架默認(rèn)啟用防偽校驗(yàn)獨(dú)立觸發(fā)一個(gè)“純CSRF”漏洞的難度確實(shí)大了。但注意CSRF并沒有消失而是更多作為“高階漏洞鏈條”中的一環(huán)出現(xiàn)。比如在以下場景中CSRF依然是關(guān)鍵點(diǎn)睛之筆配合XSS降級利用一個(gè)理論上只能自我觸發(fā)的存儲型XSS通過CSRF將數(shù)據(jù)寫入目標(biāo)用戶擴(kuò)大影響范圍。配合并發(fā)邏輯漏洞利用CSRF讓受害者在不知情的情況下啟動某個(gè)操作如綁定攻擊者賬號、關(guān)注特定用戶、上傳特定資源配合競態(tài)條件實(shí)現(xiàn)“雙重操作”。攻擊“邊緣業(yè)務(wù)”很多大公司在核心業(yè)務(wù)邊邊角角的“活動頁面”“埋點(diǎn)上報(bào)”“第三方登錄回調(diào)”里依然存在CSRF。這些接口數(shù)據(jù)價(jià)值不高但一旦涉及“修改回調(diào)地址”或者“綁定手機(jī)號”就可能是高分值漏洞。6.2 零基礎(chǔ)學(xué)習(xí)的建議不要為了“挖洞”而“挖洞”很多同學(xué)學(xué)習(xí)安全時(shí)容易陷入一種“刷漏洞”的心態(tài)。作為一個(gè)過來人我的建議是先把一條完整鏈路的底層原理摸透。為什么表單能跨域?yàn)槭裁礊g覽器要默認(rèn)攜帶Cookie為什么SameSite設(shè)置為None時(shí)容易出問題如果一個(gè)漏洞的原理你已經(jīng)能夠用自己的語言向別人解釋清楚那你離真正掌握它就不遠(yuǎn)了。同時(shí)一定要熟悉抓包工具的使用。無論是Burp Suite還是瀏覽器開發(fā)者工具你需要從“結(jié)果”反推“過程”觀察請求的來龍去脈。我見過很多初學(xué)者明明漏洞已經(jīng)觸發(fā)了卻因?yàn)榭床欢埱竺姘謇锏膮?shù)來源錯(cuò)失了確認(rèn)漏洞的機(jī)會。6.3 合法測試的邊界最后必須非常嚴(yán)肅地強(qiáng)調(diào)一遍本文所有內(nèi)容僅為技術(shù)交流與教育目的。每一個(gè)涉及漏洞的工具、思路、代碼片段都應(yīng)當(dāng)在合規(guī)授權(quán)的范圍內(nèi)使用。企業(yè)授權(quán)測試和公益SRC有明確測試范圍——在范圍外測試即使沒造成破壞也是違背法律和道德的行為。做安全技術(shù)重要邊界感更重要。這一點(diǎn)每個(gè)從事安全領(lǐng)域的人都應(yīng)該牢記?,F(xiàn)在可以打開你身邊的靶場用本地環(huán)境按照上面的演示手動復(fù)現(xiàn)一遍CSRF的完整攻擊和防御流程。這比刷一百個(gè)漏洞報(bào)告都管用。