AWS WAFがJA3フィンガープリントを使えるようになったのでブロックしようとしてハマったこととかを共有してみる

AWS WAFのJA3フィンガープリント対応に合わせて検証したら結構色々大変でした。ハマったところをつらつら共有します。
2023.09.29

こんにちは、臼田です。

みなさん、WAFWAFしてますか?(挨拶

AWS WAFがJA3フィンガープリントを利用してCountやBlockなどができるようになったので試してみたのですが、かなりハマってしまったので、備忘録として共有します。

AWS WAF now supports JA3 Fingerprint Match

ちなみにこのアップデート記事はこちらもあるので合わせてご確認ください。

アップデート概要

JA3は送信元のTLS/SSLネゴシエーションの挙動から、アプリケーションの特徴をフィンガープリントとして捉え識別する仕組みです。

従来、IPアドレスやUser-Agentなどのヘッダー情報で送信元を識別し、必要に応じてこれらをWAFなどでブロックしていましたが、その場合には攻撃者がIPアドレスを変えながら攻撃してきた場合などに対応しきれませんでした。

JA3は送信元IPなどを変えても変わらない、アプリケーションのTLSネゴシエーションのClient Helloに含まれるSSLVersionやCipher Suitesなどの情報をハッシュ化することで識別します。これによりIPアドレスが変わっても同じアプリケーションを利用していればJA3が変わらず、継続してブロックする事が可能です。

実際にはこの情報だけで保護をするのではなく、既存のIPやUser-Agentなどと組み合わせることでより精度高く保護するための要素です。

これまでAWSでは、CloudFrontでJA3フィンガープリントをオリジンヘッダーにつけることができるようになっていました。詳細は下記ブログにあります。

この機能で、Lambda@Edgeや後段の処理でJA3を使った処理が可能でしたが、今回のアップデートにより直接CloudFrontやALBでこれを識別してCountやBlockが可能になりました。

なお、アップデートには下記のようにあり、API GatewayなどにアタッチしたAWS WAFで利用可能であるかは不明です。ユーザーガイドには深く言及されていませんでしたので、使われる場合には動作確認などをお願いします。

AWS WAF が Amazon CloudFront および Application Load Balancer オリジンタイプで利用できるすべての AWS リージョンで利用できます(翻訳)

やってみてハマってみた

ここからは、実際にやってみてハマったことをつらつら書いていきます。どんな感じに使うのか、というところがサクッと知りたい場合には、飛ばすか前述のアップデート記事を参照してください。

ここで話す内容のお品書きはこんな感じ。

  • JA3フィンガープリントをCloudFront-S3構成で取得してみる
  • Lambda@Edgeでログを取得してみる
  • JA3がコロコロ変わる謎を特定してみる
  • ブロックの確認

JA3フィンガープリントをCloudFront-S3構成で取得してみる

まず、WAFを構築してサッと動作を見るだけのつもりだったので、CloudFront - S3構成でいいだろうと考えました。今考えれば、この考えは自分にとって浅はかでした。

とりあえずS3に適当なHTMLファイルを置き、CloudFrontを作成してオリジンにS3を設定、OACを設定することで閲覧できるようにしました。オリジンリクエストの設定でJA3をヘッダーに追加します。

オリジンリクエストポリシーをビヘイビアに追加します。

この段階では、なぜかCloudFrontのログにJA3フィンガープリントくらい出るだろう、という気持ちで下調べなしでログ出力用のS3バケットを設定し、ログを確認しました。

当然ありませんね。CloudFrontのアクセスログについてはこちらのユーザーガイドに詳細があります。ちなみに全然覚えてなかったですが、最近はリアルタイムログなんていうのもあるんですね。

Lambda@Edgeでログを取得してみる

というわけでCloudFrontで記録したJA3フィンガープリントをログに出したいので、Lambda@Edgeを使ってこれをロギングすることにしました。今思い返してみると、本当に行き当たりばったりです。私まともにLambda@Edge触ったこと無いのに。

アーキテクチャにするとこんな感じです。

CloudFrontのEdgeからS3 Bucketに送られるOrigin RequestにJA3フィンガープリントのヘッダーが含まれるので、これを無理やりLambda@Edgeを挟んで出力させようという魂胆です。

バックエンドに普通にALBやEC2を入れていれば直接確認できるログを、こんなに頑張って取りに行かなくてもいいのに…

とりあえず全部出力すればいいと思ったので、Lambdaのブループリントにあったコードを適当に簡略化してeventを直接出力させました。以下のような感じ。ちなみに私はNode.jsは1ミリもわかりません。

export const handler = async (event) => {
const request = event.Records[0].cf.request;
const headers = request.headers;

console.log('%j', event);

return request;
};

はじめにうっかりいつものノリで東京リージョンにLambdaを作ってアタッチできないやってなりました。

つぎに、Lambda@Edgeのアタッチ方法が分からず右往左往しました。CloudFront側から行くとバージョンの払い出しなどが必要になってうまく行かなかったため、関数の概要からトリガーをアタッチでアタッチしました。

そして、Lambda@Edgeのログがいつまで経っても出なかったのですが、ログはエッジの場所に依存して出るとのことで東京リージョンのCloudWatch Logsを見に行って、下記のようにログにフィンガープリントが出ることを確認しました。

JA3がコロコロ変わる謎を特定してみる

さて、いよいよ取得したJA3フィンガープリントを使ってWAFでブロックしてみようと考えたのですが、割とコロコロJA3フィンガープリントが変わることに気が付きました。

なんでじゃあ…

具体的には、ブラウザから連続したリクエストをする分には変わらないのですが、少し時間が空いて新しいTLSネゴシエーションをするたびに変わっているようでした。

ここで私は、JA3の仕様を詳細に調べることにしました。

GitHubにあるJA3のドキュメントによると、フィンガープリントは下記フォーマットに情報を整理してMD5ハッシュを取得しているとのことでした。

SSLVersion,Cipher,SSLExtension,EllipticCurve,EllipticCurvePointFormat

パッと見て、だいたい毎回変わる要素は無いだろうと感じました。接続時間や接続対象などによっても変わらなさそうです。元々の目的的にもそうですし。

というわけで、フィンガープリントとなるmd5ハッシュではなく、直接この値を確認したくなりました。

AWS上で直接この値を取るすべがないか確認しましたが、どうやらAWS Network Firewallのログには出るようでしたが、他は出せなさそうでした。

なので自分の目で確かめるしか無い、というわけでWiresharkでTLSのネゴシエーションを確認してみることにしました。

結構しばらくの間、何度もキャプチャを取りながら差分を取って、どこが違うかなどを目grepしたりしていました。

わからんなーと思ってふと一番下までスクロールしたら、JA3のフィンガープリントとFullstringが算出されていました。なにこれWiresharkネ申じゃん…

というわけで、JA3の元の情報を取得できました。差分がある動きはこんな感じになっていました。

その1

JA3: af6878f71c0c411c9f2f0e3b61539d2c

JA3 Fullstring: 771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,43-5-10-35-16-11-51-27-13-18-17513-0-45-23-65281-21-41,29-23-24,0

その2

JA3: 27dcef225a54ddc0cb5a89ea6c129010

JA3 Fullstring: 771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,0-5-18-11-27-45-35-16-65281-17513-43-13-23-51-10-21-41,29-23-24,0

どちらもGoogle Chromeからのリクエストですが、一部違っています。よく見ると、SSLVersion,Cipher,SSLExtension,EllipticCurve,EllipticCurvePointFormatの内、3番目のSSLExtensionの順番だけが違います。

これによって、おそらくGoogle ChromeがJA3を特定されないためのランダマイズ処理をしているのではないかと仮定しました。ぐぐってみるとFastlyからこんな記事が出ていました。

Chrome の TLS ClientHello のランダム順列機能をご紹介 | Fastly

しかし、Google Chrome ブラウザに最近実装された機能によって、ClientHello メッセージで送信された一連の TLS 拡張の実行順序がランダム化されるため、Chrome ブラウザから新しい接続が行われる度に異なる JA3 フィンガープリントが生成されています。

というわけでビンゴでした。上記記事には他にもいろんなことが書かれていますので、動向を知る上でも一読をおすすめします。

ひとまずJA3がコロコロ変わる謎は理解できました。

ブロックの確認

結構寄り道してしまいましたが、本題はAWS WAFのJA3フィンガープリントを使った機能を試すこと。

しかしJA3がコロコロ変わるのでどうするのか、ということですがそれはもう簡単です。Google Chromeがコロコロ変える要因なので、他のものを使いましょう。

今回は手っ取り早くcurlを利用します。

curl https://xxxxxxxxxxxxxxx.cloudfront.net
index.html

正常なリクエストが返ってきます。(中身のテキストがindex.html)

ログからJA3フィンガープリントを取得して、AWS WAFで設定します。ルールの設定からJA3 fingerprintを選択して固定のフィンガープリントを入力します。

実際に運用する際はリストで管理したくなりそうですが、IPアドレスなどと違ってリストを別で管理することは現状出来ません。ルールの中にORで記述していくことになるでしょう。

もう一度curlでリクエストするとブロックされることが確認できました。

curl https://xxxxxxxxxxxxxxx.cloudfront.net
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<HTML><HEAD><META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=iso-8859-1">
<TITLE>ERROR: The request could not be satisfied</TITLE>
</HEAD><BODY>
<H1>403 ERROR</H1>
<H2>The request could not be satisfied.</H2>
<HR noshade size="1px">
Request blocked.
We can't connect to the server for this app or website at this time. There might be too much traffic or a configuration error. Try again later, or contact the app or website owner.
<br clear="all">
If you provide content to customers through CloudFront, you can find steps to troubleshoot and help prevent this error by reviewing the CloudFront documentation.
<br clear="all">
<HR noshade size="1px">
<PRE>
Generated by cloudfront (CloudFront)
Request ID: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx==
</PRE>
<ADDRESS>
</ADDRESS>
</BODY></HTML>

まとめ

AWS WAFでJA3フィンガープリントを使ったルールが記述できるようになったので試してみました。

思いつきの行き当たりばったりでは大変になるというのを、久しぶりに実感しました。でもまあ、得られるものはいっぱいあったので結果オーライです。

運用を考えると、どうやってJA3の値を取得してWAFのルールに追加・維持するかなど色々課題がありますが、以前よりはまた少し使いやすくなっていると思います。活用していきましょう。