[アップデート] AWS WAF の Account Takeover Protection (ATP) 機能でオリジンレスポンスから不正なログイン試行を検知出来るようになりました

2023.02.18

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

いわさです。

Account Takeover Protection (ATP) は不正行為につながる可能性のあるアカウントの乗っ取りを防ぐために AWS WAF のマネージドルールを通して Web フォームへセキュリティレイヤーを追加することが出来るというものです。

AWS WAF の ATP は、ちょうど 1 年前に登場した機能です。
当時は Web システムへのログインに使用された認証情報が、漏洩された認証情報を使っていないかリクエストを検査出来るというものでした。

今回こちらにアップデートがありました。
オリジンからのレスポンスを検査して一定期間内に複数回定義されたルールに該当した場合は、そのアクセス元が不正アクセスをしていると判断出来るようになりました。

例えば、悪意のあるユーザーが Web システムのログインフォームに対して不正アクセスのために手当り次第に認証リクエストを送信するケースがあったとします。
オリジンはその不正なリクエストを処理し、アクセス不可のレスポンスを行います。
今回 AWS WAF の ATP でこの戻りのレスポンスをカスタムしたルールに基づいて検査し、一定期間内に該当するルールが複数回記録された場合は対象のセッションや IP アドレスを攻撃者としてラベリング出来るようになりました。

アップデートニュースやドキュメントを読むだけだとわかるような、わからないような。
本日は実際に設定方法などを確認してみましたので紹介します。

設定しましょう

後ほど触れますが、前提として本日時点ではこの機能を使った WebACL は CloudFront でのみ利用が出来ます。
そこで今回は CloudFront と API Gateway のモック機能を使って簡易的な API を用意し、CloudFront へ AWS WAF ルールを設定してみます。

最終形としてはオリジンが返す HTTP ステータスに応じて ATP を設定したいと思います。

API Gateway のモックとマッピングテンプレート

API Gateway で統合リクエストにモックを使うことでサッと API を用意します。
画面は GET メソッドですが、認証フォームでリクエストの検査もちょっと確認したかったので POST メソッドも作成しています。

今回はリクエスト時のhogeヘッダーの値がerrorだった場合に API Gateway が HTTP ステータス 500 を返すようにしてみます。
マッピングテンプレートを使うとこのように API Gateway 単体で簡易的な動的処理も実行出来ます。
ただし、マッピングテンプレートの記述は Apache Velocity Template Languate (VTL) です。

{"statusCode": 200}
#if($input.params('hoge') == 'error')
#set($context.responseOverride.status = 500)
#end

試しに API Gateway 単体の状態で cURL か何かでリクエストを送信してみましょう。

% curl -i -H "hoge:success" https://vrntwdilik.execute-api.ap-northeast-1.amazonaws.com/hoge
HTTP/2 200 
date: Sat, 18 Feb 2023 04:16:39 GMT
content-type: application/json
content-length: 0
x-amzn-requestid: ad554474-a60d-4c37-ab57-f7d88728971e
x-amz-apigw-id: AhGeQGD9tjMFarg=

% curl -i -H "hoge:error" https://vrntwdilik.execute-api.ap-northeast-1.amazonaws.com/hoge  
HTTP/2 500 
date: Sat, 18 Feb 2023 04:16:43 GMT
content-type: application/json
content-length: 0
x-amzn-requestid: 32fe8800-af1e-4169-988e-33b762b5c6d8
x-amz-apigw-id: AhGe6HrENjMF0ow=

良さげです。

CloudFront

CloudFront でディストリビューションを作成します。
オリジンドメインには API Gateway の適当にデプロイしたステージのエンドポイントを指定します。

今回の検証内容と、API Gateway のレスポンスをリクエストヘッダーで切り替えているので、キャッシュポリシーとオリジンリクエストポリシーだけ気をつけます。

キャッシュは今回無視してオリジンレスポンスを見たいところ、なのでキャッシュは無効化させます。ここではマネージドポリシーの CachingDisabled を選択しています。

また、オリジンリクエストポリシーはデフォルトの選択なしだと最低限のリクエストヘッダーしか送信されません。
カスタムヘッダーhogeを送信したいので今回はすべてのヘッダーを送信するようなポリシーを選択しましょう。
ただし、Host ヘッダーを送信すると API Gateway の SSL 周りでエラーが起きそうな気がします。AllViewerExceptHostHeader というちょうど良いマネージドポリシーがあったのでこれを使いました。

今回 POST メソッドを使おうとしているので HTTP メソッドは POST も許可します。

AWS WAF を設定する前ですが、ここでまた API の動作を確認しておきます。
今回は CloudFront 経由です。

% curl -i -H "hoge:success" https://d18blmh63v079g.cloudfront.net/
HTTP/2 200 
content-type: application/json
content-length: 0
date: Sat, 18 Feb 2023 04:34:25 GMT
x-amzn-requestid: eb0ada54-95e4-4402-a3b0-33cc76d16d51
x-amz-apigw-id: AhJE1HxItjMFUHQ=
x-cache: Miss from cloudfront
via: 1.1 360cdb248de2ad362090d67754f85dba.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT57-P2
x-amz-cf-id: N1hF3xiH9CsSGoKqdIrsNcoid0hh49KQmEN6aRkIJog6J73bEzLOeQ==

