AWS WAFが攻撃を検知したあとの反応を整理する

AWS WAFを使用すれば攻撃者からアプリケーションを保護することができます。 今回は実際に攻撃された時に何が起きるのかを整理していきます。
2021.07.02

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

AWS WAF

AWS WAFはAWSが提供するファイアウォールサービスのひとつでWebアプリケーションの保護を目的としています。 HTTPの通信の内容まで確認することでSQLインジェクションやXSSなどを検知・遮断することが可能です。 ルールを設定することで様々な攻撃に対応することができます。
また、マネージドルールと呼ばれるセキュリティベンダーが提供する定義済みのルールを利用することで素早く導入することができます。
AWS WAFについて詳しく知りたい人には以下の記事がおすすめです。

設定

今回は簡略化のためにSQLインジェクションを検知し、通信をブロックしてみましょう。 API Gateway + Lambdaで構築したREST APIをWAFで保護した状態で悪意のあるリクエストを送ってみましょう。

以下は参考にした記事です。

REST APIのデプロイ

今回Lambdaで実行するコードは以下のものです。

handler.py

import json

def injection(event, context):
    print(json.dumps(event))
    body = {
        "response": "Success"
    }

    response = {"statusCode": 200, "body": json.dumps(body)}

    return response

Serverless Frameworkを使用してこれをデプロイしましょう。 今回はPOSTメソッドを攻撃対象にします。

serverless.yml

service: injection

frameworkVersion: '2'


provider:
  name: aws
  runtime: python3.8
  lambdaHashingVersion: '20201221'

functions:
  injection:
    handler: handler.injection
    events:
      - http:
          path: /
          method: post

正しくデプロイできたか試してみましょう。

REST APIを実行してみる

$ curl -s \
    -X POST \
    -H "Content-Type: application/json" \
    -d '{"User": "Foo", "Pass": "secret"}' \
    ${ENDPOINT}
{"response": "Success"}

AWS WAFを設定する

以下のように設定します。

今回はAWSが用意したSQL用のルールを使用しました。

まずは普通にリクエストを送ってみましょう。

通常のリクエスト

$ curl -s \
    -X POST \
    -H "Content-Type: application/json" \
    -d '{"User": "Foo", "Pass": "secret"}' \
    ${ENDPOINT}
{"response": "Success"}

次にSQLインジェクションを狙ったリクエストを送ってみましょう。 4行目のUser内の' OR 'A' = 'AがSQLインジェクションを狙ったものです。

悪意のあるリクエスト

$ curl -s \
    -X POST \
    -H "Content-Type: application/json" \
    -d "{\"User\": \"' OR 'A' = 'A\", \"Pass\": \"secret\"}" \
    ${ENDPOINT}
{
  "message": "Forbidden"
}

それらしいレスポンスが帰ってきました。 JSONのスキーマも違います。(responseフィールドがありません)
以下で詳しく見てきましょう。

攻撃を検知すると何が起きるのか

ここでいう攻撃の検知とはWAFのWeb ACLにて定義されたルールにマッチするリクエストが送られてきたということです。
今回はSQLインジェクションのパターンに一致するデータがリクエストのJSONに含まれていました。

以下ではクライアント側とサービス提供者側でそれぞれ何が起きるのか確認していきます。

クライアント側

先ほどと同様のリクエストを送信し、一緒にレスポンスヘッダも表示してみます。

悪意のあるリクエスト

$ curl -s \
    -X POST \
    -H "Content-Type: application/json" \
    -d "{\"User\": \"' OR 'A' = 'A\", \"Pass\": \"secret\"}" \
    --dump-header - \
    ${ENDPOINT}
HTTP/2 403
content-type: application/json
content-length: 23
date: XXXXX
x-amzn-requestid: XXXXXXX
x-amzn-errortype: ForbiddenException
x-amz-apigw-id: XXXXXXX
x-cache: Error from cloudfront
via: 1.1 XXXXXX.cloudfront.net (CloudFront)
x-amz-cf-pop: NRTXXXXX
x-amz-cf-id: XXXXXX

{"message":"Forbidden"}

HTTPのステータスコードは403(Forbidden)です。 またx-amzn-errortypeForbiddenExceptionとなっています。 これはWAFによってブロックされた結果です。

レスポンスのスキーマも変化しています。 これは今回利用しているルールAWS-AWSManagedRulesSQLiRuleSetによって設定されているレスポンスです。 カスタムレスポンスを設定することもできますが条件があります。 今回の用途だとその条件を満たさないので、レスポンスを変えたい場合には工夫が必要です。

サービス提供者側

今回はマネージドコンソールから確認していきます。 見やすいように以下のスクリプトで不正なリクエストを予め行っておきました。

まずはOverviewの画面から見ていきます。

CloudWatchに記録されたWAFのメトリクスとSampled Requestsの二つが表示されています。

