SECURITY

Log4Shell - Log4jの脆弱性(CVE-2021-44228)を検出する(続報)

者/寄稿者:Splunkのセキュリティはいつでもファミリービジネスです。この記事は、Ryan Kovar、Shannon Davis、Johan Bjerke、James Brodsky、Dave Herrald、John Stoner、Drew Church、Mick Baccio、Jay Holladay、Lily Lee、Audra Streetman、Tamara Chaconの協力のもと執筆されました。

最新情報:Log4j RCE

Splunk SURGeチームは先日、世界中のセキュリティ防御チームに徹夜の対応を迫ったLog4jの脆弱性「Log4Shell」について、Splunk製品での対策をまとめた速報ブログセキュリティアドバイザリーを公開しています。

このブログでは、組織への攻撃の検出方法に関する追加の最新情報をお伝えします。攻撃の兆候を検出するために必要なログを収集していなくても、まだ間に合います。自社のホストが標的になっているかどうかを調査する方法がほかにも見つかりました。

Log4Shellの動作を検出する

スイスCERTは、この攻撃の各段階の概略を示す図を含む有用なブログを公開しました。この図には、検索の鍵となる情報も示されています。


Splunkでの検出方法は第1~2段階に集中しています。この段階では、脆弱性のあるサーバーに、攻撃の足掛かりとなるJNDIリクエストが送られます。

このログを記録していない場合でも、第3段階で検出する良い方法があります。そこで重要になるデータソースが、ネットワークトラフィックログとDNSクエリーログです。以下では、この2つのログを使用して、組織の環境内で侵害されたホストを検出する方法をご説明します。

SplunkでLog4Shell (Log4j 2 RCE)攻撃を検出する

侵入検知アラート

前提としてIDSの導入は欠かせません。IDSのルールを最新の状態に更新し、Splunkでアラートをインデックスするように設定していることを確認してください。ここではSuricataを使用しますが、この脆弱性のシグネチャに対応したIDSであれば製品は問いません。攻撃を検出するにはまず、次のインデックスをサーチします。

index=suricata ("2021-44228" OR "Log4j" OR "Log4Shell") 
| table _time, dest_ip, alert.signature, alert.signature_id

組織のネットワークから外部へのLDAPアクセスを検出する

組織と外部のネットワーク境界にあるファイアウォールで外部へのLDAPトラフィックを許可することはまずないでしょう。このトラフィックが検出された場合は、Log4Shellの初期アクセス活動が行われている可能性があります。SplunkのベストプラクティスとNetwork Trafficデータモデルを使用したtstatsによるサーチがこちらです。このサーチを使えば、プライベート(RFC1918)アドレス範囲外のIPアドレスに対するLDAP接続を検出できます。

| tstats earliest(_time) as earliest_time latest(_time) as latest_time values(All_Traffic.dest_ip) from datamodel=Network_Traffic.All_Traffic where All_Traffic.dest_port = 1389 OR All_Traffic.dest_port = 389 OR All_Traffic.dest_port = 636 AND NOT (All_Traffic.dest_ip = 10.0.0.0/8 OR All_Traffic.dest_ip=192.168.0.0/16 OR All_Traffic.dest_ip = 172.16.0.0/12) by All_Traffic.src_ip
| convert ctime(earliest_time) ctime(latest_time)


JNDIプローブとDNSクエリーを相関付ける

Log4jの脆弱性悪用の兆候を示すJNDI文字列の検出方法はすでに特定しています。では、その結果とプローブ(攻撃前の探査)の成功をどのように相関付ければよいでしょうか。ここで役立つのがDNSです。

最初のサーチでは、まず、正規表現を使ってJNDI文字列からドメインを抽出します。次に、抽出したドメインを追加してルックアップテーブルを更新します。このルックアップテーブルは次のサーチで使用します。このクエリーでは非構造化データを処理するため、通常のCPUサイクルの数倍、サーチするデータ量によってはある程度の時間がかかることに注意してください。このサーチを初めて実行するときは、まずルックアップファイルを作成するために、クエリー内のlookup行をコメントアウトしてください。

