[アップデート]AWS WAFの検査条件にJA3 フィンガープリントが利用できるようになりました

2023.09.28

初めに

昨日のアップデートでAWS WAFのルールの検査条件にJA3 フィンガープリントが利用できるようになりました。
またおそらくこのタイミングで合わせてと思いますがAWS WAFのログのフィールドに新たにja3Fingerprintというパラメータが追加されアクセス毎のJA3 フィンガープリントを確認することも可能となっています。

昨年(2022年)のアップデートでCloudFront側でJA3 フィンガープリントを算出しオリジンへのリクエスト時にHTTPヘッダに付与それを付与する機能が追加されていました。

この時点ではあくまでヘッダ情報としてJA3 フィンガープリントが付与されるというだけであり、ブロック処理等が必要な場合は別途オリジン側やLambda@EdgeやCloudFront Functionsで独自に実装する必要がありました。

今回WAFで取り扱えるようになったことでアクセス制御自体は独自処理の管理不要でWAFに寄せることができ、またCloudFrontがなくALB+WAFを利用しているような環境でも手軽にJA3 フィンガープリントを利用できるようになります。

JA3 フィンガープリントとは

JA3はTLSネゴシエーションのClient Hello上の情報を用いて利用算出する値となります。

算出の大枠の部分についてはJA3のリポジトリのREADMEのHow it worksに記載があるのでこちらをご参照ください。

Client Helloは基本的にはクライアントが同一であれば接続元IP等が変わったりCookieの削除等を行なっても変動せず一定の値となるためアクセス元が変わっても同一の値を示します。
また、サーバからの応答に応じてパラメータが変動するものでもないためアクセス先によって変動するものではないため別のシステムで検出されたフィンガープリントを共有して利用することも可能です。

例えば配布されたマルウェアがウェブクライアントとなってアクセスを行うような場合にそのクライアントが元のマルウェアの複製である(=同一クライアント)となるケースもあるためそういった場合にJA3 フィンガープリントを用いて制御を行うことでBOTアクセスからサイトを保護することができます。

一方でフィンガープリントの算出に利用される情報はあくまで利用可能な暗号化・圧縮方式の情報といったものとなる関係でCookieのように一意のクライアントを特定するようなものではなく、またWebクライアントのライブラリ等の変動によりTLSハンドシェイクが変更されてしまった場合(新しいバージョンのマルウェアの到来等)はこの値は変動しますのでご注意ください。

JA3 フィンガープリントを取得する

いつもの検証用のCloudFront(WAFアタッチ済)+S3の準備があるのでこちらを使います。
先に記載した通りCloudFrontからのオリジンリクエストのHTTPヘッダにもフィンガープリントは含まれますが特にそちらは利用しませんのでALBに割り当てる場合でも同じような方法で取得可能です。

JA3 フィンガープリントはAWS WAFのログのja3Fingerprintというパラメータに格納されるのでこちらから確認します。

