技術者ブログ

クラウド型WAF「Scutum(スキュータム)」の開発者/エンジニアによるブログです。
金床“Kanatoko”をはじめとする株式会社ビットフォレストの技術チームが、“WAFを支える技術”をテーマに幅広く、不定期に更新中!

2017年10月

1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31        
  • お問い合わせはこちら
Scutum開発者/エンジニアによる技術ブログ WAF Tech Blog

Tomcatなど、Java製のサーバでBEAST対策を行う方法

はじめに

CBCモードへの選択平文攻撃を扱った前々回のエントリ、そしてBEASTの全体像について解説した前回のエントリに続き、今回はTomcatなどのJava製のサーバアプリケーションにおいて、BEAST対策を実施する方法について見ていきます。なお、本エントリの対象はOracleのJava1.6系となります。他のJava実行環境やJSSE実装では事情が異なる可能性があります。

Cipher Suiteの選択が鍵

SSLの通信では、まずクライアント側が暗号化に使いたいCipher Suitesの候補の一覧を送信し、サーバ側はその中から1つ選ぶという動作となります。例として、Google ChromeのSSL通信をWiresharkでパケットキャプチャしたときのイメージを示します。


ClientHelloメッセージの中で、36ものCipherSuiteのリストが送信されていることがわかります。

次に、サーバ側から送られるServerHelloメッセージの中身を見てみます。


下から2行目から、今回はサーバ側がTLS_RSA_WITH_AES_128_CBC_SHAを選択したことがわかります。

Javaではサーバ側でCipherSuiteの優先順位を決定することができない

サーバ側でのBEAST対策として、CipherSuiteにRC4のようなストリーム暗号を使用するものを選ぶという方法があります。しかし残念なことに、TomcatのようなJavaで書かれたサーバアプリケーションでは、クライアント側から送られるCipherSuiteの一覧が先頭から順番に評価され、サーバ側で有効にしているものが見つかった時点で、そのCipherSuiteが選択されます。そのため、サーバ側がAESとRC4のどちらも有効になっており、かつクライアント側がRC4よりもAESを使いたがっている場合には、必ずAESが選択されてしまいます。

該当部分のソースコードはcom.sun.net.ssl.internal.sslパッケージのServerHandshakerクラス内にあります。以下のchooseCipherSuiteという関数内でCipherSuiteが決定されます。

/*
 * Choose cipher suite from among those supported by client. Sets
 * the cipherSuite and keyExchange variables.
 */
private void chooseCipherSuite(ClientHello mesg) throws IOException {
    for (CipherSuite suite : mesg.getCipherSuites().collection()) {
        if (isEnabled(suite) == false) {
            continue;
        }
        if (doClientAuth == SSLEngineImpl.clauth_required) {
            if ((suite.keyExchange == K_DH_ANON) || (suite.keyExchange == K_ECDH_ANON)) {
                continue;
            }
        }
        if (trySetCipherSuite(suite) == false) {
            continue;
        }
        return;
    }
    fatalSE(Alerts.alert_handshake_failure,
                "no cipher suites in common");
}

関数先頭のforループが、ClientHelloメッセージに含まれるCipherSuite一覧に対して行われていることがわかります。このようにソースコード内において完全にクライアント側を優先する実装となってしまっているため、設定項目の変更や起動オプションなどによってサーバ側の挙動を変更することはできないことがわかります。

サーバ側では、SSLSocketクラスのsetEnabledCipherSuitesメソッドを使うことにより、CipherSuiteの有効・無効については制御することができます。そのため、AESのようなブロック暗号を使用するCipherSuiteをすべて無効にしてしまうという方法も技術的には可能ですが、この場合実質的にRC4のみをサポートするSSLサーバとなってしまうでしょう(現実的にはRC4をサポートしないクライアントというのはあまりいないかと思われますので、それでも問題ないと考えることもできるかもしれません)。

JSSEをハックする

それでは、ここから何ができるか考えてみることにします。現実的には、次のような動作をするSSLサーバが望ましいでしょう。

  • クライアント側がAES等とRC4のどちらもサポートしている場合には、その順番によらず、RC4を使用する
  • クライアント側がAES等をサポートし、RC4をサポートしない場合には、AES等を使用する

これを実現するために、JSSE実装に手を入れ、ServerHandshakerクラスの挙動を変更します。ソースコードを書き換えてコンパイルし、クラスファイルを生成してからデフォルトの実装と入れ替えることで目的は達成できますが、ここでは別のアプローチを取ってみます。Javassistライブラリを利用して実行時にバイトコードを書き換えます。この方法のメリットとして、jsse.jarを書き換えずに済むため、Javaのバージョンアップの際に互換性の問題が出にくいという点があげられます。

chooseCipherSuite関数の中身を、次のようにサーバ側のCipherSuiteリストについてループするように書き換えます。

{
java.util.Collection c = enabledCipherSuites.collection();
java.util.Iterator p = c.iterator();
while( p.hasNext() )
  {
  com.sun.net.ssl.internal.ssl.CipherSuite suite = ( com.sun.net.ssl.internal.ssl.CipherSuite )p.next();
  if( !$1.getCipherSuites().contains( suite )
   || !suite.isAvailable()
    )
    {
    continue;
    }
  if (doClientAuth == com.sun.net.ssl.internal.ssl.SSLEngineImpl.clauth_required)
    {
    if ((suite.keyExchange.toString().equals( "DH_anon" ) ) || (suite.keyExchange.toString().equals( "ECDH_anon" ) ))
      {
      continue;
      }
    }
  if (trySetCipherSuite(suite) == false)
    {
    continue;
    }

  return;
  }
fatalSE( com.sun.net.ssl.internal.ssl.Alerts.alert_handshake_failure, "no cipher suites in common" );
}

