こんにちは。CTOのはせがわです。
弊社では、スマートフォン向けのアプリケーションについても通常のWebアプリケーション同様にバックエンドのWebサーバーに対しての診断を行なっています。ただ、スマホアプリはブラウザーと同じくHTTPで通信しているとは言っても、細かなところでブラウザーとは異なることがあるため、ごく稀に診断の過程でうまく通信ができないなどの問題が出てしまうことがあります。
少し前になりますがスマホアプリ向けのバックエンドサーバーの診断過程において、まさにそのような問題 ー うまく通信をキャプチャできない ー が発生しました。そのときに、Fiddlerを使って回避することができましたので、その内容を紹介したいと思います。
Burp を使うと通信ができない
ある日、弊社福岡ラボの診断員がスマートフォン向けアプリを対象としたWebアプリケーションを診断している際に、理由がよくわからない現象があるので誰か教えて欲しいというというヘルプがSlackに投げられました。 スマートフォンとバックエンドのWebアプリケーションの間にFiddlerのみを挟んでいる場合には正常に通信ができるのに、Burpを挟むと一部の通信でだけスマホアプリ上で「通信エラーが発生しました」というメッセージが表示され、正常な画面遷移ができなくなるというものです。
以下の4パターンを試したところ、Fiddlerのみのパターン1では正常に通信ができていましたが、Burpを挟んだパターン2〜4では通信が失敗してしまいました。
- パターン1 : [ スマホアプリ ] ー [ Fiddler ] ー [ Webサーバー ] … 正常に通信できる
- パターン2 : [ スマホアプリ ] ー [ Burp ] ー [ Webサーバー ] … 通信できない
- パターン3 : [ スマホアプリ ] ー [ Burp ] ー [ Fiddler ] ー [ Webサーバー ] … 通信できない
- パターン4 : [ スマホアプリ ] ー [ Fiddler ] ー [ Burp ] ー [ Webサーバー ] … 通信できない
パターン1のFiddlerを使い正常に通信が行えたときのFiddler上でのHTTPのログは以下の通りでした(もちろん、一部を伏字にしています)。
[ リスト1 : パターン1のFiddlerのHTTPログ ] POST https://app.example.jp/api/foo/bar HTTP/1.1 Accept-Encoding: gzip App-Version: 1.0.0 Accept: application/msgpack Content-Type: application/msgpack User-Agent: jp.example.app/1.0.0 (Android OS 6.0.1 / API-23 (MTC19T/2741993); LGE Nexus 5X) Authorization: XXXXXX XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX Host: app.example.jp Connection: Keep-Alive, TE TE: identity HTTP/1.1 200 OK Cache-Control: no-cache, private Content-Encoding: gzip Content-Type: application/msgpack date: Mon, 30 Oct 2017 03:26:06 GMT Server: nginx Vary: Accept-Encoding Content-Length: 158 Connection: keep-alive (バイト列、以下省略)
一方、パターン2のBurpを使い通信が行えなかったときもBurp上では以下の通り通信を記録できていました。
[ リスト2 : パターン2のBurpのHTTPログ ] POST /api/foo/bar HTTP/1.1 Accept-Encoding: gzip, deflate App-Version: 1.0.0 Accept: application/msgpack Content-Type: application/msgpack User-Agent: jp.example.app/1.0.0 (Android OS 6.0.1 / API-23 (MTC19T/2741993); LGE Nexus 5X) Authorization: XXXXXX XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX Host: app.example.jp Connection: Keep-Alive, TE TE: identity Content-Length: 0 HTTP/1.1 200 OK Cache-Control: no-cache, private Content-Encoding: gzip Content-Type: application/msgpack date: Mon, 30 Oct 2017 03:30:38 GMT Server: nginx Vary: Accept-Encoding Content-Length: 158 Connection: keep-alive (バイト列、以下省略)
特定のプロキシーツールを挟んだ場合に通信できなくなるといった場合の典型的な理由としては、TLSのサポートされているプロトコルバージョンや暗号方式が異なるというものですが、Burp上では通信が記録できているということは、どうもTLSが理由というわけではなさそうです。
Burpではレスポンスも記録できているにも関わらず、スマホアプリ側で通信エラーが発生しているということは、Burpからのレスポンスをスマホアプリが受け取れていないということなので、Burpの設定(Response Modification)などを色々変更してみますが、一向に状況は改善しません。
もう少し状況を見てみたいということで、診断員にはパターン1とパターン4それぞれにおいて、単にHTTPの内容だけでなく通信時のメタデータも含む形の *.saz
形式のログをFiddler上で取得してもらい、その saz ファイルをFiddlerの Statics タブで表示させてみるとそれぞれ以下のようになっていました。
パターン1のFiddlerのみの通信では以下の通りです。
[ リスト3 : パターン1のFiddlerでの Statistics ] (略) ClientConnected: 14:57:35.000 ClientBeginRequest: 14:57:35.408 GotRequestHeaders: 14:57:35.500 ClientDoneRequest: 14:57:35.500 Determine Gateway: 0ms DNS Lookup: 7ms TCP/IP Connect: 30ms HTTPS Handshake: 62ms ServerConnected: 14:57:35.538 FiddlerBeginRequest: 14:57:35.600 ServerGotRequest: 14:57:35.600 ServerBeginResponse: 14:57:35.649 GotResponseHeaders: 14:57:35.649 ServerDoneResponse: 14:57:35.649 ClientBeginResponse: 14:57:35.649 ClientDoneResponse: 14:57:35.649 Overall Elapsed: 0:00:00.241 (略)
一方でFiddlerの後段にBurpを挿入したパターン4での通信では以下のようになっていました。
[ リスト4 : パターン4でのFiddlerでの Statistics ] (略) ClientConnected: 14:53:37.835 ClientBeginRequest: 14:53:38.240 GotRequestHeaders: 14:53:38.332 ClientDoneRequest: 14:53:38.332 Determine Gateway: 0ms DNS Lookup: 0ms TCP/IP Connect: 0ms HTTPS Handshake: 8ms ServerConnected: 14:53:38.350 FiddlerBeginRequest: 14:53:38.359 ServerGotRequest: 14:53:38.359 ServerBeginResponse: 14:53:49.556 GotResponseHeaders: 14:53:49.556 ServerDoneResponse: 14:53:49.556 ClientBeginResponse: 14:53:49.556 ClientDoneResponse: 14:53:49.556 Overall Elapsed: 0:00:11.315
違いがわかるでしょうか。よく見ると、Burpを挿入した場合だけ ServerGotRequest
から ServerBeginResponse
まで、つまりサーバーがリクエストを受信してからレスポンスを返し始めるまで、10秒ほど時間がかかっています。どうも、このあたりに問題がありそうです。
Content-Length の有無
もう一度、リスト1とリスト2をよく見比べてみると、Fiddler上でのリクエストでは存在しない Content-Length
リクエストヘッダーが Burp上のリクエストでは Content-Length: 0
として存在していることがわかります。この違いはどこから出てきたのでしょう。
ここでパターン4の時のFiddlerおよびBurpのHTTPログを確認してみると、Fiddlerでは記録されていない Content-Length
リクエストヘッダーが後段のBurp上では Content-Length: 0
と記録されていることがわかりました。どうやらBurpが自動的に付与しているようで、つまりスマホアプリとしては Content-Length
リクエストヘッダーを送出していないということになります。
そして、リクエストはPOSTメソッドであり、また Transfer-Encoding
リクエストヘッダーもない状態なので、HTTP/1.1 に従うのなら本来は Content-Length
リクエストヘッダーを付与すべきといえます。これが付いていないためにBurpではスマホアプリからのリクエストを受信した際にどこで通信を終了するかの判断ができずconnectionが切れるまでの間スマホアプリ側へレスポンスを返さずに待ち続けてしまい、一方スマホアプリ側はその間レスポンスを待ち続け10秒間でタイムアウトのエラーが発生するというのが今回の事象のようです。
Content-Length を無理やり挿入
原因が Content-Length
リクエストヘッダーがないことだとわかってしまえば、あとは簡単です。
パターン4の形式でBurpの前段にFiddlerを置き、FiddlerScript を使って無理やり Content-Length
を挿入することにします。
具体的には、Fiddler で Rules
メニューから Customize Rules...
を選び FiddlerScript の編集で Handlers
クラスの OnBeforeRequest
メソッドに以下を追加します。
if (oSession.HTTPMethodIs("POST") && oSession.PathAndQuery.Equals("/api/foo/bar ")) { oSession.oRequest.headers.Add("Content-Length", "0"); }
このように、リクエスト時に該当URLへのPOSTであれば無理やり Content-Length: 0
を付け加えるという処理をFiddlerScriptで行いました(実際にはヘッダーを付け加える場合はもう少し複雑な条件を加味しました)。
これでFiddlerの後段にBurpを配置した場合でもスマホアプリは正常に通信させることができました。
まとめ
スマートフォン向けアプリケーションでバックエンドのサーバーとはHTTPで通信をしている場合であっても、ブラウザーによる通信とは細かなところで差異があり、そのためにBurpや診断ツールなどを利用した場合にうまく通信できなくなることがあります。 今回はうまくFiddlerだけで調査から解決まで行うことができましたが、場合によってはWiresharkのようなツールを使ってより低いレイヤーから調査を行ったりと、Webの診断といえど相応に広い知識が要求されることも多くあります。 こういったトラブルが発生すると、実際の業務を進めるという意味では確かにつらいのですが、エンジニアとしてはひとつずつ問題を解決していく達成感や新たな知見を得られる絶好の機会でもあり、ある種わくわくしてしまいますね!