{
    "timestamp": 1695874797936,
    "formatVersion": 1,
    "webaclId": "arn:aws:wafv2:us-east-1:xxxxx:global/webacl/waf-xxxx-test/xxxxx",
    "terminatingRuleId": "Default_Action",
    "terminatingRuleType": "REGULAR",
    "action    ": "ALLOW",
    "terminatingRuleMatchDetails": [],
    "httpSourceName": "CF",
    "httpSourceId": "xxxxx",
    "ruleGroupList": [],
    "rateBasedRuleList": [],
    "nonTerminatingMatchingRules": [],
    "requestHeadersInserted": null,
    "responseCodeSent": null,
    "httpReq    uest": {
        "clientIp": "xxxxx",
        "country": "JP",
        "headers": [
            {
                "name": "host",
                "value": "xxxxx.cloudfront.net"
            },
            {
                "name": "sec-ch-ua",
                "value": "\"Google Chrome\";v=\"117\", \"Not;A=Brand\";v=\"8\", \"Chromium\";v=\"117\""
            },
            {
                "name": "sec-ch-ua-mobile",
                "value": "?0"
            },
            {
                "name": "user-agent",
                "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36"
            },
            {
                "name": "sec-ch-ua-platform",
                "value": "\"macOS\""
            },
            {
                "name": "accept",
                "value": "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8"
            },
            {
                "name": "sec-fetch-site",
                "value": "same-origin"
            },
            {
                "name": "sec-fetch-mode",
                "value": "no-cors"
            },
            {
                "name": "sec-fetch-dest",
                "value": "image"
            },
            {
                "name": "referer",
                "value": "https://xxxxx.cloudfront.net/"
            },
            {
                "name": "accept-encoding",
                "value": "gzip, deflate, br"
            },
            {
                "name": "accept-language",
                "value": "ja,en-US;q=0.9,en;q=0.8"
            }
        ],
        "uri": "/favicon.ico",
        "args": "",
        "httpVersion": "HTTP/2.0",
        "    httpMethod": "GET",
        "requestId": "xxxxx=="
    },
    "ja3Fingerprint": "19577fceb7279aaff9b71c6b9048dcc8"
}

JA 3フィンガープリントを指定するルールを作成する

独自ルールを作成の際にInspectの選択肢にJA3 fingerprintが追加されているので先ほどのアクセスをブロックさせるためにString to Matchja3Fingerprintの値を設定します。

現時点では通常ルールのみの対応でレートベースルールのパラメータ指定にはまだ対応していません。

JA3 fingerprint利用時の独自項目としてFallback for missing JA3 fingerpintというものがありもしフィンガープリントが取得できない場合の挙動をどうするかの指定が必須となります。

現時点ではビジュアルエディタにバグ有り(?)

この解決にハマり地味に時間を使いました。

ビジュアルエディタで保存を行った場合上記の設定で特にエラーが表示されないけど先に進めない状態となっており、JSONエディタに切り替えたところビジュアルエディタで作ったにも関わらずどこかに利用できないパラメータが含まれるようなエラーが出力されていました。

CloudFormationでルールに上記と同等の値を設定しデプロイしたところJA3のパラメータと思われるJA3Hashが存在しないようです。

告知あったのにもしかして実装が追いついてない??と思ったのですがAPI側のアップデートを見るとJA3HashではなくJA3Fingerprintという値が指定されておりビジュアルエディタで設定後JSONエディタでこの値を書き換えることで保存可能でした。

そのうち修正されるかなとは思いますが取り急ぎ試したい方でビジュアルエディタを利用する場合は最後にJSONエディタでJA3Hashの指定をJA3Fingerprintに書き換えるようにしてみてください。