index=*  jndi
| rex field=_raw max_match=0 "[jJnNdDiI]{4}(\:|\%3A|\/|\%2F)(?\w+)(\:\/\/|\%3A\%2F\%2F)(\$\{.*?\}(\.)?)?(?[a-zA-Z0-9\.\-\_\$\{\:]+)"
| mvexpand rce_dest
| rex field=rce_dest "(?\d+\.\d+\.\d+\.\d+)"
| eval rce_domain = case(isnull(rce_ip),rce_dest)
| rex field=rce_domain "(?[0-9a-zA-A\-]+\.[0-9a-zA-A\-]+$)"
| dedup top_level_domain
| eval top_level_domain = "*.".top_level_domain
| where top_level_domain!=""
| lookup log4j_scanning_domain.csv query as top_level_domain OUTPUT query AS old_query
| where isnull(old_query)
| rename top_level_domain as query
| table query
| outputlookup append=t log4j_scanning_domain.csv

上記のサーチが完了すると、ドメインを格納したルックアップテーブルが作成されるので、Network Resolutionデータモデルを使ってtstatsサーチを実行し、JNDIプローブのドメインと一致するDNSクエリーを探します。

| tstats summariesonly=true allow_old_summaries=true
      values(host) as host, values(DNS.query_type) as DNS.query_type, values(DNS.reply_code) as DNS.reply_code, values(DNS.transport) as DNS.transport
      count from datamodel=Network_Resolution.DNS
      where [| inputlookup log4j_scanning_domain.csv | rename query as DNS.query | format] 
      by "DNS.src",sourcetype, DNS.query index _time span=1s
| stats earliest(_time) as first_seen, latest(_time) as last_seen sum(count) as count, values(DNS.reply_code) as DNS.reply_code, values(index) as index, values(DNS.src) as DNS.src, values(DNS.query_type) as DNS.query_type, values(DNS.transport) as DNS.transport by host DNS.query sourcetype
| convert timeformat="%m/%d/%Y %H:%M:%S" ctime(first_seen), ctime(last_seen)

新しいアウトバウンドトラフィックを検出する

2021年12月9日以前に送信トラフィックを生成していない内部サーバー(Egress)からのアウトバウンドトラフィックを検索する方法もあります。そのためには、期間をこの日(2021-12-09)の24時間以上前に設定して、標準的なトラフィックと比較できるようにします。このように挙動サーチの範囲を拡大すると、検索時間は長くなりますが、検索網を最大限に広げて侵害の兆候を捉えることができるメリットがあります。基本となるSPLがこちらです。

index=* src_ip=* dest_ip=* 
(NOT (dest_category="internal" OR dest_ip=10.0.0.0/8 OR dest_ip=172.16.0.0/12 OR dest_ip=192.168.0.0/16 OR dest_ip=100.64.0.0/10))
| stats
earliest(_time) as earliest 
latest(_time) as latest 
values(action) as action 
values(app) as app 
values(dest_port) as dest_port 
values(sourcetype) as sourcetype count 
by src_ip dest_ip
| eventstats max(latest) as maxlatest
```This is 2021-12-09 00:00:00```
| eval comparisonTime="1639008000"
```| eval comparisonTime="-1d@d" ```
| eval isOutlier=if(earliest >= relative_time(maxlatest, comparisonTime), 1, 0)
| convert timeformat="%Y-%m-%dT%H:%M:%S" ctime(earliest),ctime(latest) ,ctime(maxlatest)
| where isOutlier=1

