OSSTech株式会社

2021年9月に学認Shibboleth IdP構築技術ガイドの問題として公開された内容の解説

2022-07-12 - 相本 智仁

はじめに

2021年9月に学認Shibboleth IdP構築技術ガイドの問題として公開された内容を解説します。

Jetty にはクライアントIPアドレスを通信の接続元ではなくリクエストHTTPヘッダーから取得する http-forwaded モジュールがあります。 これは構成を把握して利用しないと偽装されてしまう可能性があります。 Jettyに限らず、socket以外からIPアドレスを取得する場合は仕様を把握して使いましょうというお話です。

構成

問題を把握するためIPアドレスを表示するJSPを配置して何が起こるのかを解説します。

  • Apache + Jetty の構成
    • Apache で 443(HTTPS) を Listen
    • Jetty で 8080(HTTP) を Listen
    • 検証のため意図的に Jetty は 0.0.0.0:8080 でListenし、別のサーバーからもアクセス可としています。
    • Jetty は 9.4.48.v20220622 で検証しています。
    • Apache では ProxyPass で Jetty に HTTP通信で接続します。
ProxyPass / http://127.0.0.1:8080/
  • Webアプリケーション
    • JettyにクライアントIPアドレスを表示する JSP を配置します。
<%@ page contentType="text/html; charset=UTF-8"%>
<html>
  <head><title>GET IP</title></head>
  <body>
   ipAddress:[<%=request.getRemoteAddr()%>]
  </body>
</html>
  • サーバーのIPアドレスは 192.168.48.2
  • 利用者のIPアドレスは 172.16.17.21

この構成で利用者が「Apache経由でJSPにアクセス」、「直接Jettyにアクセス」ではJSPの結果で表示される画面のIPアドレスは次のとおりです。

  1. Apache経由 -> 127.0.0.1
  2. Jetty直接 -> 172.16.17.21
$ curl  https://192.168.48.2/ -k

<html>
  <head><title>GET IP</title></head>
  <body>
   ipAddress ->[127.0.0.1]
  </body>
</html>

$ curl http://192.168.48.2:8080/

<html>
  <head><title>GET IP</title></head>
  <body>
   ipAddress ->[172.16.17.21]
  </body>
</html>

Apacheを経由したアクセスでは、Jettyにとっての接続元は Apache です。 request.getRemoteAddr() で 取得出来るIPアドレスは 127.0.0.1 となります。

この構成ではApache経由でアクセスされた時にWebアプリケーションで取得するIPアドレスが常に127.0.0.1になってしまいます。 127.0.0.1ではなくApacheに接続しているクライアントのIPアドレスを取得したいと思うでしょう。 このような時に http-forwaded モジュール の出番となります。

http-forwaded を使う

Jetty で http-forwaded を有効にします。

# cd ${JETTY_BASE}
# java -jar $JETTY_HOME/start.jar --add-to-start=http-forwarded

${JETTY_BASE}のstart.d配下にhttp-forwarded.iniが作成されます。 初期値はモジュールが有効になっているだけです。 Jettyを再起動し、http-forwaded モジュールが有効な状態でJSPにアクセスします。

  1. Apache経由 -> 172.16.17.21
  2. Jetty直接 -> 172.16.17.21
$ curl  https://192.168.48.2/ -k

<html>
  <head><title>GET IP</title></head>
  <body>
   ipAddress ->[172.16.17.21]
  </body>
</html>

$ curl http://192.168.48.2:8080/

<html>
  <head><title>GET IP</title></head>
  <body>
   ipAddress ->[172.16.17.21]
  </body>
</html>

今度はApache経由でも利用者のIPアドレスが取得出来ました。 http-forwaded モジュールはリクエストのHTTPヘッダーの値をIPアドレスとして扱います。 デフォルトでは対象のHTTPヘッダー名はX-Forwarded-Forです。

一方でApacheのmod_proxyでは、デフォルトでProxyする際に接続元IPアドレスをX-Forwarded-Forに含めて転送します。 Apacheで接続元のIPアドレスがX-Forwarded-Forに付与され、JettyではX-Forwarded-Forを接続元IPアドレスとして認識するという流れです。

問題点

X-Forwarded-ForはリクエストHTTPヘッダーであり、アクセス時に自由に制御できます。 利用者の端末からX-Forwarded-For を付けてアクセスをします。

$ curl --header "X-Forwarded-For: 10.0.0.1" https://192.168.48.2/ -k

<html>
  <head><title>GET IP</title></head>
  <body>
   ipAddress ->[10.0.0.1]
  </body>
</html>

JSPの結果より、取得したソースIPアドレスが 10.0.0.1 とアクセス時にリクエストHTTPヘッダーで指定した値となってしまいます。

何が起きているのか

Apacheのmod_proxyでは、クライアントからのリクエストHTTPヘッダーにX-Forwarded-Forがあった場合「追加」で Proxy先に転送します。

Jettyのhttp-forwaded モジュールではHTTPヘッダー内にカンマ区切りで複数のIPアドレスが存在する場合、 一番左側の値をIPアドレスとして認識します。