{
  "Name": "ja3block",
  "Priority": 0,
  "Action": {
    "Block": {}
  },
  "VisibilityConfig": {
    "SampledRequestsEnabled": true,
    "CloudWatchMetricsEnabled": true,
    "MetricName": "ja3block"
  },
  "Statement": {
    "ByteMatchStatement": {
      "FieldToMatch": {
-        "JA3Hash": {
+        "JA3Fingerprint": {
          "FallbackBehavior": "MATCH"
        }
      },
      "PositionalConstraint": "EXACTLY",
      "SearchString": "aaa",
      "TextTransformations": [
        {
          "Type": "NONE",
          "Priority": 0
        }
      ]
    }
  }
}

上記の編集を行って保存すると画面上特に問題なくJA3 Fingerprintを利用したルールとして表示されるようです。

ちなみにビジュアルエディタで表示可能なルールとなっているせいか(AND/ORがネストしていない)、編集を開くたびにJA3Fingerprintの値がJA3Hashに書き換えられてJSONエディタで書き換えが必要で手間です。

アクセスして確認する

実際に上記のルールを作成を含むWebACLを割り当てたCloudFrontに対してアクセスを行います。

先ほどChromeでアクセスした値を利用しましたがどうもChromeはこの値が変動するようで(CloudFrontの検証記事をみましたがその場合もこれがあったようです)制御が安定しなかったのでcurlで指定した値に書き換えて実行しています。

# 設定したフィンガープリントなのでアクセスが拒否されている
% curl ifconfig.io --ipv4
220.xxx.xxx.xxx
% curl https://xxxx.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>
...
# Proxyを通してIPを変えているが拒否される
 % curl ifconfig.io --ipv4
104.xx.xxx.xxx
% curl https://xxxx.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>
...

これはあくまでアクセス元のクライアントに準拠するため、同一マシンであってもcurl以外のクライアント(ブラウザ等)でアクセスした場合は拒否されずアクセスが可能です。

フィンガープリントが生成できないケースとは

Fallback for missing JA3 fingerpintが発生するケースとして障害やバグ等様々なケースが考えられますが、非常に明確的でわかりやすい例としてHTTP(HTTPSではない)アクセスが挙げられます。

JA3 fingerprintはClient Helloのパラメータから生成するという関係上TLSを利用しないアクセスにおいては生成されません。

先ほどの設定のFallback for missing JA3 fingerpintの設定をMatchにして(fingerprintが生成されない場合に条件に一致したとみなす)としてアクセスしてみます。

# curlでアクセスできるように比較するfingerprintの値は変更済
# httpsの場合はfingerprintが生成され、かつ一致していないのでアクセスできる
 % curl https://xxxx.cloudfront.net/
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
<body>
    <div>
        <form action="login" method="get">
            <p><label>ユーザ名</label> <input id="username" name="password" /></p>
            <p><label>パスワード</label> <input type="password" id="password" name="password" /></p>
            <button type="submit">送信</button>
        </form>
    </div>
</body>
# httpの場合はfingerprintが生成できない=条件にマッチ=BLOCKルールなので拒否される
% curl http://xxxxx.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>

ログを確認してみると上記のhttpのアクセスはja3Fingerprintというパラメータ自体が存在しないことが確認できます。

{
    "timestamp": 1695893216198,
    "formatVersion": 1,
    "webaclId": "arn:aws:wafv2:us-east-1:xxxxx:global/webacl/ja3test/cxxxxx",
    "terminatingRuleId": "ja3block",
    "terminatingRuleType": "REGULAR",
    "action": "BLOCK",
    "terminatingRuleMatchDetails": [],
    "httpSourceName": "CF",
    "httpSourceId": "xxxxx",
    "ruleGroupList": [],
    "rateBasedRuleList": [],
    "nonTerminatingMatchingRules": [],
    "requestHeadersInserted": null,
    "responseCodeSent": null,
    "httpRequest": {
        "clientIp": "xxx",
        "country": "JP",
        "headers": [
            {
                "name": "Accept",
                "value": "*/*"
            },
            {
                "name": "User-Agent",
                "value": "curl/7.88.1"
            },
            {
                "name": "Host",
                "value": "xxxxx.cloudfront.net"
            }
        ],
        "uri": "/",
        "args": "",
        "httpVersion": "HTTP/1.1",
        "httpMethod": "GET",
        "requestId": "Zot1BTpEICegFZcDTWi0MM1Y-i88N8ot9iA2qDnUfbYoG8RbKv1NHw=="
    }
}

終わりに

WAF上でJA3 フィンガープリントを利用した制御が可能となりマルウェアの可能性のあるクライアントに対してアクションを起こせるようになりました。

一般的なユーザクライアントというよりはマルウェア等機械的に配布された一定のクライアントをブロックするような使い方が主流とは思いますが、うまく算出できるのであれば特定規定を満たすクライアントだけを通すような使い方もできるのでしょうか?
(ただClient Hello等TLSの仕組みの部分やブラウザの仕様等少しディープな部分に触れる必要があり敷居は高そう)

そのため特に言及はないだけで以前紹介したMLルール等BOT判別系の部分では内内的に使われたりしてるのかなと個人的に思いを馳せていたりしています。