このサーチはいくつかの方法で変更できます。

  • 「comparisonTime」の参照を前日(-1d@d)あるいは任意の相対時間または絶対時間に変更します。
  • by句で「dest_ip」を除外します。これは、送信トラフィックを生成しているサーバーの特定のみが目的の場合に便利です。このフィールドを除外すると、基数(カーディナリティ)が下がってサーチのパフォーマンスが向上します。
  • ここではインラインのSPLコメント( ` 記号3つ)を使用しています。古いバージョンのSplunkでは正しく解釈されない可能性があるため、これらの行を削除してもかまいません。
  • Network Trafficデータモデルでデータモデル高速化を使用している場合は、コマンドスイッチの「summariesonly=false」を「summariesonly=true」に変更することによって、このサーチのパフォーマンスを向上できます。
      | tstats summariesonly=false allow_old_summaries=true 
      earliest(_time) as earliest 
      latest(_time) as latest 
      values(All_Traffic.action) as action 
      values(All_Traffic.app) as app 
      values(All_Traffic.dest_ip) as dest_ip 
      values(All_Traffic.dest_port) as dest_port 
      values(sourcetype) as sourcetype count 
      from datamodel=Network_Traffic 
      where (NOT (All_Traffic.dest_category="internal" OR All_Traffic.dest_ip=10.0.0.0/8 OR All_Traffic.dest_ip=172.16.0.0/12 OR All_Traffic.dest_ip=192.168.0.0/16 OR All_Traffic.dest_ip=100.64.0.0/10))
      by All_Traffic.src_ip All_Traffic.dest_ip 
      | rename "All_Traffic.*" as * 
      | eventstats max(latest) as maxlatest
      ```This is 2021-12-09 00:00:00```
      | eval comparisonTime="1639008000"
      ```| eval comparisonTime="-1d@d" ```
      | eval isOutlier=if(earliest >= relative_time(maxlatest, comparisonTime), 1, 0)
      | convert timeformat="%Y-%m-%dT%H:%M:%S" ctime(earliest),ctime(latest) ,ctime(maxlatest)
      | where isOutlier=1
      

      ベースラインに基づいて新しい送信トラフィックを検出する

      これは、前述のtstatsを使ったサーチの応用です。ルックアップに格納した過去のアクティビティをベースラインとして使用します。初回は期間を長く設定してサーチを実行することでベースラインを作成し、2回目からはクエリーの実行頻度を増やして(1時間ごとなど)、ベースラインを継続的に最新の状態にします。この方法を使えば、環境内の最新のアクティビティがベースラインに常に反映されます。

      注:このサーチを初めて実行するときは、ルックアップ「egress_src_dest_tracker.csv」が設定されていないとエラーになります。エラーを避けるには、この名前の空のルックアップを作成するか、初回はこの行をコメントアウトしてサーチを実行してください。

      | lookup egress_src_dest_tracker.csv dest_ip src_ip OUTPUT earliest AS previous_earliest latest AS previous_latest
       
      
      | tstats summariesonly=false allow_old_summaries=true 
          earliest(_time) as earliest 
          latest(_time) as latest 
          values(All_Traffic.action) as action 
          values(All_Traffic.app) as app 
          values(All_Traffic.dest_ip) as dest_ip 
          values(All_Traffic.dest_port) as dest_port 
          values(sourcetype) as sourcetype count 
          from datamodel=Network_Traffic 
          where (NOT (All_Traffic.dest_category="internal" OR All_Traffic.dest_ip=10.0.0.0/8 OR All_Traffic.dest_ip=172.16.0.0/12 OR All_Traffic.dest_ip=192.168.0.0/16 OR All_Traffic.dest_ip=100.64.0.0/10))
          by All_Traffic.src_ip All_Traffic.dest_ip 
      | rename "All_Traffic.*" as * 
      | lookup egress_src_dest_tracker.csv dest_ip src_ip OUTPUT earliest AS previous_earliest latest AS previous_latest 
      | eval earliest=min(earliest, previous_earliest), latest=max(latest, previous_latest) 
      | fields - previous_*
      | appendpipe 
          [
          | fields src_ip dest_ip latest earliest
          | stats min(earliest) as earliest max(latest) as latest by src_ip, dest_ip 
          | inputlookup append=t egress_src_dest_tracker.csv
          | stats min(earliest) as earliest max(latest) as latest by src_ip, dest_ip 
          | outputlookup egress_src_dest_tracker.csv
          | where a=b
              ] 
      | eventstats max(latest) as maxlatest
      | eval comparisonTime="-1h@h" 
      | eval isOutlier=if(earliest >= relative_time(maxlatest, comparisonTime), 1, 0) 
      | convert timeformat="%Y-%m-%dT%H:%M:%S" ctime(earliest),ctime(latest) ,ctime(maxlatest)
      | where isOutlier=1
      

      ベースラインに基づいて新しい送信トラフィックを検出する

      ベースラインに基づいて新しい国からの受信トラフィックを検出する

      このサーチの考え方は前述のサーチと同じです。違いは、内部IPにアクセスする外部IPを検出し、その外部IPの位置情報を調べる点です。ルックアップは、送信元、宛先、送信元の国で構成されます。前述のサーチと同様に、大量の結果が返され、誤検知が含まれる場合があります。その緩和策として、調査したい特定のアプリケーションサーバーのみに検索範囲を限定すること、そして特に、デスクトップシステムを除外することをお勧めします。このサーチは、有意義な結果が返るまで検索クエリーを手動で実行するのが最も効果的かもしれません。

      初回はタイムフレームを長く設定してサーチを実行することでベースラインを作成し、2回目からはクエリーの実行頻度を増やして(1時間ごとなど)、ベースラインを継続的に最新の状態にします。この方法を使えば、環境内の最新のアクティビティがベースラインに常に反映されます。

      注:このサーチを初めて実行するときは、ルックアップ「ingess_src_dest_country_tracker.csv」が設定されていないとエラーになります。エラーを避けるには、この名前の空のルックアップを作成するか、初回はこの行をコメントアウトしてサーチを実行してください。

      | lookup ingess_src_dest_country_tracker.csv dest_ip src_ip Country OUTPUT earliest AS previous_earliest latest AS previous_latest
      
      
      | tstats summariesonly=false allow_old_summaries=true 
          earliest(_time) as earliest 
          latest(_time) as latest 
          values(All_Traffic.action) as action 
          values(All_Traffic.app) as app 
          values(All_Traffic.dest_ip) as dest_ip 
          values(All_Traffic.dest_port) as dest_port 
          values(sourcetype) as sourcetype count 
          from datamodel=Network_Traffic 
          where 
          (All_Traffic.dest_category="internal" OR All_Traffic.dest_ip=10.0.0.0/8 OR All_Traffic.dest_ip=172.16.0.0/12 OR All_Traffic.dest_ip=192.168.0.0/16 OR All_Traffic.dest_ip=100.64.0.0/10)
          AND (All_Traffic.src_category="external" OR (All_Traffic.src_ip!=10.0.0.0/8 AND All_Traffic.src_ip!=172.16.0.0/12 AND All_Traffic.src_ip!=192.168.0.0/16 AND All_Traffic.src_ip!=100.64.0.0/10))
          by All_Traffic.src_ip All_Traffic.dest_ip 
      | rename "All_Traffic.*" as * 
      | iplocation src_ip
      | lookup ingess_src_dest_country_tracker.csv dest_ip src_ip Country OUTPUT earliest AS previous_earliest latest AS previous_latest 
      | eval earliest=min(earliest, previous_earliest), latest=max(latest, previous_latest) 
      | fields - previous_*
      | appendpipe 
          [
          | fields src_ip dest_ip Country latest earliest
          | stats min(earliest) as earliest max(latest) as latest by src_ip, dest_ip, Country
          | inputlookup append=t ingess_src_dest_country_tracker.csv
          | stats min(earliest) as earliest max(latest) as latest by src_ip, dest_ip, Country 
          | outputlookup ingess_src_dest_country_tracker.csv
          | where a=b
              ] 
      | eventstats max(latest) as maxlatest
      | eval comparisonTime="-1h@h" 
      | eval isOutlier=if(earliest >= relative_time(maxlatest, comparisonTime), 1, 0) 
      | convert timeformat="%Y-%m-%dT%H:%M:%S" ctime(earliest),ctime(latest) ,ctime(maxlatest)
      | where isOutlier=1
      

      まとめ:やはりパッチが重要

      この脆弱性に対応するにはやはりパッチの適応が最善策です。パッチを適用できない場合、次善の策は、攻撃範囲を最小限にとどめるための緩和策を実行することです。SURGeは引き続きこの脆弱性の動向を監視し、最新情報を適宜公開します。また、Splunkの脅威調査チームも、この脅威を検出するためのESCUと自動対応のためのSOARプレイブックの作成に全力で取り組んでおり、できる限り早急にリリースする予定です。


      Splunk SURGeは、お客様が新たな脅威や未知の脅威を迅速に検出、調査、対応できるよう高度な分析とインサイトを提供する専門のセキュリティ調査チームです。SURGeアラートではセキュリティ調査と技法に関するガイダンスを受け取ることができます。ぜひご登録ください。

このブログはこちらの英語ブログの翻訳、藤盛 秀憲によるレビューです。

US | JP | Pentagon | DARPA | Splunk