% curl -i -H "hoge:error" https://d18blmh63v079g.cloudfront.net/  
HTTP/2 500 
content-type: application/json
content-length: 0
date: Sat, 18 Feb 2023 04:34:27 GMT
x-amzn-requestid: ac7deac8-a5c7-4891-9e62-ffac6ccad36a
x-amz-apigw-id: AhJFKFlBtjMFf8g=
x-cache: Error from cloudfront
via: 1.1 ce476228a749107bee7cc7f6dbd69bec.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT57-P2
x-amz-cf-id: n08FmZQD_aN6VkQPUXNrPFEj2EZhEemXKz2l8EUFKSXaew31OuDotQ==

良さげです。

AWS WAF を作成

最後に AWS WAF を作成します。
最後にというか、よく考えたらここがメインですかね。

本日時点で ATP のオリジンレスポンス検査は CloudFront のみで利用可能ですので、CloudFront をリソースタイプに選択します。

続いて ATP の構成を行います。ATP はルールの追加からマネージドルールグループとして選択します。

そうすると AWS managed rule groups 内に Account takeover provention がありますので、まずは Add to web ACL を ON にしましょう。
ちなみに ATP は画面にも記述がありますが別途追加料金が発生しますのでよく確認頂いた上でご利用ください。
prorated hourly の記述があるのは個人的に親切だなと思いました。

有効化するといきなりエラーメッセージが表示されます。有効化しただけじゃなくてルールの編集から詳細設定必要だからね、ということです。
Edit ボタンから編集画面へ遷移します。

画面の上部にブロックやらの設定が最初にあるのですがまずは無視して基本設定から行いました。
Rule group configuration を見ると、ATP が対象とするログインフォームを指すパスと、リクエスト検査の設定とレスポンス検査の設定があります。
リクエスト検査の設定は 1 年前に ATP が登場したときに使えるようになったものです。
今回はレスポンス検査の設定を行いたかったのですが、どうやらリクエスト検査の設定も必要のようです。ユーザー名フィールドとパスワードフィールドは入力必須項目でした。

レスポンス検査についてはコンポーネントタイプを選択することが出来ます。検査する対象ですね。
そして、検査対象ごとに成功条件と失敗条件を設定します。

条件値は改行で複数固定で定義します。
今回は成功に200、失敗に500を指定しました。

このルールでは一定期間の失敗率をベースに動作し、ここで入力された条件に一致する場合のみ成功/失敗の判定が行われます。
例えば成功条件値の入力数が不十分で失敗条件値がしっかり入力されている場合は期待よりも失敗率が高くなり正常な判定ができなくなる可能性があります。
出来るだけ成功と失敗の条件値を網羅させましょう。

最後に検知後の挙動を設定します。
オリジンレスポンス検査の仕組みですが、一定時間内の失敗率が高すぎるクライアントを検出しラベリングするという仕組みになっています。
ドキュメントによると、失敗率のカウントや検出は非同期で行われるためアプリケーションパフォーマンスへの影響は無いそうです。

ラベルに従ってカスタムルールでブロックなどしても良いですし、先程スキップした ATP 設定画面上段でもこのルールグループで各ラベルに関するアクションを上書きすることも出来ます。

対象のラベルは以下の 2 つです。

  • VolumetricIpFailedLoginResponseHigh
    • awswaf:managed:aws:atp:aggregate:volumetric:ip:failed_login_response:high
  • VolumetricSessionFailedLoginResponseHigh
    • awswaf:managed:aws:atp:aggregate:volumetric:session:failed_login_response:high

動作確認

テスト方法は以下でも紹介されています。

失敗基準をテストするには、単一のクライアント IP アドレスから 10 分以内に少なくとも 10 回ログイン試行し失敗させるとのこと。
試してみます。

# 成功
% curl -X POST -H "Content-Type:application/json" -H "hoge:success" -d '{"username":"iwasa", "password":"password"}' "https://d18blmh63v079g.cloudfront.net/"
hoge api body

