オープンソースの脆弱性対策に学ぶ~Apache Struts でリモートから任意のコードが実行可能な脆弱性の対策1(CVE-2017-9805、S2-052)~
ここでは「オープンソースの脆弱性対策に学ぶ」と題して、具体的な事例を紹介しながらセキュアコーディングについて考えてみたいと思います。(約2年ぶりの続編となります。前回シリーズはこちら)
半年ほど前になりますが、Apache Struts でリモートから任意のコードが実行可能な脆弱性が見つかり大きな騒ぎになったことは皆様の記憶に新しいかと思います。
最初のパッチが提供されるも、対策が不十分だったり連鎖的に脆弱性が見つかったりしたため、相次いで追加のパッチが提供され、その対応に不眠不休を余儀なくされた方もいらっしゃいました。
国内の幾つかのサイトでは、実際にこれらの脆弱性を突いたと考えられる攻撃によって大きな被害が発生しました。
その時のことを考えますと対応に尽力された皆様には、本当にお疲れ様でしたと申し上げたいところです。
それから半年経ちまして・・・
ようやく落ち着きを取り戻したと感じられる今日この頃だった訳ですが、昨日(9月6日)に、またしてもApache Struts でリモートから任意のコードが実行可能な脆弱性が見つかったとのニュースが報じられました。
詳しくは以下のリンクをご確認ください。
- IPA 情報セキュリティ
Apache Struts2 の脆弱性対策について(CVE-2017-9805)(S2-052) - JVN
JVNVU#92761484 Apache Struts2 に任意のコードが実行可能な脆弱性 (S2-052)
大変危険な脆弱性であるため、緊急に対策を講じることが必要となります。
奇しくも半年前と同じApache Struts ですので、前回の教訓を活かして今回は実被害につながる前に手を打てたらいいなと思います。
さて、急ごしらえではありますが今回の脆弱性について確認しましたところ、Apache Struts2 のRESTプラグインにおけるXMLペイロードのデシリアライズ処理に問題があるようです。
デシリアライズ処理によって何の制限もなく任意のクラスのインスタンスが復元される状態のようですので、情報漏えい、改ざん破壊、権限昇格、バックドア設置、サービス停止など、悪用次第で様々なシナリオが考えられそうです。
対策の基本的な方針としては、XMLペイロードを無条件で受け入れてデシリアライズするのではなく、検証する、制限することになります。
調査1
これを踏まえて、先ずはGitHubでStruts のコードの修正箇所を調査しました。
以下に核心部分と思われる修正箇所を抜き出してみます。
XStreamHandler.java より抜粋
package org.apache.struts2.rest.handler; import com.thoughtworks.xstream.XStream; public class XStreamHandler implements ContentTypeHandler { public void toObject(Reader in, Object target) { XStream xstream = createXStream(); xstream.fromXML(in, target); } protected XStream createXStream() { return new XStream(); } }
デシリアライズ処理はXStreamクラスのfromXMLメソッドが行っていますが、XMLペイロードに対する検証や制限は実装されていません。
なお、XStreamはStrutsとは別のOSSになります。
XStreamHandler.java より抜粋
package org.apache.struts2.rest.handler; import com.opensymphony.xwork2.ActionInvocation; import com.opensymphony.xwork2.ModelDriven; import com.thoughtworks.xstream.XStream; public class XStreamHandler extends AbstractContentTypeHandler { public void toObject(ActionInvocation invocation, Reader in, Object target) { XStream xstream = createXStream(invocation); xstream.fromXML(in, target); } protected XStream createXStream(ActionInvocation invocation) { XStream stream = new XStream(); stream.addPermission(NoTypePermission.NONE); addPerActionPermission(invocation, stream); addDefaultPermissions(invocation, stream); return stream; } private void addPerActionPermission(ActionInvocation invocation, XStream stream) { Object action = invocation.getAction(); if (action instanceof AllowedClasses) { Set<Class<?>> allowedClasses = ((AllowedClasses) action).allowedClasses(); stream.addPermission(new ExplicitTypePermission(allowedClasses.toArray(new Class[allowedClasses.size()]))); } if (action instanceof AllowedClassNames) { Set<String> allowedClassNames = ((AllowedClassNames) action).allowedClassNames(); stream.addPermission(new ExplicitTypePermission(allowedClassNames.toArray(new String[allowedClassNames.size()]))); } if (action instanceof XStreamPermissionProvider) { Collection<TypePermission> permissions = ((XStreamPermissionProvider) action).getTypePermissions(); for (TypePermission permission : permissions) { stream.addPermission(permission); } } } protected void addDefaultPermissions(ActionInvocation invocation, XStream stream) { stream.addPermission(new ExplicitTypePermission(new Class[]{invocation.getAction().getClass()})); if (invocation.getAction() instanceof ModelDriven) { stream.addPermission(new ExplicitTypePermission(new Class[]{((ModelDriven) invocation.getAction()).getModel().getClass()})); } stream.addPermission(NullPermission.NULL); stream.addPermission(PrimitiveTypePermission.PRIMITIVES); stream.addPermission(ArrayTypePermission.ARRAYS); stream.addPermission(CollectionTypePermission.COLLECTIONS); stream.addPermission(new ExplicitTypePermission(new Class[]{Date.class})); } }
XStreamクラスのaddPermissionメソッドを使用して、デシリアライズで復元を許可するクラスを制限する処理が追加されています。
個別に許可したいクラスがある場合には、AllowedClasses、AllowedClassNames、XStreamPermissionProvider辺りをimplementすることで実現可能です。
この修正が対策として十分かどうかはもう少し分析が必要かもしれませんが、リスクは格段に小さくなっていると考えられます。
調査2
調査1で率直に感じた疑問は以下の点です。
- なぜ今までXStreamクラスのaddPermissionメソッドを使った制限が実装されて来なかったのか?
- そもそも今回の修正が行われる以前にコードが修正されたのはいつだったのか?
ということで、GitHubでStrutsのXStreamHandler.javaの履歴を調査してみました。
日付 | 履歴 |
---|---|
2017/08/21 | Commits on Aug 21, 2017 Allows define allowed classes per action lukaszlenart |
2017/08/02 | Commits on Aug 2, 2017 Adds an abstract layer to allow easily handle API change lukaszlenart |
2008/06/12 | Commits on Jun 12, 2008 WW-2623 Struts 2.1.3 omnibus ticket … Rainer Hermanns |
2008/04/27 | Commits on Apr 27, 2008 WW-2147 … Antonio Petrelli |
GitHubの記録は正確であると仮定して話を進めますが、今回の修正が行われる以前の修正は2008年6月のようです。
この時点でXStreamクラスのaddPermissionメソッドは使われていませんでした。
今度はXStreamの履歴を調査しました。
日付 | 履歴 |
---|---|
2017/05/23 | 1.4.10 Released |
2014/02/08 | 1.4.7 Released Added methods addPermission, denyPermission, allowTypesXXX and denyTypesXXX to c.t.x.XStream to setup security at unmarshalling time. |
2008/12/06 | 1.3.1 Released |
2008/02/27 | 1.3 Released |
この結果、2008年6月の時点ではXStreamのバージョンは1.3であり、2017年9月現在は1.4.10であること、XStreamクラスのaddPermissionメソッドは2014年8月にリリースされた1.4.7で追加されたことが分かります。
したがって、少なくとも2008年6月の時点ではこの問題を解消することは困難だったと思われます。
仮に2008年6月に問題に気付いていたとしたら、デシリアライズ処理で復元可能なクラスを制限する機能を持たないXStreamはそもそも採用されなかったかもしれません。
次のタイミングとして、XStreamクラスのaddPermissionメソッドが追加された2014年8月ならば、この変更に対してStruts側が対応できた可能性があります。
もし対応していたならば、その時点で問題は解消されていたかもしれません。
しかし実際のところ、依存関係のある外部コンポーネントの変更を追跡して適切な対応を選択することは、高い技術力に加えて高い組織力が必要となりそうです。
この辺りには、セキュアコーディングに含まれるか微妙ですが、以下のような課題があると感じました。
-
- 必要かつ十分な機能性とセキュリティを備えた外部コンポーネントをどのように選定するのか?
Strutsの場合、XStream以外の選択肢はあり得たか?独自実装はあり得たか? - 外部コンポーネントを使って必要かつ十分な機能性とセキュリティを備えるにはどのようにソフトウェアを開発するのか?
Strutsの場合、XStreamクラスにaddPermissionメソッドが存在しない状況でどのようにセキュリティを実現することができたか? - どうしたら外部コンポーネントの変更を追跡でき、変更内容に応じて適切な対応を選択できるのか?
Strutsの場合、XStreamクラスの変更追跡、対応の選択はどうだったか?
- 必要かつ十分な機能性とセキュリティを備えた外部コンポーネントをどのように選定するのか?
まとめ
今回はApache Struts でリモートから任意のコードが実行可能な脆弱性の対策として、昨日公開されたRESTプラグインにおけるXMLペイロードのデシリアライズの不適切な処理に起因するリモートコード実行(Remote Code Execution)(CVE-2017-9805、S2-052)の脆弱性対策についてご紹介しました。
次回は半年前のContent-Typeのマルチパートパーサーの処理が引き起こすOGNL(Object Graph Navigation Language)コードの実行(CVE-2017-5638、S2-045)についてご紹介したいと思います。