もしwebセキュリティのエンジニアがrfc7540の「http/2アプリ」をweb診断したら
TRANSCRIPT
もし Web セキュリティのエンジニアが RFC7540 の「 HTTP/2 アプリ」を Web診断したら略して「もし R 」
自己紹介Twitter : abend@number3to4
Web セキュリティエンジニア
宣伝7/26 (日) に開催される JULY TECH FESTA にて、「フリーでできるセキュリティチェック」というタイトルにて、プレゼンをさせていただきます。
”無料”のツールを利用して、ラクにセキュリティチェックを行うためのノウハウをご紹介いたします。
インフラエンジニア、運用を担当されているエンジニア向けの内容となっています。
http://2015.techfesta.jp/
HTTP/2 ってHTTP ( HyperperText Transfer Protocol )の最新バージョンにあたり、 HTTP/1.1から 16 年ぶりのアップデート。 HTTP/2 については RFC7540 、関連する HPACK はRFC7541 でリリースされました。
HTTP/1.1 と比較すると HTTP/2 は複雑になっています。HTTP/1.1 との違いはこんな感じ。
① 多重化② バイナリメッセージ③ ヘッダ圧縮( HPACK )④ フロー制御⑤ 優先度⑥ 依存関係⑦ Server Push
nghttp2 ってHTTP/2 に対応した Web サーバ、 Web クライアント、 Proxy 機能を有する。通信内容の確認は、 hexdump オプションを指定することで確認することが可能。
https://nghttp2.org/
① 多重化HTTP/1.X では、 1 つの TCP コネクションで 1 つのリクエストおよびレスポンスをやりとりしていた。 HTTP/2 では、 stream という概念により 1 つの TCP コネクションで複数のリクエストおよびレスポンスをやりとりできるようなった。
stream
Stream_id :偶数
Stream_id :奇数
Stream_id : 0
クライアントサーバ
StreamID0 :コネクション制御用奇数:クライアントが開始偶数:サーバが開始
コネクション
② バイナリメッセージHTTP フレームが新たに定義され、 stream 上でやりとりをする。フレームタイプによって Frame Payload のフォーマットが変わる。フレームヘッダは 9byte 。
フレームヘッダ
HEADERS フレーム
② バイナリメッセージリクエスト、レスポンスヘッダがバイナリベースとなっている。
Nghttp でアクセスした際の dump 結果。赤枠箇所がリクエストの HEADERS フレーム。
② バイナリメッセージリクエストの HEADERS フレーム。
00 00 26 01 25 00 00 00 0d 00 00 00 0b 0f 82 84 87 41 8b 0b e2 5c 2e 3c b8 5b 7d 70 b2 cf 53 03
2a 2f 2a 90 7a 89 aa 69 d2 9a c4 c0 57 02 e0緑箇所の 3byte 「 00 00 26 」がフレームサイズで 38byte ( 0x26 )最初の 9byte (固定長)は、フレームヘッダ。
青箇所の「 01 」がフレームタイプを表しており、 0x01 は HEADERS フレーム。黄色箇所の「 25 」は Flag で詳細は RFC7540 へ。
茶色箇所の 4byte (正確には 31bit 分)は Stream ID で、この場合は 13 。
② バイナリメッセージリクエストの HEADERS フレームペイロード(赤箇所)
00 00 26 01 25 00 00 00 0d 00 00 00 0b 0f 82 84 87 41 8b 0b e2 5c 2e 3c b8 5b 7d 70 b2 cf 53 03
2a 2f 2a 90 7a 89 aa 69 d2 9a c4 c0 57 02 e0青箇所は依存先の Stream ID を指している。
白箇所で Priority フラグ( 0x20 )がセットされた場合にのみ、青箇所と緑箇所が出力される。
緑箇所は weight で、「 0f 」の場合、 15+1=16 となる。
③ ヘッダ圧縮( HPACK )HTTP/2 はヘッダ圧縮のために HPACK ( RFC7541 )を採用しています。ハフマン符号や静的テーブルと動的テーブルを用いて、圧縮しています。
Index の場合、 7bit の値がリクエスト・レスポンスヘッダのヘッダ名、値を指している。「 : 」から始まるヘッダ名は HTTP/2 のために作られた疑似ヘッダフィールドです。
③ ヘッダ圧縮( HPACK )さっきの Header Block の場合
00 00 26 01 25 00 00 00 0d 00 00 00 0b 0f 82 84 87 41 8b 0b e2 5c 2e 3c b8 5b 7d 70 b2 cf 53 03
2a 2f 2a 90 7a 89 aa 69 d2 9a c4 c0 57 02 e00x82 ( 1000 0010 )で Index の 2 を指しており、「 :method: GET 」を意味します。同様に 0x84 、 0x87 は、それぞれ Index が 4 、 7 を指しています。
Index 4 ・・・ :path: /Index 7 ・・・ :scheme: https
③ ヘッダ圧縮( HPACK )bit により、どういう意味なのか変わってくる。
Index は事前に定義されたヘッダ名の値をセットする。 0x41 の場合、 Index が 1 である :authority を指す。
2byte 目(緑箇所)は、上位 bit がハフマンエンコーディングの有無を指定し、値長を指定している( 11byte )。
00 00 26 01 25 00 00 00 0d 00 00 00 0b 0f 82 84 87 41 8b 0b e2 5c 2e 3c b8 5b 7d 70 b2 cf 53 03
2a 2f 2a 90 7a 89 aa 69 d2 9a c4 c0 57 02 e0
③ ヘッダ圧縮( HPACK ):authority: ヘッダの値は、赤箇所の byte 列( 11byte 分)になる。
この byte 列はハフマンエンコーディングされているため、このままでは可視できるものではないので、ハフマンでコーディングを行う必要がある。
00 00 26 01 25 00 00 00 0d 00 00 00 0b 0f 82 84 87 41 8b 0b e2 5c 2e 3c b8 5b 7d 70 b2 cf 53 03
2a 2f 2a 90 7a 89 aa 69 d2 9a c4 c0 57 02 e0
③ ヘッダ圧縮( HPACK )まずは、 11byte を 2 進数に変換し、結合する。
RFC7541 で定義されているテーブルから戻してあげればいい。赤箇所の「 00001 」は 1 を意味しており、白は9 、青は 2 を意味する。
これを繰り返すと「 192.168.159.133 」となる。緑箇所は余りで 1 で padding されている。
0000101111100010010111000010111000111100101110000101101101111101011100001011001011001111
④ フロー制御HTTP/2 では 1 つのコネクションで複数の stream が通信できる。複数の stream が一斉に大量の通信をはじめたら・・・。
俺、通信するっ!! じゃ、俺も俺も。 じゃ、俺も俺も。
誰も「どうぞ、どうぞ」を言わないと、重要な通信が行えなくなってしまう。
どうぞ、そうぞ どうぞ、そうぞ えっ!?
④ フロー制御ウィンドウサイズによるフロー制御が存在しており、優先度にも応じて通信を行う。ただし、フロー制御の対象となるのは、 DATA フレームのみ。
コネクションの使用可能なウィンドウサイズ: 65535
Stream ごとの使用可能なウィンドウサイズ: 65535
使用可能な量を使い切ってしまった場合、受信者側から WINDOW_UPDATEを受け取ることで、使用可能な量を追加することができる。
クライアントサーバ
④ フロー制御たとえば、 237byte のデータを StreamID が 1 のウィンドウサイズ 127byte の設定で通信した場合・・・
2. サーバが 237byte のデータのうち、 127byte を send
クライアントサーバ
コネクション ウィンドウサイズ: 65535
Stream ID:1 ウィンドウサイズ: 127
1. クライアントが Initial Window Size を 127byte へ変更を send
コネクション ウィンドウサイズ: 65535
StreamID :1 ウィンドウサイズ: 127
クライアントサーバ
④ フロー制御
4. サーバが残りの 110byte を send
クライアントサーバ
コネクション ウィンドウサイズ: 65408
Stream ID:1 ウィンドウサイズ: 127
3. クライアントが WINDOW_UPDATE(StreamID:1)127byte を send
コネクション ウィンドウサイズ: 65408
Stream ID:1 ウィンドウサイズ: 0
クライアントサーバ
④ フロー制御Initial Window Size は、 SETTING フレームで指定します。
00 00 0c 04 00 00 00 00 00 00 03 00 00 00 64 0004 00 00 00 7f
赤箇所 9byte はフレームヘッダ。黄色箇所以降が SETTING フレームのペイロード。
2byte の識別子と 4byte の値の計 6byte単位で構成される。黄色箇所 0x03 は、 SETTINGS_MAX_CONCURRENT_STREAMS を意味し、値は緑箇所の 0x64(100) 。 0x04 が SETTINGS_INITIAL_WINDOW_SIZE で、 0x07(127) 。
※ SETTINGS_INITIAL_WINDOW_SIZE はオプションで強制的に 0x07 に指定してます。
④ フロー制御WINDOW_UPDATE は、 WINDOW_UPDATE フレームで実行されます。
00 00 04 08 00 00 00 00 0d 00 00 00 1f
WINDOW_UPDATE は、「コネクションに対して」と「 Stream に対して」行うことが可能で、フレームヘッダで指定する Stream ID が 0 の場合、「コネクションに対して」、指定する Window Size が追加される。「 Stream に対して」行う場合は該当する Stream ID を指定する必要がある。両方に対して行う必要がある場合はそれぞれの WINDOW_UPDATE フレームを送信する必要がある。
⑤ 優先度各 Stream が Window Size の上限まで使い切ることがないように、 Stream に対して優先度 (Wight) をつけて、その割合に応じて通信を行う。
値は 1 から 256 の整数で指定することが可能です。 Default は 16 です。
StreamID:1 Wight:12StreamID:3 Wight:3
この場合、 StreamID:1 は全体の 5 分の 4 の割合で通信を行います。
非常に参考になりました。HTTP/2 Deep Dive: Priority & Server Push by Moto Ishizawahttps://speakerdeck.com/summerwind/2-deep-dive-priority-and-server-push
⑤ 優先度優先度の指定は、 PRIORITY フレームを用いて行うことが可能です。
00 00 05 02 00 00 00 00 03 00 00 00 00 c8
赤箇所はフレームヘッダ。青箇所は依存する StreamID を指定し、この場合は 0 を指していている。緑箇所が該当 Stream(StreamID:3) の優先度で、 0xc8(200)+1=201 が優先度となる。
⑥ 依存関係各 Stream は、他の Stream と依存関係を持ちます。
ルートには StreamID:0 がセットされ、依存される StreamID のほうが優先してリソースの割り当てを行われます。
StreamID:0
StreamID:1 StreamID:3
StreamID:5 StreamID:7
⑦ Server Push通常、クライアントからリクエストを送信し、それに対してレスポンスを返すが、 Server Push はクライアントからのリクエストを受けずにサーバがレスポンスを返す。
クライアント サーバ
リクエスト
レスポンス
Server Push のレスポンスは、クライアントが送信すると予定されるリクエストとデータを返します。データはクライアントでキャッシュされます。
⑦ Server PushServer Push するために、サーバからクライアントに PUSH_PROMISE フレームで通知しておく必要があります。
Promissed Stream ID は、 Server Push され StreamID がセットされる。サーバが起点となるため、この値は偶数となる。また、 Header Block Fragment には予定されるリクエストをセットされます。
⑦ Server PushServer Push するために、サーバからクライアントに PUSH_PROMISE フレームで通知しておく必要があります。
00 00 18 05 04 00 00 00 0d 00 00 00 02 82 04 8761 09 f5 41 57 22 11 87 41 87 08 9d 5c 0b 81 70ff赤箇所はフレームヘッダ。緑箇所は、 Server Push する StreamID で ID:2 を指定。水色箇所は、予定されるリクエストヘッダになっており、 HEADERS フレームと同様にデコード可能。
Web 診断ってWeb アプリケーションの脆弱性の有無を検査することを Web 診断としています。Web 診断では、基本的に Proxy ツールを用いて行います。
Proxy ツールで検査パターンを付加してリクエストし、そのレスポンスの内容によって脆弱性の有無を判断します。
リクエスト
Web サーバクライアント検査パターンを付加する。 Proxy ツール
レスポンス
Web 診断ってProxy ツールを使う理由は、ブラウザで操作できる内容が限られているためです。
都道府県:
性別:
都道府県:
性別: 男 女
東京都 (ブラウザ上で)入力形式が固定化されている場合に Proxy ツールを用いて、入力値を変更します。
Web 診断ってProxy ツールのひとつに Burp Suite があります。Java で動作し、一部の機能は有償版のみとなりますが、基本的にフリーで使えるツールです。
リクエストを Intercept して変更することで、ブラウザでは変更できないパラメータも変更可能となります。
結論HTTP/2 のアプリに対して、 Burp Suite でアクセスしてみた。
Burp Suite は、 HTTP/2 に対応していないのでできなかった。
じゃあ、どうすんのソースとか見てたら、 nghttpx という Proxy 機能があることに気づいたのでこれを活用することに。
クライアント Proxy ツールHTTP/1.1 HTTP/2
nghttpxHTTP/1.1
こうすれば既存の仕組みのまま、 HTTP/2 アプリにも接続可能。
HTTP/2 アプリ
試してみたNghttp2 を Proxy として動作させることで、 Burp Suite でもアクセスできるようになった。nghttpx --client -f0.0.0.0,8080 –b[HTTP2 サーバの IP],443 -k -o -LINFO
これで脆弱性見つけられるnghttp2 には、アプリケーションサーバとしての機能がないため、以下、構成で診断が行えるか確認することに。
クライアント Proxy ツールHTTP/1.1
HTTP/2
nghttpxHTTP/1.1
Web アプリ
HTTP/1.1nghttpx
当然見つかるHTTP/1.X と同様に何も変わることなく、 XSS を見つけることができた。
じゃあ、すべての脆弱性も?HTTP ヘッダインジェクションはどうなるのだろうか。
脆弱なサイト一般利用者
Proxy Server
攻撃者
キャッシュが汚染される
メールやページ書込みによるリンク
XSSと同じ被害
なぜ、 HTTP ヘッダインジェクションなのか
HTTP/2 だとレスポンスヘッダ(リクエストヘッダもだけど)はバイナリになるため、以下の行為ができないのではないかと考えた。
① 任意のヘッダ挿入 勝手に Set-Cookie とセットされる可能性がある。
② レスポンススプリッティング Proxy サーバに不正な内容をキャッシュさせられる可能性がある。
なぜ、 HTTP ヘッダインジェクションなのか
検証するにあたり、脆弱なソースを用意した。※オープンリダイレクタも存在します。
#!/usr/bin/perluse utf8;use strict;use CGI;
my $cgi = new CGI;my $url = $cgi->param('url');
print "Status: 302 Moved\n";print "Content-Type: text/html; charset=UTF-8\n";print "Location: $url\n";print "\n";print "<hmtl>\n";print "<body>\n";print "redirect done!!\n";print "</body>\n";print "</hmtl>\n";exit;
① 任意のヘッダ挿入HTTP/2 の場合、 HPACK を用いてヘッダの圧縮を行っているため、脆弱性を悪用してヘッダにインジェクトしようとしても、ただの文字列として処理されてしまうのではないかと推測した。
<レスポンス>HTTP/1.1 302 FoundDate: Thu, 25 Jun 2015 00:17:28 GMTSet-Cookie: a=aLocation: http://www.yahoo.co.jp以下略
<リクエスト>GET http://192.168.159.152:8433/redirect.cgi?url=http://www.yahoo.co.jp%0a%0dSet-Cookie:a=a HTTP/1.1以下略
脆弱だった。。。
① 任意のヘッダ挿入なぜこうなったのか。
<推測です>
今回の構成の場合、バックエンドサーバである脆弱サーバで改行が評価されヘッダに挿入されたレスポンスをそのまま HTTP/2 へ返すために、 HTTP/2 にはまったく関係がなく、とめることはできないのではないかと推測しています。
② レスポンススプリッティングHTTP/2 では、レスポンスヘッダとレスポンスボディはそれぞれ違うフレームとして送信されるため、レスポンスを分割したとしてもキャッシュされないのではないかと推測。<リクエスト>GET http://192.168.159.152:8433/redirect.cgi?url=http://www.yahoo.co.jp%0a%0dContent-Length:0%0a%0d%0a%0dHTTP/1.1%20200%20OK%0a%0d%0a%0d<html><body>inject</body></html> HTTP/1.1Host: 192.168.159.152:8433以下略
<赤字箇所の URL デコード結果>改行Content-Length:0改行改行HTTP/1.1 200 OK
<html><body>inject</body></html>
② レスポンススプリッティング結果は・・・。HTTP/1.0 302 Moved TemporarilyDate: Thu, 25 Jun 2015 16:57:28 GMTLocation: http://www.yahoo.co.jpContent-Length: 100Content-Type: text/html; charset=UTF-8Connection: keep-alive
HTTP/1.1 200 OK
<html><body>inject</body></html>
<hmtl><body>redirect done!!</body></hmtl>
プログラムがそもそも意図した通りになっていなかった。。。
② レスポンススプリッティング※20015/6/27に内容を追加してます。
意図した通りのレスポンスになっていないため、 python で強制的にレスポンススプリッティングした結果が HTTP/2(nghttpx) ではどのように評価されるか検証してみた。
参考http://qiita.com/otoyo/items/98098e927797272d0a39
プログラムは一部抜粋self.wfile.write("HTTP/1.1 302 Found\r\n")self.wfile.write("Content-Type: text/html; charset=utf-8\r\n")self.wfile.write("Location: http://www.yahoo.co.jp\r\n")self.wfile.write("Content-Length:0\r\n")self.wfile.write("\r\n")self.wfile.write("HTTP/1.1 200 OK\r\n")self.wfile.write("\r\n")self.wfile.write("<html><body>inject</body></html>\r\n")
② レスポンススプリッティング
Python サーバにアクセスすると以下のレスポンスが常に返される。
HTTP/1.1 302 FoundContent-Type: text/html; charset=utf-8Location: http://www.yahoo.co.jpContent-Length: 0
HTTP/1.1 200 OK
<html><body>inject</body></html>
※20015/6/27に内容を追加してます。
青字箇所がレスポンススプリッティングされることを想定したレスポンス。
② レスポンススプリッティング
ブラウザ → Burp Suite → フォワードプロキシ (nghttpx) → リバースプロキシ (nghttpx) → Python サーバという順でアクセスした結果。
HTTP/1.1 302 FoundDate: Sat, 27 Jun 2015 08:10:08 GMTLocation: http://www.yahoo.co.jpVary: Accept-EncodingContent-Length: 0Content-Type: text/html; charset=UTF-8Server: nghttpx nghttp2/1.0.4Via: 1.1 nghttpx, 2 nghttpx
2 つ目のレスポンスがなくなっている。
※20015/6/27に内容を追加してます。
② レスポンススプリッティング
リバースプロキシ (nghttpx) のログは、大きく 4 つのポイントがあります。
1. フォワードプロキシ (nghttpx) からの HTTP/2 の GET リクエスト2. Python サーバへの HTTP/1.1 の GET リクエスト3. Python サーバからの HTTP/1.1 のレスポンス4. フォワードプロキシ (nghttpx) への HTTP/2 のレスポンス
※20015/6/27に内容を追加してます。
② レスポンススプリッティング
1. フォワードプロキシ (nghttpx) からの HTTP/2 の GET リクエスト
:method: GET:scheme: http:authority: リバースプロキシの IP:path: /user-agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:38.0) Gecko/20100101 Firefox/38.0accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8accept-language: ja,en-US;q=0.7,en;q=0.3accept-encoding: gzip, deflatex-forwarded-proto: httpvia: 1.1 nghttpx
※20015/6/27に内容を追加してます。
② レスポンススプリッティング
2. Python サーバへの HTTP/1.1 の GET リクエスト
GET http:// リバースプロキシの IP/ HTTP/1.1Host: リバースプロキシの IPUser-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:38.0) Gecko/20100101 Firefox/38.0Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8Accept-Language: ja,en-US;q=0.7,en;q=0.3Accept-Encoding: gzip, deflateVia: 1.1 nghttpx, 2 nghttpx
※20015/6/27に内容を追加してます。
② レスポンススプリッティング
3. Python サーバからの HTTP/1.1 のレスポンス
:status: 302content-type: text/html; charset=utf-8location: http://www.yahoo.co.jpcontent-length: 0via: 1.1 nghttpx
※20015/6/27に内容を追加してます。
※疑似ヘッダフィールド「 :status 」が出力されている。理由は不明。
② レスポンススプリッティング
4. フォワードプロキシ (nghttpx) への HTTP/2 のレスポンス[2805.766] send HEADERS frame <length=27, flags=0x04, stream_id=5> ; END_HEADERS (padlen=0) ; First response header :status: 302 content-type: text/html; charset=utf-8 location: http://www.yahoo.co.jp content-length: 0 via: 1.1 nghttpx[2805.766] send DATA frame <length=0, flags=0x01, stream_id=5> ; END_STREAM
※20015/6/27に内容を追加してます。
レスポンスは、 HEADERS フレームと DATA フレームのみで、 DATA フレームのサイズも 0 となっている。
② レスポンススプリッティング
レスポンススプリッティングした「 HTTP/1.1 200 OK 」のレスポンス自体は認識されずに、破棄されていると思われる。
※20015/6/27に内容を追加してます。
Nghttpx を介すことで、レスポンススプリッティングされる可能性は減ると思うが、実装状況にも依存すると思われるため、 HTTP/2 だからセキュアになるということではないと思う。
まとめ
大事なことなので 2 度言います。
宣伝7/26 (日) に開催される JULY TECH FESTA にて、「フリーでできるセキュリティチェック」というタイトルにて、プレゼンをさせていただきます。
”無料”のツールを利用して、ラクにセキュリティチェックを行うためのノウハウをご紹介いたします。
インフラエンジニア、運用を担当されているエンジニア向けの内容となっています。
http://2015.techfesta.jp/
・ HTTP/2 は以前のバージョンと比較すると複雑になり、 telnet とかで試すことが できなくなった。
まとめ
・ HTTP/2 を使えば安全というわけではなく、 Web アプリケーションはこれまで 通りセキュリティ対策はとる必要はある。
・ hexdump した結果を何度も見ていると、だんだんフレームが見えてくる。
ちなみに前から疑問だったのだが・・・。
HTTP/2 コネクションの初期設定確立のために、 24byte の固定されたコネクションプリフェイスをサーバが送信する。
「 HTTP/2.0 」だと!?正式名称は「 HTTP/2 」 or 「 HTTP2 」じゃないのか。って、前から??でした。
参考資料RFC7540https://http2.github.io/http2-spec/http://summerwind.jp/docs/rfc7540/ (日本語訳)
RFC7541http://www.rfc-editor.org/rfc/rfc7541.txthttp://syucream.github.io/hpack-spec-ja/rfc7541-ja.html (日本語訳)
HTTP/2 Frequently Asked Questionshttp://http2.info/faq.html#why-is-http2-multiplexed
HTTP2 Advent Calendar 2014http://qiita.com/advent-calendar/2014/http2
HTTP/2.0 のインパクトhttps://www.janog.gr.jp/meeting/janog32/doc/janog32-http2.0-shimizu-01.pdf
HTTP/2 入門http://www.slideshare.net/techblogyahoo/http2-35029629
HTTP/2 Deep Dive: Priority & Server Pushhttps://speakerdeck.com/summerwind/2-deep-dive-priority-and-server-push