# 失敗
% curl -X POST -H "Content-Type:application/json" -H "hoge:error" -d '{"username":"iwasa", "password":"password"}' "https://d18blmh63v079g.cloudfront.net/"
hoge api body
% curl -X POST -H "Content-Type:application/json" -H "hoge:error" -d '{"username":"iwasa", "password":"password"}' "https://d18blmh63v079g.cloudfront.net/"
hoge api body
% curl -X POST -H "Content-Type:application/json" -H "hoge:error" -d '{"username":"iwasa", "password":"password"}' "https://d18blmh63v079g.cloudfront.net/"
hoge api body
% curl -X POST -H "Content-Type:application/json" -H "hoge:error" -d '{"username":"iwasa", "password":"password"}' "https://d18blmh63v079g.cloudfront.net/"
hoge api body
% curl -X POST -H "Content-Type:application/json" -H "hoge:error" -d '{"username":"iwasa", "password":"password"}' "https://d18blmh63v079g.cloudfront.net/"
hoge api body
% curl -X POST -H "Content-Type:application/json" -H "hoge:error" -d '{"username":"iwasa", "password":"password"}' "https://d18blmh63v079g.cloudfront.net/"
hoge api body
% curl -X POST -H "Content-Type:application/json" -H "hoge:error" -d '{"username":"iwasa", "password":"password"}' "https://d18blmh63v079g.cloudfront.net/"
hoge api body
% curl -X POST -H "Content-Type:application/json" -H "hoge:error" -d '{"username":"iwasa", "password":"password"}' "https://d18blmh63v079g.cloudfront.net/"
hoge api body
% curl -X POST -H "Content-Type:application/json" -H "hoge:error" -d '{"username":"iwasa", "password":"password"}' "https://d18blmh63v079g.cloudfront.net/"
hoge api body
% curl -X POST -H "Content-Type:application/json" -H "hoge:error" -d '{"username":"iwasa", "password":"password"}' "https://d18blmh63v079g.cloudfront.net/"
hoge api body
% curl -X POST -H "Content-Type:application/json" -H "hoge:error" -d '{"username":"iwasa", "password":"password"}' "https://d18blmh63v079g.cloudfront.net/"
hoge api body
% curl -X POST -H "Content-Type:application/json" -H "hoge:error" -d '{"username":"iwasa", "password":"password"}' "https://d18blmh63v079g.cloudfront.net/"
hoge api body
% curl -X POST -H "Content-Type:application/json" -H "hoge:error" -d '{"username":"iwasa", "password":"password"}' "https://d18blmh63v079g.cloudfront.net/"
hoge api body
% curl -X POST -H "Content-Type:application/json" -H "hoge:error" -d '{"username":"iwasa", "password":"password"}' "https://d18blmh63v079g.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: JRHZKOetBMP_yZqcBs42R_wnClsOY2ywMbKm7cCoNMIA1dEgjcAjJA==
</PRE>
<ADDRESS>
</ADDRESS>
</BODY></HTML>

13 回目の試行から、WAF によって 403 でブロックされるようになりました。
対象 WebACL の CloudWatch Logs も確認してみます。

{
    "timestamp": 1676701851391,
    "formatVersion": 1,
    "webaclId": "arn:aws:wafv2:us-east-1:123456789012:global/webacl/hoge0218-cloudfront-wafatp/017790f1-b4de-40e7-9607-e67f09c25b3e",
    "terminatingRuleId": "AWS-AWSManagedRulesATPRuleSet",
    "terminatingRuleType": "MANAGED_RULE_GROUP",
    "action": "BLOCK",
    "terminatingRuleMatchDetails": [],
:
    "labels": [
        {
            "name": "awswaf:managed:token:absent"
        },
        {
            "name": "awswaf:managed:aws:atp:aggregate:volumetric:ip:low"
        },
        {
            "name": "awswaf:managed:aws:atp:aggregate:volumetric:ip:failed_login_response:high"
        },
        {
            "name": "awswaf:managed:aws:atp:aggregate:volumetric:ip:medium"
        }
    ]
}

期待どおりラベリングされていますね!

なお不正なラベリングがされると、一定時間は正常な挙動が期待できそうなリクエストでもオリジンまで到達しなくなりました。
一定期間内の失敗率が高いので不正アクセスを行っているクライアントとして処理されている形ですね。

以下は先程まで 200 ステータスが取得出来ていたリクエストです。

% curl -X POST -H "Content-Type:application/json" -H "hoge:success" -d '{"username":"iwasa", "password":"password"}' "https://d18blmh63v079g.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: nHT-vAIxlYjqM8ahbcTIKE-9f4YQnB5V7POG04dJEi3eEXY7HU1YVA==
</PRE>
<ADDRESS>
</ADDRESS>
</BODY></HTML>

本日時点では CloudFront 以外では利用不可

何度か話に出てきていますが、本日時点では CloudFront でのみ利用が可能です。
試しに東京リージョンで WebACL を作成してみると以下のようにリクエスト検査のみが可能な状態でした。

ただし What's new によると CloudFront 以外のサポートも対応の予定はあるそうなので楽しみに待っていましょう。

さいごに

本日は AWS WAF の Account Takeover Protection (ATP) 機能でオリジンレスポンスから不正なログイン試行を検知出来るようになったので試してみました。

はじめ、オリジンレスポンスの検査ってどういうコト?と思ったのですが非同期で失敗率を計測してラベリングしてくれているということで理解が出来ました。
失敗率計測のためのカウント数や計測期間、あとはどの程度アクセスできなくなるかなどがカスタマイズ出来るようになるとかなり需要あるのではないでしょうかこの機能。自前で作ってるシステムいくつもある気がします。

また、今回気がついたのですが、ATP はリリース時は東京リージョンに対応していなかったのですが、いつのまにか使えるようになっていますね。CloudFront 以外でもリクエスト検査だけは使えるので、是非使ってみてください。