** 本記事は、Inside the code: How the Log4Shell exploit works の翻訳です。最新の情報は英語記事をご覧ください。**
Apache の Java ベースのログ記録ユーティリティ Log4j に存在する重大な脆弱性 (CVE-2021-44228) は、「最近 10 年間で最も深刻な脆弱性」と呼ばれています。この脆弱性は Log4Shell とも呼ばれており、多くのソフトウェア製品の開発者が、アップデートや対応策を顧客に提供する必要に迫られています。この脆弱性が発見されて以来、Log4j のメンテナンス担当者は 2 件の新バージョンを発表しました。2 件目のバージョンでは、この脆弱性の発端となった機能が削除されています。
以前の記事でも触れた通り、Log4Shell は、Log4j の「メッセージ置換」機能のエクスプロイトです。この機能は本来、外部コンテンツを呼び出す文字列を挿入することで、イベントログをプログラム側から修正するものです。この機能をサポートするコードでは、Java Naming and Directory Interface (JNDI) URL を使用した「ルックアップ (検索)」が可能でした。
この機能を利用すると、攻撃者は、Log4j を使用するソフトウェアへのリクエストに悪意のある JNDI URL が埋め込まれた文字列を挿入できます。挿入される URL はリモートコードをロードし、ロガーで実行するものです。この機能の悪用がどれほど危険であるかをより良く理解するために、悪用に用いられるコードを詳しく分析します。
Log4j によるロギングの仕組み
Log4j は時間、スレッド、カテゴリおよびコンテキスト情報から成る TTCCLayout クラスを用いてイベントログを出力します。デフォルトでは、以下のフォーマットが用いられます。:
%r [%t] %-5p %c %x - %m%n
上記の %r はプログラムが開始してからの経過時間 (ミリ秒)、%t はスレッド、%p はイベントの優先度、%c はカテゴリ、%x はイベントを生成したスレッドに関連するネスト化診断コンテキスト、%m は記録されるイベントに関係するアプリケーションから提供されるメッセージを表しています。この最後の欄 (%m) を利用して、脆弱性が悪用されます。
今回の脆弱性は、JNDI URL (「jndi:dns://」、「jndi:ldap://」、あるいは以前の記事で紹介した他の JNDI 定義インターフェイス) を含むメッセージパラメータを用いて「logger.error()」関数が呼び出された際に悪用されます。上記の URL がパラメータとして関数に渡されると、JNDI の「検索」が呼び出され、リモートコードが実行される可能性があります。
脆弱性を再現するため、公開されている多くの概念実証 (PoC) のうち、多くのアプリケーションが Log4j とどのように相互作用するかを再現している 1 件を分析しました。この PoC 内で logger/src/main/java/logger/App.java に位置するコードでは、メッセージパラメータ (以下のスクリーンショットの「msg」) を用いて logger.error() 関数が呼び出されています。
package logger; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.logger; public class App { private static final Logger logger = LogManager.getLogger(App.class); public static void main(String[] args) { String msg = (args.length > 0 ? args [0] : ""); logger.error(msg); } }
デバッグのため、msg の中身を JNDI で DNS を使用するテスト URL (Interactsh ツールで構成されたもの) に変更して「logger.error()」関数にパラメータとして渡し、プログラムを実行しました。
細工した URL を引数として「AbstractLogger」クラスの「logger.error()」メソッドが呼び出された後、「logMessage」という別のメソッドが呼び出されていることがわかります。
メソッド「log.message」は、指定された URL のメッセージオブジェクトを作成します。
次に、このメソッドは「LoggerConfig」クラスから「processLogEvent」メソッドを呼び出し、イベントのログを記録します。
その後、「AbstractOutputStreamAppender」クラスの「append」メソッドが呼び出され、メッセージオブジェクトがログに追加されます。
脆弱性の悪用箇所
上記の結果、「directEncodeEvent」メソッドが呼び出されます。
この directEncodeEvent メソッドは、ログメッセージをフォーマットし、与えられたパラメータをログメッセージに追加する「getLayout().Encode」メソッドを呼び出します。今回の例では、上記のテスト URL が追加されます。
次に、新しく「StringBuilder」オブジェクトが作成されます。
「StringBuilder」オブジェクトは、「MessagePatternConvert」クラスの「format」メソッドを呼び出し、与えられた URL を解析します。URL を特定するために、‘$’ および ‘{’ という文字列が検索されます。
その後、‘:’ または ‘-’ で区切られたさまざまな変数名とその値を識別しようとします。
そして、「StrSubstitutor」クラスから「resolveVariable」メソッドを呼び出し、変数を特定します。特定される変数は以下のいずれかです。
{date, java, marker, ctx, lower, upper, jndi, main, jvmrunargs, sys, env, log4j}
さらに、このコードは「Interpolator」クラスの「lookup」メソッドを呼び出し、変数に関連付けられたサービス (今回の例では「jndi」) を探します。
「jndi」を発見すると、「jndiManager」クラスから「lookup」メソッドが呼び出され、 JNDI リソースの値が評価されます。
その後、「IntialContext」クラスから「getURLOrDefaultInitCtx」が呼び出されます。このメソッドは、取得した URL に応じて、コンテキストを取得するために JNDI インターフェイスに送信されるリクエストを作成します。この段階から、脆弱性が悪用されはじめます。今回の例では、URL スキームは DNS です。
今回のような DNS URL の場合、リクエストが送信された後、取得した URL への DNS クエリを Wireshark で確認できます。
(ただし、今回用いたのはテスト URL であり、悪意のあるものではありません。)
URL が「jndi:ldap://」で始まる場合、「ldapURLContext」クラスの別のメソッドが呼び出され、URL が「queryComponents」を含むかどうかが確認されます。
その後、「ldapURLContext」クラスの「lookup」メソッドが呼び出されます。ここでは、変数「name」には ldap URL が格納されています。
接続が完了すると、「OutputStreamManager」クラスから「flushBuffer」メソッドが呼び出されます。以下のスクリーンショットの ‘buf’ には LDAP サーバーから返されたデータ (今回の例では「mmm….」という文字列) が格納されています。
Wireshark でパケットキャプチャを見ると、作成されるリクエストには以下のような部分があることがわかります。
この部分はシリアル化されたデータで、クライアントには以下のように表示され、脆弱性が悪用されたことを示します。スクリーンショット中の「[main] ERROR logger .App」という文字列とその後ろのデータに注目してください。:
脆弱性の修正パッチ
上記の脆弱性は、Log4j 2 のバージョン 2.14 までの (セキュリティリリース 2.12.2 を除く) すべてのバージョンに存在します。バージョン 2.14 までは JNDI サポートが解決できる名前に制限がなかったためです。これらのバージョンではいくつかのプロトコルは安全でなく、またリモートコード実行 (RCE) が可能でした。Log4j 2.15.0 は JNDI を LDAP 検索のみに限定し、これらの検索機能は、デフォルトでローカルホストの Java プリミティブオブジェクトへの接続のみに限定されています。
しかし、バージョン 2.15.0 においても一部の脆弱性は残存しています。コンテキストルックアップ (「$${ctx:loginId}」など) や、スレッドコンテキストマップパターン (「%X」、「%mdc」、あるいは「%MDC」) など、「特定の非デフォルト」レイアウトパターンを用いた実装では、JNDI 検索パターンを通じて悪意のある入力データを作成し、サービス拒否 (DOS) 攻撃を実行することが可能でした。最新のリリースでは、デフォルトですべての検索が無効になっています。この設定は JNDI 機能を完全に無効化してしまいますが、リモートからの悪用に対して Log4j を保護します。
結論
Log4j は、非常に有名なロギングフレームワークであり、相当数の人気ソフトウェア製品、クラウドサービス、その他のアプリケーションで使用されています。攻撃者は 2.15.0 以前のバージョンの脆弱性を悪用することで、関係するアプリケーションや、その基盤となる OS からデータを取得したり、Java ランタイム (Windows システムの Java.exe) に与えられた権限で Java コードを実行したりできます。このコードは、ローカルの OS に対してコマンドやスクリプトを実行することができ、さらに悪意のあるコードのダウンロードや権限の昇格、持続的なリモートアクセスの経路を提供することに繋がります。
脆弱性が公表された際にリリースされた Log4j バージョン 2.15.0 ではこれらの問題は修正されていますが、DOS 攻撃やエクスプロイトに関する脆弱性が残っています (2.16.0 で少なくとも一部は修正されています)。 12 月 18 日には、サービス拒否を引き起こす可能性のある、再帰的な問い合わせを使った攻撃を防止する修正が施された 3 件目の新バージョンである 2.17.0 がリリースされました。各組織は、内部で開発しているアプリケーションに含まれる Log4j のバージョンを調査し、最新バージョン (Java 7 では 2.12.2、Java 8 では 2.17.0) へのパッチを適用しましょう。また、ベンダーからのソフトウェアパッチもリリース後すぐに適用しましょう。
ソフォス製品は、この脆弱性に関連するネットワークの動作やペイロードを以下のような検出名で検出します。
AV:
- Troj/JavaDl-AAN
- Troj/Java-AIN
- Troj/BatDl-GR
- Mal/JavaKC-B
- XMRig Miner (PUA)
- Troj/Bckdr-RYB
- Troj/PSDl-LR
- Mal/ShellDl-A
- Linux/DDoS-DT
- Linux/DDoS-DS
- Linux/Miner-ADG
- Linux/Miner-ZS
- Linux/Miner-WU
- Linux/Rootkt-M
IPS:
Sophos Firewall:
- SIDs : 2306426, 2306427, 2306428, 58722, 58723, 58724, 58725, 58726, 58727, 58728, 58729, 58730, 58731, 58732, 58733, 58734, 58735, 58736, 58737, 58738, 58739, 58740, 58741, 58742, 58743, 58744, 58751, 58784, 58785, 58786, 58787, 58788, 58789, 58790, 58795
Sophos Endpoint
- SID: 2306426, 2306427, 2306428, 2306438, 2306439, 2306440, 2306441
Sophos SG UTM
- SID: 58722, 58723, 58724, 58725, 58726, 58727, 58728, 58729, 58730, 58731, 58732, 58733, 58734, 58735, 58736, 58737, 58738, 58739, 58740, 58741, 58742, 58743, 58744, 58751, 58784, 58785, 58786, 58787, 58788, 58789, 58790, 58795