CloudWatch

AWS WAFではリクエストとそのマッチ結果をCloudWatchのメトリクスとして利用できます。 CloudWatch側で確認すると以下の6つのメトリクスが利用できるようです。


今回はBlockedだけですが、メトリクス名にBlocked,Allowed,Countedの三つがあります。

  • Allowed: 許可されたリクエスト
  • Blocked: ブロックされたリクエスト
  • Counted: カウントモードで検知されたリクエスト


大まかに分けるとRuleには今回利用しているSQL用のルールとALLの二種類があります。 これはルールが増えるごとに増えていきます。

今回は1つしかルールがないのでALLとSQL用のルールのメトリクスは一致していますが、 ALLでは全てのルールの合計値がメトリクスとして利用できます。
ちなみにCloudWatchでのモニタリングが可能なのでアラームの設定も可能です。

SampledRequest

AWS WAFではリクエストの内容とそれに対する処理が記録されています。 過去3時間分についてはこのSampledRequestから確認できます。 リクエストの詳細を確認してみると以下のようになっています。

どのルールに検知されブロックされたのかが確認できました。 意図した通りにルールが設定するか確認するのに便利です。 ただ、リクエストのどの部分が問題となったのかや、bodyの情報は見れない点は注意が必要です。

ロギング

リクエストのログはKinesis Data Firehose経由でS3などに保存することができます。

このときKinesis Date Firehoseの名前はaws-waf-logs-から始まる必要があります。 Athenaなどを使用すればログの分析も可能です。 ログではリクエストのbodyは保存されませんが、どの部分にマッチしたのかを見ることはできます。

以下がログの例です。

リクエストログ

{
  "timestamp": XXXXX,
  "formatVersion": 1,
  "webaclId": "arn:aws:wafv2:XXXXXX",
  "terminatingRuleId": "AWS-AWSManagedRulesSQLiRuleSet",
  "terminatingRuleType": "MANAGED_RULE_GROUP",
  "action": "BLOCK",
  "terminatingRuleMatchDetails": [
    {
      "conditionType": "SQL_INJECTION",
      "location": "BODY",
      "matchedData": [
        "{\"User\": \"",
        "OR",
        "A",
        "=",
        "A\", \"Pass\": \"secret\"}"
      ]
    }
  ],
  "httpSourceName": "APIGW",
  "httpSourceId": "XXXXX:XXXXX:dev",
  "ruleGroupList": [
    {
      "ruleGroupId": "AWS#AWSManagedRulesSQLiRuleSet",
      "terminatingRule": {
        "ruleId": "SQLi_BODY",
        "action": "BLOCK",
        "ruleMatchDetails": null
      },
      "nonTerminatingMatchingRules": [],
      "excludedRules": null
    }
  ],
  "rateBasedRuleList": [],
  "nonTerminatingMatchingRules": [],
  "requestHeadersInserted": null,
  "responseCodeSent": null,
  "httpRequest": {
    "clientIp": "XXX.XXX.XXX.XXX",
    "country": "JP",
    "headers": [
      { "name": "X-Forwarded-For", "value": "XXX.XXX.XXX.XXX" },
      { "name": "X-Forwarded-Proto", "value": "https" },
      { "name": "X-Forwarded-Port", "value": "443" },
      {
        "name": "Host",
        "value": "XXXXX.us-east-1.amazonaws.com"
      },
      {
        "name": "X-Amzn-Trace-Id",
        "value": "XXXXXX"
      },
      { "name": "Content-Length", "value": "43" },
      { "name": "User-Agent", "value": "curl/7.77.0" },
      {
        "name": "X-Amz-Cf-Id",
        "value": "XXXXXXXXX"
      },
      {
        "name": "Via",
        "value": "2.0 XXXXX.cloudfront.net (CloudFront)"
      },
      { "name": "Accept", "value": "*/*" },
      { "name": "content-type", "value": "application/json" },
      { "name": "CloudFront-Is-Mobile-Viewer", "value": "false" },
      { "name": "CloudFront-Is-Tablet-Viewer", "value": "false" },
      { "name": "CloudFront-Is-SmartTV-Viewer", "value": "false" },
      { "name": "CloudFront-Is-Desktop-Viewer", "value": "true" },
      { "name": "CloudFront-Viewer-Country", "value": "JP" },
      { "name": "CloudFront-Forwarded-Proto", "value": "https" }
    ],
    "uri": "/dev/",
    "args": "",
    "httpVersion": "HTTP/1.1",
    "httpMethod": "POST",
    "requestId": "XXXXXXXXX"
  }
}

感想

WAFを使用すればL7でのアプリケーションの保護が可能になることは知っていましたが、検知されたあとにどうなるかは理解が曖昧でした。 今回の記事を通して整理できたと思います。

参考文献