0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

--resolveでは間に合わぬ、--connect-toだ

Posted at

問題

8080番ポートで待ち受けているhttpsサーバがある。このサーバは192.168.0.5/2410.0.0.5/24の2つのIPを持ち、またゲートウェイ192.168.0.1/24 == 10.0.0.5/24がポートフォワード10.0.0.10:4444 -> 192.168.0.5:8080をかけている。

サーバが証明書を持つドメインとしてexample.comsecret.example.comがあり、どちらもAレコードとして10.0.0.5のみを持っているとする。

サーバは仮想ホストexample.comsecret.example.comを抱えており、Server Name Indication (SNI)を読んだ上でHostヘッダからリクエストを仮想ホストに分配する。仮想ホストはホストヘッダについて次の2項目を確認する。

  • ホスト名がSNIと一致していること
  • ポート番号がリクエストを受け付けたポート番号(8080)と一致していること

この時、ネットワークBからcurlコマンドを用い、ゲートウェイ経由でhttps://secret.example.com:8080にアクセスするにはどうすればよいか。

回答

curl --connect-to secret.example.com:8080:10.0.0.10:4444 https://secret.example.com:8080

解説

PPPoEを契約していても、より高速なIPoE(IPv4 over IPv6)経由で通信させたいという気持ちは湧くはずである。しかし多くのIPoEプロバイダでは操作不能かつ固定不能1な、割り当てられたポートしか使えない。とはいえIPoEに応じてサーバのポートを調整するという作業はしたくない。ファイアウォールの調整や内部サービスとの衝突など、色々面倒だからだ。よって、ゲートウェイでポートフォワードを噛ませる形になるだろう。これは問題における10.0.0.0/24をWANに置き換えた時と同じネットワークになる。

ところで、httpは仮想ホストを実現するための仕組みとしてHostヘッダがあるが、httpsになるとSNIによってもホスト名が送られる。さらにHostヘッダにはポート番号も書かれている。これらの指定子について整合性チェックを取るようRFCにも書いてあるのだが、送信するべきSNI・Hostヘッダと実際の送信先が異なるようなクエリはURLでは表現できない。IPとポート番号を直書きしたURLではサーバまで届きはするものの、その後の不整合を理由に適切な証明書が返ってこなかったり、400 Bad Requestとか421 Misdirected Requestを返されたりすることになる。

と、IPoE経由で自前のhttpsサーバを叩きに行くという状況がこの問題の背景な訳だが、シェルスクリプトから叩きたいこともよくあることだろう。最も標準的なコマンドラインツールであるcurlにはさまざまなオプションがついているから、適宜活用して適切なリクエストを構成できる。

以下ではより弱い問題から始めてこの問題の最適解を見ていく。

準備問題1: httpサーバ

TCPには送信先ホスト名を伝達する機能は存在せず、上位レイヤで勝手に実装されている。httpではHostヘッダにホスト名が記述されるから、これを手で書き換えれば良い:

curl -H "Host: secret.example.com:8080" http://10.0.0.10:4444

準備問題2: 受付ポートとHostヘッダの不一致を無視するhttpsサーバ

httpsサーバではその前段であるTLSで証明書のやり取りをするが、curlは送られてきたサーバ証明書のドメイン名がURLのドメインと一致しない場合、エラーを出して中断するのがデフォルトの挙動である。そこで-kオプションで証明書の整合性の確認をオフにして

curl -k -H "Host: secret.example.com:8080" https://10.0.0.10:4444

で上手くいく……かは実装による。仮想ホストの振り分けがHostヘッダのみで行われているならこれで良いが、SNIによって振り分けていた場合、生のIPによるアクセスではSNIが送信されないので、デフォルトで降ってくる証明書のドメインと紐づいた仮想ホストに流されうる2。今回の場合、デフォルトのホストがexample.comであった場合、https://example.com:8080が見えてしまう。

