はじめに
こんにちは。
最近、業務で脆弱性診断はほぼ行っておらず、プライベートでもバグバウンティしていない。微妙な脆弱性なのか判断しづらいものを報告したりはしているのだが、反応はない。
何もしてないように見えるのも良くない気がしてきたので、プライベートで受理されやすそうな脆弱性を探して、IPAに報告しようかと思ったのだが、普通に探すのも面白くない。同じようなことをやる気はなかなか起きないのだ。
何か目先を変えるべく、脆弱性スキャナーを使って探すことにした。Burp Proを使えばいいのだが、プライベートの一円にもならないもののために貴重な金は一円たりとも使いたくない。毎年のように報奨金をもらったお金でBurpのライセンスを買うと言ってたものの、結局一度も買わずじまいだ。
ということで、脆弱性を探すのに脆弱性スキャナーを自作してみることにした。車輪の再生産だ。すでにChatGPTに課金していたのでこれに働いてもらうことにする。
理由まとめ
- Burpのライセンス高いじゃん
- nucleiはちょっと使いにくいしfuzzingが限定的にしかできないし改行挟んだ検知にバグがあるし(将来的にはこれでよくなる未来が来ると思うけど)
- ChatGPT頭良くなってきたじゃん
- できたら楽しそうじゃん
脆弱性スキャナーのターゲットとなる脆弱性と環境
環境
汎用性を求めるなら言語やサーバーに合わせたペイロードを書く必要があるが、とりあえず手元のテスト環境に合わせる。
- Windows/Linux
- MySQL
- php
脆弱性
機械的にレスポンスの解析で検出できるインジェクション系をターゲットにする。ターゲットは以下の通り。変なリクエストを送ると30xが返ることは意外と多いのでオープンリダイレクトって判定が意外と面倒な気がする。
- XSS
- SQLi
- パストラバーサル
- RCE
- SSTI
大まかなスキャナーの設計
- ぼくのかんがえた Burp Proのスキャナー(とRepeaterとIntruder)の代替
- 今のところプロキシ機能は実装しない
アプリケーションのイメージ
- Webアプリとして作る
アプリとして作る方がパフォーマンスはよさそうだけど、好みの問題
動作の流れ
- Webページからテストしたいリクエストを入力して、ボタンをクリックすると検査
- htmlページのJavaScriptからWebAPIを叩く
- WebAPIの言語は何でもいい
- とりあえずnodejsで実装してみる
脆弱性の見つけ方のイメージ
- リクエストを送信して、レスポンスから脆弱性が疑われる挙動を見つける
- レスポンスの内容(入力値が反射されているかとか)
- レスポンスにかかる時間がどうかとか(遅延の発生)
- 状態遷移を見てるような面倒なアプリのことは今のところ気にしない
- 今のところペイロードに凝るよりは雑に検出して最後は手動で確認する仕様
- 本当に何かしたいならSQLMapとか使えばいいじゃん
その他実現したいこと
- リクエスト/レスポンスの保存と再現ができるようにする
- 思い出したときに再現確認したい
これくらい。
スキャナーの実装
早速実装する。
ChatGPTを使った実装
私立文系卒のプログラマーでもないおじさんよりはChatGPTの方がよっぽどいいコードを書けるポテンシャルがあるので(常に書けるとは言ってない)、がんばってChatGPTに書いてもらうことにする。記載されたコードに間違いがあっても責任は取れない(動いてはいる)。だいたいのコードで使用したモデルはChatGPT 4oだが、アップデートされた後は一部o1を使っている。o1の方がいいものを高い確率で出してくる気がする。テストは手動。
前述の通り htmlページからWeb APIを呼び出す形式を取る。まずは、フロントのイメージをしつつAPI側を作成する。
リクエストとレスポンスに関連するAPI
基本となるリクエストの送受信と保存を行うAPI群を作る。変な改変が行われず、思った通りのリクエストを送信してレスポンスを受信することを目標とする。
リクエストとレスポンスを行うAPI(/api/request)
まず基本として http/httpsのリクエストを送受信するAPIを作る。作るとは書いているが、いいものができるように祈りつつ作ってもらう(以下全て同じ)。ヘッダ、ボディを全て含むHTTPリクエストを丸ごと送って、レスポンスを受け取るAPIだ。
当初、nodejsで実装していたが、簡単に使えるhttp系のライブラリではクエリストリングの値がURLエンコードされるので断念。Pythonを使うことに。もう少し低レイヤーを直接触ればいいみたいだが、そこまでnodejsにこだわる理由もない。フレームワークにはflaskとflask_sqlalchemyを使っているが、ChatGPTに従ったまでだ。
conn = http.client.HTTPSConnection(host, context=context)
Pythonだとhttp.client.HTTPSConnection
を使って普通に接続するだけでURLエンコードされず送信される。
オプション
- プロキシ経由するオプション
- 保存オプション
プロキシ経由でのアクセスの場合は
conn = socket.create_connection((proxy_host, proxy_port))
target_port = 443 if protocol == "https" else 80 # Determine port based on protocol
conn.sendall(f"CONNECT {host}:{target_port} HTTP/1.1\r\nHost: {host}\r\n\r\n".encode())
みたいにCONNECTメソッドを叩く必要がある。テストで使っただけなのでバグが残存しているかもしれないが、結局デバッグ以外でアップストリームのプロキシを通することはないのであまりプロキシを通す機能は必要ないかもしれない。面倒なので接続先は 127.0.0.1:8080
固定にしている。
保存オプションは作らず、別のAPIを作った(後述)。
その他の機能
- Content-Length の再計算
POSTリクエストのbodyを変更した際、Content-Lengthを合わせる必要があるが、今のところリクエストを変形させる方のAPIで再計算を行っている。API側で再計算させるオプションがあった方がいいかもしれない。
- gzip圧縮の展開
一応実装した。
ここまで書いて何ではあるが、リクエスト/レスポンス部分はわざわざ自力でがんばらなくても、実績のあるcurlとか使えばいいような気がする、まあ、いいじゃない。
リクエストとレスポンスを保存するAPI(/api/save)
リクエストAPIに保存オプションを付ければ良かっただけのような気がするけど、動作確認のときわかりやすくしたかったので分割した。パフォーマンスを考えるとお勧めはしない。
機能としては /api/request
のリクエストとレスポンスと実行時間を保存して、UUIDを返す。リクエストにすでにあるUUIDを含めると上書き保存するようにしている。これによって編集もできることになりいろいろ便利になった気がする。
実際はJavaScriptによって、画面の保存オプションにチェックが入っている場合だけ、保存APIにアクセスすることになる。
保存されたリクエスト/レスポンスを取得する(/api/load/uuid/)
/api/save
APIで保存したリクエストを呼び出すAPI。リクエストとレスポンスと実行時間が返る。
別途、全リクエストを取得する /api/load/all
APIも作っているがデバッグ用で実際には使ってはいない。
この3つを実装したことでrepeaterの実現が可能になる。
Intruder
フロントのJavaScriptをがんばって書けばIntruderの実装も可能だが(軽く実装はした)、これだけではいろいろ面倒だったのでとりあえず先送りにした。それっぽいAPIを作ったので(後述)そっちを使った方が手堅いと思う。
複数のリクエストをひとまとめにして扱うためのAPI
今回行いたい脆弱性スキャンのためには、元のリクエストからさまざまな改変を行ったリクエストを送信する必要がある。
元のリクエストが
http://localhost/test.php?key=value
だとすると
http://localhost/test.php?key="><s>
http://localhost/test.php?key='||'
のようなリクエストを作成し、その結果を取得し、結果について考察するという流れになる。
毎回作成して送信してもいいのだが、途中で止めることや、再送することを考えるとちょっとそれはやりたくない。
ということで、この元のリクエストとペイロードを付与したリクエストをリクエストリストとしてひとまとめにして扱えるようにしたい。偉そうに書いてるが、まとめるだけだ。
ここからはそのような複数リクエストをリストとして扱うためのAPIを作成する。
リクエストを作成されたリストに追加するAPI (/api/request-list/add
)
まず、リクエストを指定されたリストに追加するAPIを作成する。リクエストを保存した際にUUIDが付与されているが、そのUUIDとリスト名を指定すると、リクエストがリストに追加されていく。
追加だけでなく、以下のAPIもそれぞれ実装した。偉そうに書いてるけど、やってることはUUIDを追加編集削除してるだけだ。
- リストに追加されたリクエスト一覧の取得(
/api/request-list/<list_name>
) - リクエストの編集(
/api/request-list/edit/<uuid>
) - 削除(
/api/request-list/delete/<uuid>
)
モデルは以下のようになっている。
class RequestList(db.Model):
id = db.Column(db.Integer, primary_key=True)
uuid = db.Column(db.String(36), unique=True, nullable=False, default=str(uuid4()))
list_name = db.Column(db.String(100), nullable=False )
requests = db.relationship('SavedRequest', backref='request_list', lazy=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
modified_at = db.Column(db.DateTime, default=datetime.utcnow)
これで、脆弱性を探すためのリクエストを格納できるようになった。
リクエストに脆弱性を見つけるためのペイロードを貼り付けるためのAPI群
1つのリストに複数のリクエストを格納できるようになったが、今のところ1つのリクエストしか扱っていない。
ここからはこの1つのリクエストに対して、脆弱性を検出するためのペイロードをさまざまな場所に付与するAPIを作成する。
http://localhost/test.php?key=value&key2=value
を
http://localhost/test.php?key="><s>1</s>&key2=value
http://localhost/test.php?key=value&key2="><s>1</s>
http://localhost/test.php?key="+"&key2=value
http://localhost/test.php?key=value&key2="+"
のように複数のリクエストに最終的には増殖させるイメージだ。
リクエストにマークを付けるAPI(/api/mark-request-list
)
まずは、ペイロードを付与する場所を自動で決めるAPIを作る。
BurpのIntruderでAuto§
ボタンをクリックすると
GET /xsssample/xss-showcase-001.php?word=§test§ HTTP/2
のようにマークが付けられるが、同じことを実現する。具体的には、リクエストの =を探して値の方に§§
を付ける処理を行う。保存されたUUIDが返る。のでそれを参照すると元にリクエストに§§
が付いたリクエストが見えることになる。
マークをペイロードに置き換えるAPI(/api/replace-marks
)
次に、この§§マークが付いた箇所を脆弱性検出用のペイロードに置き換えるAPIを実装する。
http://localhost/test.php?key=§value§
を
http://localhost/test.php?key="><s>1</s>
http://localhost/test.php?key="+"
のように機械的に置き換えるだけだ。
置き換えたいリクエストのUUIDを指定すると、置き換えたリクエストを指定されたリストに保存する。リストの指定がない場合は新規に作成される。ペイロードとなる文字列は指定した辞書ファイルから読み込ませている。
bodyは普通のapplication/x-www-form-urlencoded
だけでなくmultipart/form-data
とapplication/json
にも対応しているはず。
この2つのAPIで、脆弱性を探すためのリクエストを作成することが可能となる。
実際にはJavaScriptで、フォームに入力してボタンをクリックすると連続してAPIが呼び出されて、脆弱性を探すリクエストが準備されるようになっている。
これによりIntruder的なことも可能になるはず。
脆弱性を判定するためのAPI群
脆弱性を見つけるためのリクエストを作成したので、このレスポンスから脆弱性を見つけるための処理を行うAPIを作成する。
以下の2つを作った。他の判定が必要になったら随時追加する。
リクエストの内容に検索語が含まれているかチェック(/api/check-response-words)
このAPIではXSSの判定のように送信したワードがそのままレスポンスに表示されるような、レスポンスから機械的に判定できるものを想定している。単純にレスポンスに辞書のワードが含まれていたり、passwdファイルの中身のような文字列があったり、計算結果と思しき数字があったりすると脆弱性の疑いありという報告を行う。
実行時間を比較するAPI(/api/compare-executiontime)
UUIDを2つ入力すると、それぞれの実行時間を比較してくれる。差があれば脆弱性の疑いがあるという見立てだ。RCEやSQLインジェクションを雑にタイムベースで検出することにしたのでこれを使う。
リクエストリストをひとまとめにしてプロジェクトとして扱うAPI群
ここまでで1つのリクエストに対して、脆弱性を検出するためのペイロードを付与した複数のリクエストを作成し、指定のリストに保存する処理を行うことができるようになった。
ここからはその複数リクエストリストをプロジェクトとしてひとまとめにするAPIを作る。
先ほどは
http://localhost/test.php?key=value
のような1つのリクエストにペイロードを展開したものをひとまとめにしたが、ここでは
http://localhost/test.php?key=value
http://localhost/test/test.php?key=value
http://localhost/user?id=1
のような複数の別のURL(とペイロードを展開したもの)をひとまとめにするイメージだ。これで1アプリのURL一覧を1プロジェクトとして縦断的にまとめるようなことを期待している。別に制限してないのでどんなドメインでもいいんだけど。
構造としてはこんなかんじで非常にシンプル。
class Project(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), unique=True, nullable=False)
requests = db.relationship('ProjectRequest', backref='project', lazy=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
modified_at = db.Column(db.DateTime, default=datetime.utcnow)
ここでは
-
プロジェクト作成API(
/api/project
) -
プロジェクトの情報を更新するAPI(
/api/project/update_request_id
) -
プロジェクトに入っているリクエストリストを取得するAPI(
/api/project/<project_name>
) -
プロジェクトからリクエストリストを削除するAPI(/api/project/remove_request)
を作成した。
これを組み合わせることで複数のリクエストに対し、脆弱性を見つけるペイロードを貼り付けたリクエストを準備して、実行し、その結果を取得することが可能となった。
将来的な展開としては、ここで集めたリクエストに対して横断的に何かするみたいなことも考えてはいる。
そういえばまだ認証とかセッション管理とかしてないが、一人で使うものなので必要ないか。
フロントの作成
最後にこれらのAPIを使うためのフロントエンドのことを書く。時系列としてはAPIを作っているときに同時に作って追加修正を繰り返しているが、最終的にできあがったものについて書く。htmlとjavascriptを使っている。デザインするのが面倒なのでフレームワークにbootstrapを使っている。そしてjQueryが使われているが、これはモーダルダイアログのためにChatGPTが勝手に使っていた。今のところ画面は以下の通り。3つしかない。シンプルだ。どのAPIを使っているのかは、上の文章を見て想像してほしい。
プロジェクトの管理ページ(project)
プロジェクト名でプロジェクトの情報を呼び出すか新規に作成するページ。基本ここから開くのでindexにすればよかった。
ボタンをクリックするとモーダルダイアログが開き、1つずつURLかリクエストを入力してボタンをクリックすると、脆弱性検出用のペイロードが付与され展開され保存され、タイルとして表示される。読み込む辞書も指定はできるようになっている。
画面はこのようになっている
追加された後はこんなかんじ。各タイルをクリックするとリクエストの内容が読める。リクエストを修正して保存することもできる。
脆弱性を探すページ(list)
テストボタンをクリックすると、脆弱性を探すページが開く。やってることはペイロードが付与されたリクエストを順番に並べている。「Execute Request」ボタンをクリックするとリクエストが送受信される。デフォルトでは1秒ごとに送信され、終わるとボタンの色が変わる。
ボタンをクリックするとリクエストとレスポンスの詳細が表示される
脆弱性が見つかると、ここに表示される。
リクエストの送受信を行う (repeater)
list画面の各リクエストにあるrepeaterリンクをクリックするとリクエストを再送できるrepeater画面が開く。
別のページだが、リクエストを追加する画面と基本的に違いはない。入力欄に入ったリクエストを実行して、リクエストとレスポンスと実行時間を表示する。
脆弱性の発見と報告
自分のXSS再現ページなどで動作確認してXSSを検出することができたので、当初の目的である、オープンソースのWebアプリケーションの脆弱性を探すことにする。
初手として、去年脆弱性を見つけてIPA経由で報告し、すでに修正が行われたWebアプリケーションがあるのでこれを動作確認も兼ねてテストすることにした。
ツールはわりと予想通りに動いており、ちゃんと動いて良かったなあと思いつついくつかのURLをテストしていると、怪しいパラメーターを持つURLがある。ツールを実行すると、早速脆弱性が見つかってしまった。単純な脆弱性だったので、単純なペイロードで見つかった。
作るのはわりと時間がかかっているのに、あっさり見つかってしまい拍子抜けしてしまった。
とりあえずIPAに報告すると、一昨日受理された連絡が来た。修正されるとIPAのページで公表されるので、詳細はそれまで待ってほしい。
あっさり終わって良くなかったようなモヤモヤとした気持ちはあるが、ひとまずこれについては終了。少し休憩して、やる気が復活すれば機能を追加したり、改良したりするかもしれない。
今後拡張するには
今のところ手を動かす気力が湧かないが、このへんあったら便利かもなあということを書く。
プロキシ機能
burpの代替といいつつ設定する元のリクエストをburpから取得しているという根本的な問題があるので、リクエストを取得する手段を実装する必要がある。
- Chromeの拡張
- そんなに難しくはないのでやる気の問題
- pythonで実装
- mitmproxyをimportすれば超簡単
- mitmproxyを使う
- これでいいのでは or ZAP使う
入力の拡充
- ヘッダを追加する
X-Forwarded-Host ヘッダとか追記して送信することで脆弱性を見つける方法もある。フロントエンドで対応可能だけどAPI作ってもいいかも。
- パラメーター側もにペイロード
parama=data
でdata側にしかペイロードを貼り付けてないのでパラメーター側に追記するとかパラメータを配列にするとかもできた方がいい
- Mass Assigment
プロジェクトで送信パラメーターを全部覚えてて、追加するとかはできそう
脆弱性検出のロジック拡充
今のところ単純なペイロードと単純な検出ロジックしか書いてないので、見つかる脆弱性も限定されている。世の中で見つかる脆弱性の多くはこのような単純なペイロードでも見つかるものだとは思うが、もう少し踏み込む必要はある。
- 今のところオープンリダイレクトすら見つけられない
- 埋めこみXSSとかセカンドオーダーSQLインジェクションとか入力と発現が異なるケースはまったくダメ
- 前後のリクエストの比較やステップを踏む必要があるものに対応する必要
- MySQL以外のSQLサーバーに対応していない
ここを深掘りするのがプロっぽい気がするが、仕事じゃないのでモチベーションが全く湧かない
まとめ
作成に3ヵ月かかって、一瞬で脆弱性が見つかった。一応形にはなったのでよかったが、ターゲットの選定を間違ったとも言える。もうちょっと探す方でも試行錯誤したかった。
結局のところプライベートでスキャナーを使ってゴリゴリ脆弱性を探すような習慣がないので、1つ脆弱性を見つけると満足してしまった。作ってる過程が楽しかったのでいいか。
SNSによると、コードはプロンプトを書くだけでAIが作ってくれるらしいので、誰でも簡単にこれと同じような脆弱性スキャナーを作れると思う。なのでここではコードは公表しない。
業務とは全く関係ない個人的な取り組みなので、会社に問い合わせたりするのは本当に勘弁してほしい(二回目)。
個人的に問い合わせていただくのは大歓迎だが、電話には出ない。
結論
Burp使おう