SSTエンジニアブログ

SSTのエンジニアによるWebセキュリティの技術を中心としたエンジニアブログです。

FiddlerScriptでスマホアプリの診断を支援

こんにちは。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の診断といえど相応に広い知識が要求されることも多くあります。 こういったトラブルが発生すると、実際の業務を進めるという意味では確かにつらいのですが、エンジニアとしてはひとつずつ問題を解決していく達成感や新たな知見を得られる絶好の機会でもあり、ある種わくわくしてしまいますね!