エンタープライズギークス (Enterprise Geeks)

企業システムの企画・開発に携わる技術者集団のブログです。開発言語やフレームワークなどアプリケーション開発に関する各種情報を発信しています。ウルシステムズのエンジニア有志が運営しています。

Struts1の脆弱性問題の対処方法

Apache Struts 1の脆弱性問題の対処方法を紹介する。

この問題は、4月24日に株式会社ラックによって以下のように報告された。
http://www.lac.co.jp/security/alert/2014/04/24_alert_01.html

ServletFilterによる対処では、multipart/form-dataに対応できない。

上記報告に記載されている

不正な文字列がパラメータに含まれる場合(正規表現:「(^|\W)[cC]lass\W」に合致するパラメータ名が含まれる場合等)にリクエストを拒否するフィルタ機能を実装する

という方法は、リスク軽減策としては有効だ。
しかし残念ながら、ServletFilterで対応するこの方法では enctype="multipart/form-data" であるサブミット情報が来た場合は防ぐことができない。

問題の本質

Strutsのセキュリティ問題の多くは、本質的にはサブミット情報をActionFormオブジェクト配下のオブジェクトツリーに反映する際に発生する。今回のClassLoader操作に関しても、そういった攻撃パターンの1つである。過去には、ActionFormオブジェクト配下のオブジェクトツリーに存在する、MultipartRequestHandlerやServletオブジェクトも攻撃の標的になったこともある。そのため、本質的にはActionFormオブジェクト配下のオブジェクトツリーに対するアクセスを制限することが、セキュリティ対策には重要となる。

要するに、ActionFormオブジェクト配下のオブジェクトツリーは、インターネットから(サブミット情報によって)自在に操作可能であるため、そこに制限を加える必要があるのだ。

Struts1の脆弱性問題への対処方法

今回我々は、Strutsが同梱している commons-beanutils.jar を修正し、ActionFormオブジェクト配下であっても、特定の型のオブジェクトには触らせないようにする修正を行った。「特定の型」に含めたのは、以下のクラス群である。

これらの型が、ActionFormオブジェクト配下のオブジェクトツリーの中間ノードや末端ノードに来た場合、不正とみなしてエラーとする。チェックロジックは以下の通り。

/**
 * セキュリティ上、操作すべきではないクラスを扱っていないかを検査する。 検査の結果、問題がある場合は
 * IllegalBeanManipulationException を発生する。
 */
public class BeanutilsSecurityChecker {
    /** デフォルトで禁止するクラス名 */
    protected static String[] DEFAULT_CHECK_CLASS_NAMES = {
        "java.lang.Class",
        "java.lang.ClassLoader",
        "javax.servlet.Servlet",
        "org.apache.struts.upload.MultipartRequestHandler"};
    /** 禁止するクラス一覧 */
    protected static Class[] CHECK_CLASSES;
    static {
        defineCheckClassNames(DEFAULT_CHECK_CLASS_NAMES);
    }
    /**
     * 禁止するクラス名を設定する。
     * 
     * @param checkClassNames 禁止するクラス名
     */
    public static void defineCheckClassNames(String[] checkClassNames) {
        List list = new ArrayList();
        for (int i = 0; i < checkClassNames.length; i++) {
            try {
                list.add(Class.forName(checkClassNames[i]));
            } catch (ClassNotFoundException e) {
                e.printStackTrace(); // 通知のみ
            }
        }
        CHECK_CLASSES = (Class[]) list.toArray(new Class[0]);
    }
    /**
     * チェックロジック本体。
     * 
     * @param object チェック対象のオブジェクト
     * @throws IllegalBeanManipulationException 検査の結果、問題がある場合に発生
     */
    public static void check(Object object) throws IllegalBeanManipulationException {
        if (object == null) {
            return;
        }
        for (int i = 0; i < CHECK_CLASSES.length; i++) {
            Class c = CHECK_CLASSES[i];
            if (c.isInstance(object)) {
                throw new IllegalBeanManipulationException(object.getClass().getName());
            }
        }
    }
}

このチェックに引っかかった場合は、以下の例外を発生させる。

/**
 * セキュリティ上、操作すべきではないクラスを扱っている場合に発生。
 */
public class IllegalBeanManipulationException extends RuntimeException {
    /**
     * コンストラクタ
     * 
     * @param message インスタンス化しようとしたオブジェクトのクラス名
     */
    public IllegalBeanManipulationException(String message) {
        super(message);
    }
}

このようなチェックをする場所は、org.apache.commons.beanutilsパッケージのPropertyUtilsとなる。Strutsが常に呼び出すメソッドはgetNestedPropertyであるため、少なくともこのメソッドにチェックロジックを埋め込む必要がある。getNestedPropertyに対するチェックロジックは以下の通りだ。

    public static Object getNestedProperty(Object bean, String name)
            throws IllegalAccessException, InvocationTargetException,
            NoSuchMethodException {
       // ・・・ 中略 ・・・

            if (bean == null) {
                throw new IllegalArgumentException
                        ("Null property value for '" +
                        name.substring(0, indexOfNESTED_DELIM) + "'");
            }

            BeanutilsSecurityChecker.check(bean); // [S2-020]対応 https://struts.apache.org/release/2.3.x/docs/s2-020.html

            name = name.substring(indexOfNESTED_DELIM + 1);
        }

        indexOfINDEXED_DELIM = name.indexOf(INDEXED_DELIM);
        indexOfMAPPED_DELIM = name.indexOf(MAPPED_DELIM);

        if (bean instanceof Map) {
            bean = ((Map) bean).get(name);
        } else if (indexOfMAPPED_DELIM >= 0) {
            bean = getMappedProperty(bean, name);
        } else if (indexOfINDEXED_DELIM >= 0) {
            bean = getIndexedProperty(bean, name);
        } else {
            bean = getSimpleProperty(bean, name);
        }

        BeanutilsSecurityChecker.check(bean); // [S2-020]対応 https://struts.apache.org/release/2.3.x/docs/s2-020.html

        return bean;

    }

なお、PropertyUtilsには他にも多くのメソッドがあり、このようなチェックを行うべき箇所がいくつかある。ひとまず、以下のメソッドに対しては、チェックを埋め込むことをお薦めする。

  • getNestedProperty
  • setNestedProperty

S2-020と類似のStruts1の脆弱性によって、任意のJavaコードが実行可能なのか?

Tomcat8であれば、残念ながらYESだ。Tomcat8のクラスローダーは、WebappClassLoaderというカスタムクラスローダーとして実装されており、このクラスローダーは、引数なしのgetResourcesメソッドを提供している。Tomcat8では、このオブジェクトツリー配下を辿って行くことで、Tomcatアクセスログファイルを通常のJSPとして認識させ、任意のJavaコードをJSPスクリプトレットとして実行する所まで持ち込むことが可能だ。

Tomcat8以外のAPサーバーのリスクに関しては、ClassLoaderの作り次第となる。各APサーバーは標準のjava.lang.ClassLoaderを継承し、カスタムクラスローダーを作ってあるはずだが、そこでTomcat8のようなpublicメソッドを提供しているかどうかに依存する。

[近棟 稔]