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.xmlのstruts.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として評価されず例外が発生するようになっている。