こんにちは。技術部エンジニアの松永です。
PC・スマホアプリケーションや組込機器の脆弱性診断の現場では、クライアント(アプリや機器)とサーバー間の通信に Web API が使用されるケースによく遭遇します。
Web APIの診断には、弊社では主に Burp Suite Professional を使用しています。
通常のWebアプリケーションとは異なり、Web APIではバイナリフォーマットのメッセージが使用されたり、リクエストの暗号化や改ざん検知が施されていたりします。
今回はバイナリフォーマットのメッセージの例として、ボディ部が MessagePack でフォーマットされたHTTPリクエスト/レスポンスを、Burpを使って試験する方法を紹介します。
バイナリデータの可視化や編集を効率的に行うには、Burpを拡張するツール(Burp Extension)の開発が必要になります。
開発したExtensionをGitHubで公開しましたので、この記事ではExtensionの導入方法やテストアプリの使い方を紹介したいと思います。
MessagePackとは
一言で言うと、バイナリフォーマット版のJSONです。
軽量かつ高速に動作し、多数のプログラミング言語のライブラリが存在します。
詳細は公式ドキュメントをご覧ください。
Burp Extension
以下のGithubリポジトリで公開しています。
https://github.com/CyberDefenseInstitute/burp-msgpack
MessagePackはJSONとの親和性が高いことから、HTTPメッセージの送受信時はMessagePack、閲覧・編集時はJSONとすることで作業が容易になります。
burp-msgpackはこの変換を自動的に行うBurp Extensionです。
今回はPythonで実装しています。ちょっとした拡張の場合、Pythonが一番楽ではないかと思います。
Pythonの場合、 Burp Extender API はburpモジュールから使用できます。
また、BurpはPythonで書かれたExtensionの実行にJythonを使用するため、swing等javaのGUIを使用可能ですが、今回は特に触れません。
burp-msgpackの古い実装ではswingを使用していますので、興味のある方はご覧ください。
burp-msgpackを簡単に説明します。
- Content-Typeヘッダで示されるMIMEが "application/*msgpack" (*はワイルドカード)であるリクエスト/レスポンスに対し、以下の処理を行う
- MessageEditor(ProxyやRepeaterに表示されるRequest/Response)に "mpack" タブを追加する
- mpackタブにはMessagePackからJSONに変換したRequest/Responseが表示される
- mpackタブのリクエストはJSONの状態で編集が可能で、自動的に実際のリクエストに反映される
- 以下のBurpツールから送信されるリクエストがBurpのターゲット・スコープに含まれる場合、JSONからMessagePackへの変換を行う
- Scanner
- Intruder
- Extender
ここからはコードの解説をします。
Extensionのロード
JavaでBurp Extensionを実装する場合、IBurpExtenderという抽象クラス(interface class)を実装(implement)した具象クラス(concrete class)を作成します。
registerExtenderCallbacksというインターフェースがBurpから呼び出されるので、registerExtenderCallbacksをインプリメントして必要な初期化処理やExtensionの登録処理を実装することになります。
Pythonの場合も同様で、IBurpExtenderを継承したクラスがBurpから呼び出されます。
burp-msgpackでは、IBurpExtenderCallbacks#registerMessageEditorTabFactoryでメッセージエディタタブのファクトリークラスの登録を行い、IBurpExtenderCallbacks#registerHttpListenerでHTTPメッセージのリスナーをBurpに登録しています。
メッセージエディタタブのファクトリークラスを登録した場合、BurpからIMessageEditorTabFactory#createNewInstanceが呼び出されるので、メッセージエディタタブクラスを生成します。
class BurpExtender( IBurpExtender, IMessageEditorTabFactory ):
def registerExtenderCallbacks( self, callbacks ):
self._callbacks = callbacks
callbacks.setExtensionName( "Burp MessagePack" )
callbacks.registerMessageEditorTabFactory( self )
callbacks.registerHttpListener( HttpListener( callbacks ) )
def createNewInstance( self, controller, editable ):
return MessageEditorTab( controller, editable, self._callbacks )
mpackタブ(エディタ)の処理
次はメッセージエディタタブクラス(MessageEditorTab)を見ていきます。
ここにはProxyやRepeaterなどに表示されるメッセージエディタへ追加する "mpack" タブの処理を記述します。
このタブにはMessagePackをJSONに変換したHTTPメッセージを表示し、内容が編集された場合はMessagePackへ再変換する役割があります。
テキストエディタのUIにはBurpが提供する IBurpExtenderCallbacks#createTextEditor を使用しています。
class MessageEditorTab( IMessageEditorTab, MpackJsonHelper ):
def __init__( self, controller, editable, callbacks ):
MpackJsonHelper.__init__( self, callbacks )
self._controller = controller
self._editable = editable
self._editor = self._callbacks.createTextEditor()
self._editor.setEditable( editable )
def getTabCaption( self ):
return "mpack"
def getUiComponent( self ):
return self._editor.getComponent()
IMessageEditorTab#isEnabled は、任意のHTTPメッセージが選択されたタイミングで呼び出されます。
ここで真(True)を返すとBurpがタブ(mpackタブ)を追加してくれます。
burp-msgpackは、Content-Type が MessagePack のものである場合、Trueを返します。
タブが表示される場合、後続の IMessageEditorTab#setMessage に続きます。
def isEnabled( self, content, isRequest ):
if content is None:
return False
info = self.analyzeMessage( content, isRequest )
isMessagePack = self.isMessagePack( info.getHeaders() )
return isMessagePack
IMessageEditorTab#setMessage ではHTTPメッセージをMessagePackからJSONに変換し、表示します。
def setMessage( self, content, isRequest ):
info = self.analyzeMessage( content, isRequest )
newRaw = self.toJson( content, info )
self._editor.setText( newRaw )
self._content = content
self._isRequest = isRequest
IMessageEditorTab#getMessage / IMessageEditorTab#isModified は、他のタブへの表示切り替えが発生したタイミングや、リクエストの送信が発生したタイミングでBurpから呼び出されます。
具体的にはメッセージエディタ内の mpackタブ から Rawタブ へ表示を切り替えた時や、RepeaterでGoボタンをクリックした時などです。
IMessageEditorTab#getMessage で返した内容は実際に送信するリクエストに反映されるので、burp-msgpackではJSONからMessagePackへ再変換したメッセージを返します。
MessagePackはバイナリ文字列(バイト配列)を扱うことができますが、JSONはバイナリを扱えないため、編集内容によっては変換に失敗することがあります。
もちろん、JSONフォーマットを崩した場合も変換はできません。
変換に失敗した場合は未編集のメッセージをそのまま返し、さらに内部的にはBurpのAlertsタブにアラートを表示するようにしています。
def getMessage( self ):
content = self._editor.getText()
info = self.analyzeMessage( content, self._isRequest )
try:
newContent = self.toMpack( content, info )
except:
return self._content
return newContent
def isModified( self ):
return self._editor.isTextModified()
HTTPリスナー
最後に、HTTPリスナークラスを説明します。
IHttpListener#processHttpMessage では、Burpが送受信するHTTPメッセージを取得・変更することが可能です。
Scanner や Intruder では上記の仕組みが使えないため、リクエストが送信されるタイミングで MessagePack への再変換が行えるようにしました。
使い方は後ほど解説しますが、mpackタブから直接ScannerタブやIntruderタブへリクエストを送る "Send to Intruder" のような機能は実装していないため、JSON形式の対象リクエストを一旦Rawタブへコピーし、 "Send to Intruder/Scanner" する必要があります。
ここでポイントになる処理を解説します。
- 引数の toolFlag で Scanner/Intruder/Extender のいずれかのツールから送信されたメッセージであることを確認しています。
- IExtensionHelpers#analyzeRequest には複数のオーバーロードがありますが、 IExtensionHelpers#analyzeRequest(byte[] request) を使うとAPIのドキュメントに書かれている通り IRequestInfo#getUrl が使用できなくなるので注意が必要です。 リクエストの内容だけでは、オリジンを決定できないためだと思われます。
- IHttpRequestResponse#setRequest で MessagePack へ変換したリクエストをセットします。
class HttpListener( IHttpListener, MpackJsonHelper ):
def __init__( self, callbacks ):
MpackJsonHelper.__init__( self, callbacks )
self._toolMask = self._callbacks.TOOL_SCANNER | \
self._callbacks.TOOL_INTRUDER | \
self._callbacks.TOOL_EXTENDER
def processHttpMessage( self, toolFlag, isRequest, httpReqRes ):
if False == isRequest:
return
if 0 == ( self._toolMask & toolFlag ):
return
requestInfo = self._helpers.analyzeRequest( httpReqRes )
if False == self._callbacks.isInScope( requestInfo.getUrl() ):
return
if False == self.isMessagePack( requestInfo.getHeaders() ):
return
rawRequest = httpReqRes.getRequest()
try:
newRequest = self.toMpack( rawRequest, requestInfo )
if None == newRequest:
return
except:
return
httpReqRes.setRequest( newRequest )
インストール
Python Environmentの設定
今回はPythonでの実装のため、BurpのPython環境設定から説明してみたいと思います。
公式なドキュメントはBurpのSupport Centerにあります。
https://portswigger.net/burp/help/extender.html#options_pythonenv
この説明の対象OSはLinuxです。WindowsやOSXをお使いの方は適宜読み替えて下さい。virtualenv環境への読み替えも同様にお願いします。
Free版のBurpで解説します(業務ではPro版を使用しています、念の為)。
また、Pythonとpipはインストール済であるものとします。
- pipからmsgpack-pythonをインストールします。
$ pip2 install msgpack-python
- jython.orgからJythonのjarファイルをダウンロードします。(本稿執筆時点の最新はjython-standalone-2.7.0.jarです)
- Burpの[Extender - Options - Python Environment]から、ダウンロードしたJarファイルとPythonモジュールのパスを指定します。モジュールのパスは以下のコマンドで表示されると思います。
$ python2 -c 'import sys; print(sys.path)'
Extensionのインストール
- git cloneなどの方法でExtensionをダウンロードします。
- [Extender - Extensions - Burp Extensions]を開き、ダウンロードしたExtensionをAddします。Extension TypeはPythonを指定します。
エラーが発生しなければひとまずインストール成功です。
テストアプリ
MessagePackで通信を行うRuby on Railsのサンプルアプリを作成しました。
https://github.com/jx6f/blog_mpac
サンプルアプリ起動までのコマンドを以下に示します。
Railsの実行環境が構築済であることが前提となっています。
$ git clone https://github.com/jx6f/blog_mpac.git && cd blog_mpac
$ git checkout add_request_parser
$ bundle install
$ rake db:migrate
$ rails s -b localhost
Railsの環境が無い場合、Dockerをご使用ください。
$ git clone https://github.com/jx6f/blog_mpac.git && cd blog_mpac
$ git checkout dockerize
$ docker build -t blog_mpac:latest .
$ docker run -d --name blog_mpac -p 127.0.0.1:3000:80 -e SECRET_KEY_BASE=$(head -c 30 /dev/urandom | xxd -p) blog_mpac:latest
無事起動できたら、ブラウザで http://localhost:3000/ を表示します。
このアプリは、Postリソースの表示と更新のリクエストでmsgpackフォーマットを指定することで、MessagePack形式のレスポンスを返します。
Railsのルーティング上は以下の2つです。
GET /posts/:id(.:format) posts#show
PATCH /posts/:id(.:format) posts#update
詳細は後ほどBurpを使いながら説明します。
MessagePack API を Burp で試験する
ここまでの手順でBurpでMessagePackを扱う準備ができました。
ここからは実際にBurpでメッセージの内容をJSONフォーマットで表示したり、MessagePackリクエストの改変を行います。
Proxy、Repeater、Intruderを使って説明します。
引き続きFree版のBurpにて解説します。
Proxy
localhost:3000 で起動中のテストアプリをBurpのTarget Scopeに含めるようにします。
[Target - Scope - Target Scope - Add] です。
また、Burpのデフォルト設定ではProxyタブにOther binary(MessagePackはこれに分類される)が表示されないのでチェックを入れておきます。
ブラウザから http://localhost:3000/posts/new を開き、データを作成します。
ブラウザのプロキシの設定を行い、Burpを通すようにしておいて下さい。
http://localhost:3000/posts/1 へ遷移したと思います。
フォーマット(パスの拡張子部分)を msgpack としてページを開くと、MessagePack形式のレスポンスが返ります。
http://localhost:3000/posts/1.msgpack を開いてください。
ProxyタブのResponseを確認します。
Rawタブにはburp-msgpackが変換を行う前の、MessagePack形式のままのレスポンスが表示されます。
mpackタブにはburp-msgpackがJSONへ変換を行った後のレスポンスが表示されます。
レスポンスがヒューマンリーダブルになったところで、次はリクエストを見ていきます。
今回はpryを使って、MessagePack over HTTPリクエストを対話的に作成します。
pryはテストアプリのDockerコンテナにインストール済です。
こちらを使用する場合はDockerのネットワークインターフェースに割り当てられたIPを調べて、Burpでbindしておきます。
ホスト側で直接pryを使用できる場合、この設定は特に必要ありません。
$ ip a show docker0
4: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:9e:16:b0:7c brd ff:ff:ff:ff:ff:ff
inet 172.17.0.1/16 scope global docker0
valid_lft forever preferred_lft forever
以下のオプションでpryのREPLを立ち上げます。
# Dockerコンテナを使わずに直接立ち上げる場合
$ pry -r msgpack -r uri -r 'net/http' -r json
# Docker内のpryの場合
$ docker exec -it blog_mpac pry -r msgpack -r uri -r 'net/http' -r json
pryにrubyのコードを打ち込み、Burpプロキシを通しながらMessagePack over HTTPリクエストを送信します。
#から始まる行はコードの説明のためのコメントです。
# http bodyデータの生成(MessagePackフォーマット)
[1] pry(main)> body = MessagePack.pack( {"post":{"title":"update test","body":"Hi, MessagePack"},"commit":"Update Post"} )
=> "\x82\xA4post\x82\xA5title\xABupdate test\xA4body\xAFHi, MessagePack\xA6commit\xABUpdate Post"
# Proxy経由でhttpサーバに接続するクラスを生成
# Dockerの場合、最初の'localhost'の部分をBurpでbindしたDockerのIPに変更
[2] pry(main)> proxy = Net::HTTP::Proxy('localhost',8080).new('localhost',3000)
=> #<#<Class:0x000000023f23f0> localhost:3000 open=false>
# httpメッセージ送受信
# /post/1 のリソースを、PATCHメソッドで更新するリクエスト
[3] pry(main)> resp = proxy.patch( "/posts/1.msgpack", body, {'Content-Type' =>'application/msgpack'} )
=> #<Net::HTTPOK 200 OK readbody=true>
# レスポンスをアンパックして内容を表示
[4] pry(main)> MessagePack.unpack(resp.body)
=> {"id"=>1,
"title"=>"update test",
"body"=>"Hi, MessagePack",
"created_at"=>"2016-05-06T04:02:12.408Z",
"updated_at"=>"2016-05-06T04:08:49.973Z"}
BurpでInterceptしている場合、リクエストを送信した時点でInterceptタブにはJSONに変換されたリクエストが表示されます。
この状態でリクエストを編集することも可能です。
HTTP historyタブにも同様の内容が表示されます。
Repeater
次はRepeaterを使ってリクエストの編集を行います。
先ほどのリクエスト(PATCH /posts/1.msgpack)を Send to Repeater でRepeaterに送ります。
mpackタブでリクエストを編集し、rawタブを確認します。
rawタブのMessagePack形式のリクエストに編集内容が反映されたことが確認できます。
Goボタンをクリックしてリクエストを送信し、レスポンスを確認します。
編集した内容でデータが更新されたことが、レスポンス(の雰囲気)からわかります。
Intruder
IntruderではProxyやRepeaterのように個別にメッセージを編集することが出来ません。
JSON形式のベースリクエストとペイロードを使ってリクエストを作成し、リクエスト送信のタイミングで burp-msgpack がMessagePackに変換するようにしています。
RepeaterのmpackタブのリクエストをRawタブにコピーし、 Send to Intruder でIntuderにリクエストを送ります。
Intruderの設定をします。
ここでのポイントは、JSON形式のWeb APIを試験する時と同じく、JSONフォーマットのリクエストを作ることを意識することです。
ペイロードのURLエンコード機能は、今回は不要なのでチェックを外しておきます。
[Intruder - タブ番号 - Payloads - Payload Encoding]です。
デフォルトでこの設定にしておきたい場合は、[メニューバー - Intruder - New tab behavior]からデフォルト設定を使用しないようにしておくといいでしょう。
ペイロードもJSONフォーマットに従ったエスケープが必要です。
今回はIPA ウェブ健康診断仕様に記載されているペイロードをJSONエスケープしたものを使用します。
パラメータ値にAppendすべきものとReplaceすべきものが混ざっていますが、今回はそれっぽくIntruderを動かしたいだけなのであまり気にしないでください。
また、エスケープを手動で行わずに Extension で実装することも可能ですが、今回は実装していないため触れません。
'
'and'a'='a
and 1=1
'>\"><hr>
'>\"><script>alert(document.cookie)<\/script>
<script>alert(document.cookie)<\/script>
javascript:alert(document.cookie);
..\/..\/..\/..\/..\/..\/..\/bin\/sleep 20|
;\/bin\/sleep 20
Start attackをクリックしてIntruderによる試験を開始します。
残念ながら実際に送信したMessagePack形式のリクエストは確認できませんが、レスポンスから意図したリクエストが送信できていることがお分かり頂けるかと思います。
テストアプリのログを確認すると、より確認しやすいかもしれません。
Started PATCH "/posts/1.msgpack" for 127.0.0.1 at 2016-05-07 00:02:20 +0900
Processing by PostsController#update as MSGPACK
Parameters: {"post"=>{"title"=>"'>\"><script>alert(document.cookie)</script>", "body"=>"Hi, MessagePack"}, "commit"=>"Update Post", "addParam"=>12345, "id"=>"1"}
Post Load (0.1ms) SELECT "posts".* FROM "posts" WHERE "posts"."id" = ? LIMIT 1 [["id", 1]]
(0.1ms) begin transaction
SQL (0.1ms) UPDATE "posts" SET "title" = ?, "updated_at" = ? WHERE "posts"."id" = 1 [["title", "'>\"><script>alert(document.cookie)</script>"], ["updated_at", "2016-05-06 15:02:20.493800"]]
(31.2ms) commit transaction
Rendered text template (0.0ms)
Completed 200 OK in 34ms (Views: 0.5ms | ActiveRecord: 31.5ms)
Intruderの設定を見た時点でお気づきの方もいたかもしれませんが、JSONフォーマットが崩れてしまうペイロード挿入箇所があります。
文字列が二重引用符で囲まれていないため、burp-msgpackがJSONのパースに失敗してしまいます。
パースに失敗したリクエストはAlertsタブに表示されます(burp-msgpackがアラートを上げます)。
この場合、出来損ないのJSON文字列がそのまま送信されます。
ちなみにテストアプリもパラメータのパースに失敗します。
Started PATCH "/posts/1.msgpack" for 127.0.0.1 at 2016-05-07 00:02:51 +0900
Error occurred while parsing request parameters.
Contents:
{"post": {"title": "Intruder test", "body": "Hi, MessagePack"}, "commit": "Update Post", "addParam": and 1=1}
ActionDispatch::ParamsParser::ParseError (109 extra bytes after the deserialized object):
(スタックトレース)
MessagePackやJSONの場合は文字列型のパラメータしかインジェクションできないのか?というとそうでもないのですが、また別の機会にご紹介できればと思います。
おわりに
サイバーディフェンス研究所では、既存のツールでは対応できないアプリケーションや機器の診断、攻撃者視点での診断ツールの高品質化や効率化に、積極的に取り組んでいます。
今回はバイナリフォーマットなHTTPメッセージをBurpで試験する方法として、 MessagePack を例に Extension の実装方法と使用方法、実際に試験する上でポイントとなりそうな箇所をご紹介しました。
皆様のお役に立つ情報があれば幸いです。