そうでなくとも、TLSではexample.comの証明書で認証し、httpとしてはsecret.example.comに繋ぐというのは、かなり攻撃に近い接続3であるし、サーバ証明書をちゃんと確認しないのも危なっかしい。-kオプションを使わない解決策はあるだろうか。

用件を考え直すと、要するにTLSやhttpにはsecret.example.comに送っている気分になってもらいつつ、実際の送信先は10.0.0.10にできれば良いわけで、つまりドメイン解決だけ騙せば良いのである。この辺りの用語で検索すると--resolveオプションが出てくる:

curl --resolve secret.example.com:4444:10.0.0.10" http://secret.example.com:4444

これで、secret.example.com:4444へのリクエストは10.0.0.10(のポート4444)に送られるようになり、TLS・httpはsecret.example.com:4444に繋いでいる気分で動いてくれる。

本題: 受付ポートとHostヘッダの不一致を取り締まるhttpsサーバ

ところで、これでもHostヘッダはsecret.example.com:4444と、ポート4444に繋いでいる気分なのだが、それで問題ないだろうか?httpに関するRFCを覗くと、

RFC 9110 7.4. Rejecting Misdirected Requestsより

Once a request is received by a server and parsed sufficiently to determine its target URI, the server decides whether to process the request itself, forward the request to another server, redirect the client to a different resource, respond with an error, or drop the connection. This decision can be influenced by anything about the request or connection context, but is specifically directed at whether the server has been configured to process requests for that target URI and whether the connection context is appropriate for that request.

For example, a request might have been misdirected, deliberately or accidentally, such that the information within a received Host header field differs from the connection's host or port. If the connection is from a trusted gateway, such inconsistency might be expected; otherwise, it might indicate an attempt to bypass security filters, trick the server into delivering non-public content, or poison a cache. See Section 17 for security considerations regarding message routing.

Unless the connection is from a trusted gateway, an origin server MUST reject a request if any scheme-specific requirements for the target URI are not met. In particular, a request for an "https" resource MUST be rejected unless it has been received over a connection that has been secured via a certificate valid for that target URI's origin, as defined by Section 4.2.2.

The 421 (Misdirected Request) status code in a response indicates that the origin server has rejected the request because it appears to have been misdirected (Section 15.5.20).

と、スキームごとの整合性チェックに失敗したら421 Misdirected Requestを返すよう規定されている。特に証明書のドメイン名との不一致はMUSTでリジェクトするよう指示されている。

加えてRFC 9110 4.3.3. https Originsより

An origin server might be unwilling to process requests for certain target URIs even when they have the authority to do so. For example, when a host operates distinct services on different ports (e.g., 443 and 8000), checking the target URI at the origin server is necessary (even after the connection has been secured) because a network attacker might cause connections for one port to be received at some other port. Failing to check the target URI might allow such an attacker to replace a response to one target URI (e.g., "https://example.com/foo") with a seemingly authoritative response from the other port (e.g., "https://example.com:8000/foo").

と、ドメインが同一でポートが異なるhttpsバーチャルホストから侵入される可能性を記述している。ここから、Hostヘッダについてポート番号の整合性をチェックする実装があってもおかしくないし、そうするべきだろう。4

従って、ポートに関する整合性が走るサーバではHostヘッダに含まれるポート番号も正しく記述する必要がある。

回答A: 泥臭く

というわけで、Hostヘッダを書き換えて

curl --resolve secret.example.com:4444:10.0.0.10 -H "Host: secret.example.com:8080" http://secret.example.com:4444

とすれば良い。

しかしあまり気分の良い方法ではない。

問題点1: Hostヘッダの書き換え

準備問題1でもそうだが、httpの仕様の根幹にあるHostヘッダを手で書き換えるというのは行儀が良くない。Hostヘッダはできればcurlに勝手に生成して欲しいものである。経由ポートでしかない4444が、curlのhttpモジュールに使われてしまっているのが原因だが、これを防ぐにはURLのホスト部がsecret.example.com:8080にしないといけない。