これにより、ソースIPアドレスが10.0.0.1 と認識されてしまいます。

この例ではただ単にJSPの値が変わるだけで害が無いように見えるかもしれません。 しかし、WebアプリケーションでソースIPアドレスでアクセス制御をしていたら 攻撃者がHTTPヘッダーを細工してアクセス制御を突破できてしまいます。 また監査ログにIPアドレスを出力していたら偽装されている可能性があり信用できない値になってしまいます。

Forwardedヘッダーを使った偽装

X-Forwarded-Forの他にリクエストHTTPヘッダーにForwardedを含めることでもIPアドレスの偽装が可能です。

$ curl --header "Forwarded: for=10.0.0.2; proto=https" https://192.168.48.2/ -k

<html>
  <head><title>GET IP</title></head>
  <body>
   ipAddress ->[10.0.0.2]
  </body>
</html>

HTTPリクエストヘッダーForwardedはRFC7239で定義されている拡張ヘッダーです。 http-forwaded モジュールはRFC7239に対応しており、 リクエストHTTPヘッダーにForwardedが正しい形式で含まれているとそれをIPアドレスと認識します。 また X-Forwarded-ForForwarded の両方が含まれている場合 Forwarded が優先されます。

Apache はデフォルトでは Forwarded に対して何もしません。 クライアントのリクエストにForwardedが含まれているとそのまま Jetty に転送されるため 偽装が可能となります。

対策

Apache にて下記の設定を行います。(学認のMLに記載があります)

RequestHeader unset Forwarded
RequestHeader unset X-Forwarded-For

RequestHeaderディレクティブでリクエストヘッダーのForwardedX-Forwarded-Forを削除します。 Apacheの動作としてはmod_headersによるRequestHeaderの削除処理の後に、mod_proxyのハンドラでX-Forwarded-Forの追加が行われます。 従ってクライアントからのリクエストのX-Forwarded-Forは削除され、Apacheの接続元IPアドレスがX-Forwarded-Forにセットされますので JettyでApacheの接続元IPアドレスが取得できます。

ForwardedはとにかくJettyに渡らないようにします。

ポート番号とスキーム

今回の問題と少し話がそれますが、学認の構築手順では下記の設定があります。

RequestHeader set X-Forwarded-Port 443
RequestHeader set X-Forwarded-Proto https

X-Forwarded-PortX-Forwarded-Proto は Jetty に https と 443 を認識させるために付いています。 HTTPSでサービス提供する場合は付けるべきです。 これまではソースIPアドレスの偽装に焦点を充てていましたが、http-forwardedモジュールはプロトコルやスキームもリクエストHTTPヘッダーの値を認識します。 JSPにrequest.getScheme()request.getServerPort()を追加し、リクエストHTTPヘッダーを細工してアクセスすることで確かめることが出来ます。 Webアプリケーションでこれらのメソッドを使っている際に(例えば自身のリダイレクトURLを生成する際に使うのではないでしょうか。) 意図しない値が返る可能性があるためです。

Apacheを経由しない経路は作らないこと

上記の対策は、Apacheの設定による対策です。Apacheを経由しない経路があると対策になりません。 Apacheで対策しても Jetty に直接アクセスすると偽装可能です。

$ curl --header "Forwarded: for=10.0.0.2; proto=https" http://192.168.48.2:8080/

<html>
  <head><title>GET IP</title></head>
  <body>
   ipAddress ->[10.0.0.2]
  </body>
</html>

学認のShibboleth IdP構築手順には、任意の設定ですがバックチャネルの通信を行うために Jetty で 8443 で Listen する設定手順があります。 2021/9に Apache による対策を行うアナウンスがされておりましたが この 8443 の通信は Apache を経由しない経路であったためにIPアドレスの偽装が可能でした。 本件について学認に報告し、バックチャネルの通信の手順が修正されています。1

このようにサーバー構成を把握し、Apacheを経由しない経路がないことを確認することも重要です。

まとめ

Webアプリケーションの前段にリバースプロキシサーバーやロードバランサー等が存在し、 ユーザーのアクセスが直接アプリケーションサーバーに届かない構成を取ることは多くあると思います。

「リクエストHTTPヘッダーからIPアドレスを取得」は便利で利用した方が良いと思います。 しかし、設定を誤ったり理解が不足していると偽装されてしまう恐れがあります。 利用にあたっては次の仕様を把握して設定することが重要だと考えます。

  1. 前段の機器がどのようにリクエストHTTPヘッダーにIPアドレスをセットするか
    • ヘッダー名は何?すでにリクエストに含まれていた場合は上書きする?複数入ることはありうる?
  2. IPアドレスを認識するソフトウェアは、どのようにIPアドレスとして認識するか
    • ヘッダー名は何?複数含まれていた場合はどうなる?

  1. Jettyの設定により 8443 の Listen は http-forwardedモジュールを有効としない対策がされています。 Jettyのhttp-forwardedモジュールによる対策が出来れば良いと思っているのですがソースコードを確認した限り 有効な手段となる方法は見つけられませんでした。Apacheのmod_remoteipのRemoteIPInternalProxyディレクティブにあたる機能があれば良いのですが… ↩︎