Javassistで書き換えるためのソースコードであるため、ソースコード中のクラス名はパッケージ名を省略せずに記述しています。また、$1は1つめの引数(ClientHello mesg)を示します。

アプリケーションの起動に続き、書き換え対象のクラスがロードされる前にJavassistを使ってバイトコードを書き換えます。これは次のようなコードで行います。

private static void initServerHandShaker()
throws Exception
{
ClassPool pool = ClassPool.getDefault();
pool.get( "com.sun.net.ssl.internal.ssl.Debug" ).toClass();
pool.get( "com.sun.net.ssl.internal.ssl.Handshaker" ).toClass();
CtClass cc = pool.get( "com.sun.net.ssl.internal.ssl.ServerHandshaker" );
CtMethod method = cc.getDeclaredMethod( "chooseCipherSuite" );
String bodyStr = MStreamUtil.streamToString( MStreamUtil.getResourceStream( "net/jumperz/app/MCipherSuiteUtil/ServerHandshaker.txt" ) );
method.setBody( bodyStr );
cc.toClass();
}

コードはこちら(cipher_suite_util.jar)からダウンロードできます(ライセンスはMozilla Public License 1.1です)。実行には別途Javassistライブラリが必要となります。

また、注意が必要な点として、JavassistはJRE/lib/以下にあるjarファイルについてはバイトコードの書き換えを行うことができないという点が挙げられます。そのため、実行する際にはjsse.jarを別の場所に移動した上で、別途クラスパスに含むようにする必要があります。

使用方法

前項で示したコードはアプリケーションの起動のごく早い段階で実行される必要があるため、別のアプリケーションをランチャーのように起動する、ブートストラップ用のクラスとしてまとめました。例として、書き換えたJSSEを使って起動したいアプリケーションのクラス名が次のようなものである場合を考えます。

jp.example.app1

起動は通常、次のように行われるものとします。アプリケーションに対して2つの引数(arg1 arg2)を与えています。

java jp.example.app1 arg1 arg2

バイトコード書き換えを行う場合、前述したようにjsse.jarを移動してクラスパスに含み、またcipher_suite_util.jarとJavassistライブラリがロードできる状態(クラスパスの指定や、JRE/lib/ext/以下に配置する等)にした上で、次のようにします(クラスパスの記述は省略しています)。

java net.jumperz.app.MCipherSuiteUtil.Bootstrap jp.example.app1 arg1 arg2

このようにすることで、必要なクラスファイルの書き換えを行った後に、アプリケーション本体のクラスのmainメソッドがarg1とarg2を引数として呼び出されます。

Tomcatに適用する

それでは実際の例として、Tomcatに適用する方法(Linuxの場合)を見ていきます。

Tomcatの起動や終了はあらかじめ用意されているシェルスクリプトで行うことが一般的です。本来、Tomcat起動時のクラスパスを指定するためにはシェルスクリプトを書き換える必要があります。しかしTomcatにはJSSE_HOMEという環境変数を指定することでjsse.jarが別の場所にあってもロードすることができる機能があるので、今回はこれを利用します。また、cipher_suite_util.jarについてもこの機能をうまく利用することでクラスパスを書き換える必要がなくなります。

/usr/local/jsse/lib/というディレクトリを作成し、そこにJRE/lib/jsse.jarを移動します。また、同時にcipher_suite_util.jarをjcert.jarという名前にリネームしてこのディレクトリに置いておきます。

/usr/local/jsse/lib/
                   +--jsse.jar
                   +--jcert.jar

次のように、環境変数JSSE_HOMEの値を/usr/local/jsse/に設定します(bashの場合)。"lib"は含まなくてよいので注意します。

export JSSE_HOME=/usr/local/jsse/

また、JavassistのjarファイルをJRE/lib/ext/内に配置します。

続いて、アプリケーションが起動される際に、javaコマンドに与えられるクラス名の部分を書き換えます。このためにはシェルスクリプトを直接書き換える必要があります。書き換える対象のファイルはcatalina.shになります。

書き換え前

org.apache.catalina.startup.Bootstrap "$@" start \

書き換え後

net.jumperz.app.MCipherSuiteUtil.Bootstrap org.apache.catalina.startup.Bootstrap "$@" start \

書き換える箇所は、通常は300行目付近になります(Securityマネージャなどを使っている場合には別の場所になります)。

このようにすることで、サーバ側でCipherSuiteを選択可能なTomcatを起動することができるようになります。

少々ややこしいので、手順を箇条書きでまとめると以下のようになります。

  • /usr/local/jsse/lib/を作成する
  • JRE/lib/jsse.jarを/usr/local/jsse/lib/に移動する
  • cipher_suite_util.jarをjcert.jarという名前にして/usr/local/jsse/lib/に配置する
  • 環境変数JSSE_HOMEの値を/usr/local/jsse/に設定する
  • JavassistのjarファイルをJRE/lib/ext/に配置する
  • catalina.shの書き換えを行う

まとめ

このように、Javassistを利用することで、1.6系列のJavaにおいてサーバ側でCipherSuiteを選択することが可能となります。BEAST対策が現状サーバ側で必要とされるのかどうか、という点についてはさまざまな意見があるかと思いますが、「Javaサーバアプリケーションではそもそも対策できない」というのでは少々さみしい気がしますので、今回このようなエントリとしてまとめてみました。Guardian@JUMPERZ.NETのような、Java製のWAFアプリケーションなどでも、同じ方法を利用することができます。

BEASTについてのエントリはこの3つでひとまず区切りとなります。フィードバック等あればお気軽に@kinyukaまでご連絡ください。