問題点2: URLの書き換え

準備問題2でもそうだが、URLをhttps://secret.example.com:4444としてしまっている。問題点1で指摘した通りhttpモジュールにポートを誤認させられるならその方が良いし、そもそもURLhttps://secret.example.com:8080で指定されるエンドポイントに、10.0.0.10:4444経由で繋がりたいという欲求なのだから、プロキシを指定するように書けるべきだ。つまり、--resolveオプションがポートも変換できれば良いのだが、そんなオプションがあるだろうか。

回答B: --connect-toを使う

以下curl(1)より、--connect-toオプションの説明。

For a request to the given "HOST1:PORT1" pair, connect to "HOST2:PORT2" instead. This option is suitable to direct requests at a specific server, e.g. at a specific cluster node in a cluster of servers. This option is only used to establish the network connection. It does NOT affect the hostname/port that is used for TLS/SSL (e.g. SNI, certificate verification) or for the application protocols. "HOST1" and "PORT1" may be the empty string, meaning "any host/port". "HOST2" and "PORT2" may also be the empty string, meaning "use the request's original host/port".

A hostname specified to this option is compared as a string, so it needs to match the name used in request URL. It can be either numerical such as "127.0.0.1" or the full host name such as "example.org".

--connect-to can be used several times in a command line

Example:
curl --connect-to example.com:443:example.net:8443 https://example.com

See also --resolve and -H, --header.

というわけで、顧客が本当に必要だったものはちゃんと存在している:

curl --connect-to secret.example.com:8080:10.0.0.10:4444" http://secret.example.com:8080

URLは書き換わらず、10.0.0.10:4444をプロキシのように使うという点まで明白になっている。また、準備問題1準備問題2でもこの方法は有効である。

さらに嬉しいことに、10.0.0.10の部分はドメインでも良い。元々考えていた状況はPPPoEとIPoEの2本立てで、PPPoEのIPについてドメインに登録しているという状況だったから、IPoEのIPを登録することもできるはずだし、明らかにその方が便利だ。ここではipoe.example.comにAレコード10.0.0.10を登録してあるとする。--resolveでは変換先として生のIPアドレスしか指定できないので、別のツールでドメイン解決をする手間があった。--connect-toならドメインで指定すればよいので、ワンライナーになる5:

curl --connect-to secret.example.com:8080:ipoe.example.com:4444 http://secret.example.com:8080

まとめ

  • 421 Misdirected Requestはポートフォワードと相性が悪い
  • --resolveよりも--connect-toの方が自由度が高くて便利
  • manpageは目を通しておくべき
  1. 稀に(工事後など)IPv4・IPv6 prefix・利用可能ポートの割り当てが変動する場合がある。

  2. 普通はSNIがついていなかったらフォールバックとしてHostでルーティングするとは思うが、存在しうる実装である。この実装なら少なくともSNI Spoofingは発生しないという利点がある。

  3. 実例: NGINXリバースプロキシでTLS Server Name Indication (SNI)と異なるドメイン名のバックエンドホストへルーティングできちゃう件について

  4. 自作のnginxモジュールngx-strict-sniはそういう実装になっている。

  5. これは単に記述が簡便に済むという点以外にも、ドメイン解決ツール(例えばdig)とcurlのドメイン解決が異なる実装を採用している場合、本来疎通しない状況で疎通してしまうという問題を解決する。これが問題となるのはhttpsサーバをTCPレベルの疎通確認として使っている場合である。例えばsshに利用可能な最も高速な経路を使わせるためにMatch execで各種ルートでの疎通確認をやっている時、sshと同一のドメイン解決手段で確認しないと、疎通確認は成功したのにsshはドメイン解決で詰まる、みたいなことが起きる。しかしこれは有効な事例があまりにも狭いので脚注とした。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?