先日、サーバー側でリクエストの署名を検証するサービスに出会いました。それは小さなオンライン カジノで、リクエストごとにユーザーがブラウザーから送信した値をチェックしていました。カジノで何をしていたか (賭けをしていたか、入金していたか) に関係なく、各リクエストには「sign」という値が追加され、一見ランダムな文字のセットで構成されていました。この値なしでリクエストを送信することは不可能でした。サイトはエラーを返し、独自のカスタム リクエストを送信できませんでした。
この価値がなかったら、私はその瞬間にサイトを離れ、二度とそのことを考えなかったでしょう。しかし、予想に反して、私を興奮させたのは、すぐに利益が得られるという感覚ではなく、カジノが私に与えてくれた、その絶対確実な方法による研究への興味と挑戦でした。
開発者がこのパラメータを追加したときに念頭に置いていた目的に関係なく、それは時間の無駄だったように思えます。結局のところ、署名自体はクライアント側で生成され、クライアント側のアクションはすべてリバースエンジニアリングの対象となる可能性があります。
この記事では、私がどのようにして次のことを達成したかについて説明します。
この記事は、安全なプロジェクトに関心のある開発者向けに、貴重な時間を節約し、役に立たないソリューションを拒否する方法を説明します。また、ペンテスターの場合は、この記事を読んだ後、デバッグに関する役立つ教訓や、セキュリティのスイスナイフ用の独自の拡張機能のプログラミングを学ぶことができます。つまり、誰もがプラスの立場にいるということです。
いよいよ本題に入りましょう。
つまり、このサービスは、一連の古典的なゲームを備えたオンライン カジノです。
サーバーとのやり取りは、完全に HTTP リクエストに基づいて行われます。選択したゲームに関係なく、サーバーへのすべての POST リクエストは署名されている必要があります。署名されていない場合、サーバーはエラーを生成します。これらの各ゲームでのリクエストの署名は同じ原則で機能します。同じ作業を 2 回行わなくても済むように、1 つのゲームだけを調査対象とします。
私は「Dragon Dungeon」というゲームをやります。
このゲームの本質は、騎士の役割で城のドアを順番に選択することです。各ドアの後ろには宝物またはドラゴンが隠れています。プレイヤーがドアの後ろにドラゴンを見つけた場合、ゲームは終了し、お金を失います。宝物が見つかった場合、最初の賭け金の額が増加し、プレイヤーが賞金を獲得するか、負けるか、すべてのレベルを通過するまでゲームが続きます。
ゲームを開始する前に、プレイヤーは賭け金の額とドラゴンの数を指定する必要があります。
合計として 10 を入力し、ドラゴンを 1 つ残して、送信されるリクエストを確認します。これは、どのブラウザでも開発者ツールから実行できます。Chromium では、ネットワーク タブがこれを実行します。
ここでは、リクエストが/srv/api/v1/dungeon
エンドポイントに送信されたことも確認できます。
ペイロードタブにはリクエスト本文自体がJSON形式で表示されます。
最初の 2 つのパラメータは明らかです。UI から選択しました。最後のパラメータは、ご想像のとおり、 timestamp
、つまり 1970 年 1 月 1 日から経過した時間で、Javascript の一般的な精度であるミリ秒単位です。
未解決のパラメータが 1 つ残ります。それは署名そのものです。署名がどのように形成されるかを理解するために、[ソース] タブに移動します。この場所には、ブラウザーが読み込んだサービスのすべてのリソースが含まれています。サイトのクライアント部分のすべてのロジックを担当する Javascript も含まれます。
このコードは縮小されているため、理解するのはそれほど簡単ではありません。すべてを難読化解除することはできますが、それは長くて面倒なプロセスであり、多くの時間がかかります (ソース コードの量を考慮すると)。そのため、私はそれを実行する準備ができていません。
2 番目でより簡単なオプションは、キーワードでコードの必要な部分を見つけてデバッガーを使用することです。サイト全体の動作を知る必要はなく、署名がどのように生成されるかを知るだけでよいので、私はこれを実行します。
したがって、コードの生成を担当するコード部分を見つけるには、 CTRL+SHIFT+F
キーの組み合わせを使用してすべてのソースを検索し、リクエストで送信されたsign
キーへの値の割り当てを探します。
幸いなことに、一致するものは 1 つだけなので、正しい方向に進んでいることになります。
一致したものをクリックすると、署名自体が生成されるコードセクションにアクセスできます。コードは以前と同様に難読化されているため、読み取るのは依然として困難です。
コード行の反対側にブレークポイントを設定し、ページを更新して「ドラゴン」で新しい入札を行います。これで、スクリプトは署名が形成された瞬間に正確に動作を停止し、いくつかの変数の状態を確認できます。
呼び出される関数は 1 文字で構成され、変数も同様ですが、問題ありません。コンソールに移動して、それぞれの値を表示できます。状況が明らかになっていきます。
最初に出力した値は、関数である変数H
の値です。コンソールからそれをクリックすると、コード内で宣言されている場所に移動できます。以下にリストを示します。
これはかなり長いコード スニペットですが、ヒントは SHA256 でした。これはハッシュ アルゴリズムです。また、関数に 2 つのパラメーターが渡されていることもわかります。これは、これが単なる SHA256 ではなく、秘密を持つ HMAC SHA256 である可能性を示唆しています。
おそらくここに渡される変数(コンソールにも出力されます)は次のとおりです。
10;1;6693a87bbd94061678473bfb;1732817300080;gRdVWfmU-YR_RCuSkWFLCUTly_GZfDx3KEM8
- HMAC SHA256 操作が直接適用される値。31754cff-be0f-446f-9067-4cd827ba8707
秘密の役割を果たす静的定数です。
これを確かめるために、関数を呼び出して想定されるシグネチャを取得します。
ここで、HMAC SHA256 をカウントするサイトにアクセスし、値を渡します。
そして、入札時にリクエストで送信されたものと比較します。
結果は同一で、私の推測が正しかったことを意味します。実際には、静的シークレットを使用した HMAC SHA256 が使用され、レート、ドラゴンの数、その他のパラメータを含む特別に形成された文字列が渡されます。これについては、この記事の後半で詳しく説明します。
アルゴリズムは非常にシンプルでわかりやすいものです。しかし、それでもまだ十分ではありません。作業プロジェクト内でペンテストで脆弱性を見つけることが目標である場合、Burp Suite を使用して独自のクエリを送信する方法を学ぶ必要があります。
そして、これには間違いなく自動化が必要であり、それについてこれからお話しします。
署名生成のアルゴリズムを理解しました。次は、リクエストを送信するときに不要なものをすべて抽象化するために、署名を自動的に生成する方法を学びます。
ZAP、Caido、Burp Suite、その他のペンテスト ツールを使用してリクエストを送信できます。この記事では、最もユーザー フレンドリーでほぼ完璧であると思われる Burp Suite に焦点を当てます。Community Edition は公式サイトから無料でダウンロードでき、すべての実験に十分です。
Burp Suite は、そのままでは HMAC SHA256 を生成する方法を知りません。そのため、これを実行するには、Burp Suite の機能を補完する拡張機能を使用できます。
拡張機能はコミュニティのメンバーと開発者自身によって作成され、組み込みの無料 BApp ストア、Github、またはその他のソース コード リポジトリを通じて配布されます。
選択できるパスは 2 つあります。
これらのパスにはそれぞれ長所と短所がありますので、両方を紹介します。
既製の拡張機能を使用する方法が最も簡単です。BApp Store からダウンロードし、その機能を使用してsign
パラメータの値を生成します。
私が使用した拡張機能はHackvertorと呼ばれます。これを使用すると、XML のような構文を使用できるため、さまざまなデータを動的にエンコード/デコード、暗号化/復号化、ハッシュ化できます。
Burp をインストールするには、次のものが必要です。
拡張機能タブに移動します
検索に「Hackvertor」と入力
リストから見つかった拡張機能を選択します
インストールをクリック
インストールされると、Burp に同じ名前のタブが表示されます。そこにアクセスすると、拡張機能の機能と、それぞれを組み合わせることができる利用可能なタグの数を評価できます。
たとえば、タグ<@aes_encrypt('supersecret12356','AES/ECB/PKCS5PADDING')>MySuperSecretText<@/aes_encrypt>
を使用して、対称 AES で何かを暗号化できます。
秘密とアルゴリズムは括弧で囲まれ、タグの間には暗号化されるテキスト自体が入ります。Repeater、Intruder、その他の Burp Suite 組み込みツールでは、どのタグも使用できます。
Hackvertor 拡張機能を使用すると、タグ レベルで署名を生成する方法を記述できます。実際のリクエストの例でこれを実行します。
そこで、Dragon Dungeon で賭けを行い、この記事の冒頭でインターセプトしたのと同じリクエストを Intercept Proxy でインターセプトし、それを Repeater にストレスを与えて編集し、再送信できるようにします。
ここで、 ae04afe621864f569022347f1d1adcaa3f11bebec2116d49c4539ae1d2c825fc
値の代わりに、Hackvertor によって提供されるタグを使用して HMAC SHA256 を生成するアルゴリズムを置き換える必要があります。
Формула генерации у меня получилась следующая <@hmac_sha256('31754cff-be0f-446f-9067-4cd827ba8707')>10;1;6693a87bbd94061678473bfb;<@timestamp/>000;MDWpmNV9-j8tKbk-evbVLtwMsMjKwQy5YEs4<@/hmac_sha256>
。
すべてのパラメータを考慮してください:
10
- 賭け金1
- ドラゴンの数6693a87bbd94061678473bfb
- MongoDB データベースからの一意のユーザー ID。ブラウザから署名を分析しているときに見ましたが、そのときは書きませんでした。Burp Suite でクエリの内容を検索することで見つけることができ、 /srv/api/v1/profile/me
エンドポイント クエリから返されます。
<@timestamp/>000
- タイムスタンプ生成、最後の 3 つのゼロは時間をミリ秒に調整しますMDWpmNV9-j8tKbk-evbVLtwMsMjKwQy5YEs4
- /srv/api/v1/csrf
エンドポイントから返され、各リクエストのX-Xsrf-Token
ヘッダーに置き換えられる CSRF トークン。<@hmac_sha256('31754cff-be0f-446f-9067-4cd827ba8707')>
および<@/hmac_sha256>
- 定数31754cff-be0f-446f-9067-4cd827ba8707
としてシークレットを使用して置換された値から HMAC SHA256 を生成するための開始タグと終了タグ。
重要な注意点:パラメータは、厳密な順序で;
を介して相互に接続する必要があります。そうしないと、署名が誤って生成されます。このスクリーンショットでは、レートとドラゴンの数を入れ替えています。
そこにすべての魔法が存在します。
ここで、正しいクエリを作成し、パラメータを正しい順序で指定して、すべてが正常に実行され、ゲームが開始されたという情報を取得します。つまり、Hackvertor は数式ではなく署名を生成し、それをクエリに代入して、すべてが機能しているということです。
ただし、この方法には大きな欠点があります。手作業を完全になくすことはできないのです。JSON でドラゴンのレートや数を変更するたびに、署名自体を変更して一致させる必要があります。
また、Proxy タブから Intruder または Repeater に新しいリクエストを送信する場合は、数式を書き直す必要があります。これは、さまざまなテスト ケースに多数のタブが必要な場合には非常に不便です。
この数式は、他のパラメータが使用される他のクエリでも失敗します。
そこで、これらの欠点を克服するために独自の拡張機能を作成することにしました。
Burp Suite の拡張機能は、Java と Python で作成できます。ここでは、よりシンプルで視覚的な 2 番目のプログラミング言語を使用します。ただし、事前に準備する必要があります。まず、公式 Web サイトから Jython Standalone をダウンロードし、次にダウンロードしたファイルへのパスを Burp Suite 設定に入力します。
その後、ソースコード自体と拡張子*.py
持つファイルを作成する必要があります。
基本的なロジックを定義するビレットはすでにあります。その内容は次のとおりです。
すべてが直感的にシンプルでわかりやすいです。
getActionName
- このメソッドは、拡張機能によって実行されるアクションの名前を返します。拡張機能自体は、あらゆるリクエストに柔軟に適用できるセッション処理ルールを追加しますが、これについては後で詳しく説明します。この名前は拡張機能の名前とは異なる場合があり、インターフェイスから選択できることを知っておくことが重要です。performAction
- 選択されたリクエストに適用されるルール自体のロジックがここに記述されます
どちらのメソッドも、 ISessionHandlingActionインターフェイスに従って宣言されます。
次に、 IBurpExtenderインターフェースについて説明します。これは、拡張機能のロード直後に実行され、拡張機能が動作するために必要な唯一の必須メソッドregisterExtenderCallbacks
を宣言します。
ここで基本的な設定が行われます。
callbacks.setExtensionName(EXTENSION_NAME)
- 現在の拡張機能をセッションを処理するアクションとして登録しますsys.stdout = callbacks.getStdout()
- 標準出力 (stdout) を Burp Suite 出力ウィンドウ (「拡張機能」パネル) にリダイレクトします。self.stderr = PrintWriter(callbacks.getStdout(), True)
- エラーを出力するためのストリームを作成しますself.stdout.println(EXTENSION_NAME)
- Burp Suiteの拡張機能の名前を出力しますself.callbacks = callbacks
- コールバック オブジェクトを self 属性として保存します。これは、後で拡張コードの他の部分で Burp Suite API を使用するために必要です。self.helpers = callbacks.getHelpers()
- 拡張機能の実行時に必要となる便利なメソッドも取得します
準備はこれで完了です。これで拡張機能をロードして、正常に動作するか確認できます。これを行うには、[拡張機能] タブに移動して [追加] をクリックします。
表示されるウィンドウで、
「次へ」をクリックします。
ソース コード ファイルが適切にフォーマットされている場合、エラーは発生せず、[出力] タブに拡張機能の名前が表示されます。これは、すべてが正常に動作していることを意味します。
拡張機能は読み込まれて動作しますが、読み込まれたのはロジックのないラッパーだけなので、リクエストに直接署名するためのコードが必要です。すでにコードを作成しており、以下のスクリーンショットに表示されています。
拡張機能全体の動作は、リクエストがサーバーに送信される前に、拡張機能によって変更されるというものです。
まず拡張機能がインターセプトしたリクエストを取得し、その本体からレートとドラゴンの数を取得します。
json_body = json.loads(message_body) amount_currency = json_body["amountCurrency"] dragons = json_body["dragons"]
次に、現在のタイムスタンプを読み取り、対応するヘッダーからCSRFトークンを取得します。
currentTime = str(time.time()).split('.')[0]+'100' xcsrf_token = None for header in headers: if header.startswith("X-Xsrf-Token"): xcsrf_token = header.split(":")[1].strip()
次に、リクエスト自体がHMAC SHA256を使用して署名されます。
hmac_sign = hmac_sha256(key, message=";".join([str(amount_currency), str(dragons), user_id, currentTime, xcsrf_token]))
関数自体と、秘密とユーザーIDを表す定数は、先頭で事前に宣言されていました。
def hmac_sha256(key, message): return hmac.new( key.encode("utf-8"), message.encode("utf-8"), hashlib.sha256 ).hexdigest() key = "434528cb-662f-484d-bda9-1f080b861392" user_id = "zex2q6cyc4ba3gvkyex5f80m"
次に、値がリクエスト本文に書き込まれ、JSONに変換されます。
json_body["sign"] = hmac_sign json_body["t"] = currentTime message_body = json.dumps(json_body)
最後のステップは、署名と修正を加えたリクエストを生成し、
httpRequest = self.helpers.buildHttpMessage(get_final_headers, message_body) baseRequestResponse.setRequest(httpRequest)
これでソース コードの記述は完了です。これで、Burp Suite で拡張機能を再読み込みし (スクリプトを変更するたびに実行する必要があります)、すべてが機能することを確認できます。
ただし、まずリクエストを処理するための新しいルールを追加する必要があります。これを行うには、[設定] の [セッション] セクションに移動します。ここには、リクエストの送信時にトリガーされるさまざまなルールがすべて表示されます。
特定の種類のリクエストでトリガーされる拡張機能を追加するには、[追加] をクリックします。
表示されるウィンドウでは、すべてをそのままにして、ルールアクションで追加を選択します。
ドロップダウン リストが表示されます。その中から、「Burp 拡張機能の呼び出し」を選択します。
そして、リクエストを送信するときに呼び出される拡張機能を指定します。私は 1 つ持っていますが、それは Burp Extension です。
拡張機能を選択したら、[OK] をクリックします。次に、[スコープ] タブに移動して、次の内容を指定します。
ツール スコープ - Repeater (Repeater を介して手動でリクエストを送信すると、拡張機能がトリガーされます)
URL スコープ - すべての URL を含めます (送信するすべてのリクエストで機能するように)。
下のスクリーンショットのように動作するはずです。
[OK]をクリックすると、拡張ルールが一般リストに表示されました。
最後に、すべてを実際にテストできます。これで、クエリを変更して、署名が動的に更新される様子を確認できます。クエリが失敗しても、それは署名に問題があるからではなく、負のレートを選択したためです (お金を無駄にしたくないだけです 😀)。拡張機能自体は機能し、署名は正しく生成されます。
すべては素晴らしいのですが、3つの問題があります。
これを解決するには、サードパーティのrequests
ではなく、組み込みの Burp Suite ライブラリによって実行できる 2 つの追加リクエストを追加する必要があります。
これを実現するために、クエリをより便利にするためにいくつかの標準ロジックをラップしました。Burp の標準メソッドを通じて、クエリとのやり取りはプレーンテキストで行われます。
def makeRequest(self, method="GET", path="/", headers=None, body=None): first_line = method + " " + path + " HTTP/1.1" headers[0] = first_line if body is None: body = "{}" http_message = self.helpers.buildHttpMessage(headers, body) return self.callbacks.makeHttpRequest(self.request_host, self.request_port, True, http_message)
そして、必要なデータ、CSRF トークン、および UserID を抽出する 2 つの関数を追加しました。
def get_csrf_token(self, headers): response = self.makeRequest("GET", "/srv/api/v1/csrf", headers) message = self.helpers.analyzeRequest(response) raw_headers = str(message.getHeaders()) match = re.search(r'XSRF-TOKEN=([a-zA-Z0-9_-]+)', raw_headers) return match.group(1) def get_user_id(self, headers): raw_response = self.makeRequest("POST", "/srv/api/v1/profile/me", headers) response = self.helpers.bytesToString(raw_response) match = re.search(r'"_id":"([a-f0-9]{24})"', response) return match.group(1)
そして送信ヘッダー内のトークン自体を更新することで
def update_csrf(self, headers, token): for i, header in enumerate(headers): if header.startswith("X-Xsrf-Token:"): headers[i] = "X-Xsrf-Token: " + token return headers
署名関数は次のようになります。ここで重要なのは、リクエストで送信されるすべてのカスタム パラメータを取得し、それらの末尾に標準のuser_id
、 currentTime
、 csrf_token
を追加し、区切り文字として;
を使用してすべてをまとめて署名することです。
def sign_body(self, json_body, user_id, currentTime, csrf_token): values = [] for key, value in json_body.items(): if key == "sign": break values.append(str(value)) values.extend([str(user_id), str(currentTime), str(csrf_token)]) return hmac_sha256(hmac_secret, message=";".join(values))
メインフローは数行に削減されました:
OrderedDict
使用していることにここで注意することが重要です。 csrf_token = self.get_csrf_token(headers) final_headers = self.update_csrf(final_headers, csrf_token) user_id = self.get_user_id(headers) currentTime = str(time.time()).split('.')[0]+'100' json_body = json.loads(message_body, object_pairs_hook=OrderedDict) sign = self.sign_body(json_body, user_id, currentTime, csrf_token) json_body["sign"] = sign json_body["t"] = currentTime message_body = json.dumps(json_body) httpRequest = self.helpers.buildHttpMessage(final_headers, message_body) baseRequestResponse.setRequest(httpRequest)
念のためスクリーンショット
ここで、カスタム パラメータが 2 ではなく 3 になっている他のゲームにアクセスしてリクエストを送信すると、正常に送信されることがわかります。これは、拡張機能がユニバーサルになり、すべてのリクエストで機能するようになったことを意味します。
アカウント補充リクエストの送信例
拡張機能は Burp Suite の不可欠な部分です。多くの場合、サービスでは、他の誰も作成しないカスタム機能を事前に実装します。そのため、既製の拡張機能をダウンロードするだけでなく、独自の拡張機能を作成することも重要です。この記事では、その点について説明しようとしました。
とりあえず以上です。体調を整えて、病気にならないようにして下さい。
拡張機能のソース コードへのリンク: *クリック* 。