SSTエンジニアブログ

SSTのエンジニアによるWebセキュリティの技術を中心としたエンジニアブログです。

AWS WAFとLambda@edgeで理想のフルログはできるのか

はじめに

はじめまして、事業開発部と研究開発部に属している宇田川です。
AWS関連の新機能や新サービスに都度都度熱狂しておりますが、最近もっとも熱狂したニュースはこちら。

AWS WAF の包括的なログ記録機能が新たに利用可能に
f:id:woodykedner:20180914154735p:plain:w750

早速、調査!

だが、しかし、POSTリクエストのBodyは記録されず…

私が欲しい理想のフルログは、POSTリクエストのBodyのデータも出力されているログ。
で無ければ、AWS WAFで検知したとしても、正常な検知なのか誤検知なのか判断できません。

駄目なのか。。。
諦めかけていたそのとき、希望の光

Lambda@Edgeでrequest bodyが取得可能となりました!!
aws.amazon.com

そこで思い立つ

AWS WAFとLambda@edgeをCloudFrontで併用して、
紐付ければ理想のフルログが取得できるのでは?

調査あるのみ!

仕様を確認しよう!

手を動かす前にドキュメントを見ておく。

まずはAWS WAFのログの構造
https://docs.aws.amazon.com/ja_jp/waf/latest/developerguide/logging.html
長いので抜粋
f:id:woodykedner:20180918173322p:plain

そして、Lambda@edgeで操作できるリクエストの内容
https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/lambda-event-structure.html#lambda-event-structure-request
長いので抜粋
f:id:woodykedner:20180918173339p:plain

どちらのログにも、「requestId」が存在している。
「requestId」で紐付ければ、理想のフルログを取得できそうだ!

調査環境準備

AWS WAFのログをFirehoseでS3に出力する内容はクラスメソッドさんのブログを参考にさせていただきました。
dev.classmethod.jp

Lambda@Edge により HTTP リクエストボディをログに残す方法もクラスメソッドさんのブログを参考にさせていただきました。
dev.classmethod.jp

※クラスメソッドさん、いつも分かりやすいブログをありがとう!!

補足で、 kiesis firefoseとs3はバージニア北部リージョンで設定。

AWS WAFにはOWASP TOP 10対策ルールから、SQLインジェクションのルールのみ付与。
Use AWS WAF to Mitigate OWASP’s Top 10 Web Application Vulnerabilities

まずはActionをCountに設定。

Lambda@edgeの出力ログには、上記のクラスメソッドさんのブログではbodyのみとなっていましたが、bodyと共にrequestIdを出力するように変更しました。

exports.handler = (event, context, callback) => {
    var requestId = event.Records[0].cf.config.requestId;
    var bodyData = new Buffer(event.Records[0].cf.request.body.data, 'base64').toString("utf-8");
    var dataString = {'requestId':requestId,'body':bodyData};
    var AWS = require('aws-sdk');
    var kinesisfh = new AWS.Firehose({region: 'XXXXXXXXXXXXX'});  //uncomment if you want a specific-region of Firehose
    var params = {
        DeliveryStreamName: 'XXXXXXXXXXXXX', /* required */
        Record: { /* required */
        Data: JSON.stringify(dataString)
        }

構成

こんな感じです。 f:id:woodykedner:20180914161505p:plain:w750

検証

clientからSQLインジェクションにヒットするリクエストで対象のCloudFrontにアクセス

OWASPの「Testing for SQL Injection」から取得。
Testing for SQL Injection (OTG-INPVAL-005) - OWASP

SELECT * FROM hogehoge WHERE Username='1' OR '1' = '1' AND Password='1' OR '1' = ‘1’ 

実行コマンドはこちら

# curl -d "username=1‘%20or%20’1‘%20=%20’1&password=1‘%20or%20’1‘%20=%20‘1" -X POST http://CloudfrontのFQDN/

検証結果

上記、コマンド実行。1分ぐらい待つと AWS WAFのログ、Lambda@edge取得ログがS3の対象のバケットに出力された。
早速、ログをダウンロードして、中身を確認する。

  • AWS WAFのログ
{
    ~省略~
        "uri": "//",
        "args": "",
        "httpVersion": "HTTP/1.1",
        "httpMethod": "POST",
        "requestId": "QB-vwZ_FwzcXyzUggYNo3ZazoZGq7QOU5oPq1saJd-wcpz3NrKnosw=="
    }
}
  • Lambda@edge取得ログ
{
    "requestId": "QB-vwZ_FwzcXyzUggYNo3ZazoZGq7QOU5oPq1saJd-wcpz3NrKnosw==",
    "body": "username=1'%20or%20'1'%20=%20'1&password=1'%20or%20'1'%20=%20'1"
}

requestIdが一致!!!!

続、検証

AWS WAFのSQLインジェクションのルールのActionをBlockに設定。

検証結果

AWS WAFのBlockログは取得できた!

だが、しかし...

Lambda@edgeでログが取得できなかった...

AWS WAFでBlockされたリクエストは、Lambda@edgeで処理されない。

こんな感じ。 f:id:woodykedner:20180914162356p:plain:w750

ここまでのまとめ

  • CloudFrontにAWS WAFとLambda@edgeを併用した際、①AWS WAF、②Lambda@edgeという順番で実行される。

  • AWS WAFでCountしたリクエストは、Lambda@edgeでrequest bodyを含むログが取得できる。

  • AWS WAFでblockしたリクエストは、Lambda@edgeでrequest bodyを含むログが取得できない..

やはりBlock設定でrequest bodyが取得できないのは残念です。
AWSさん、機能追加よろしくお願いします。

ただ、Count設定でrequest bodyの取得は朗報ですね。
サイトにWAFを導入する際、最初からBlock設定で入れることは無く、検知のみの設定で様子を見ます。
一般的にエージング期間と言われますが、Lambda@edgeでrequest bodyが取得できるので誤検知した際の内容の確認及び対策が可能です。
また、本運用後でも、誤検知が多いルールに対して、一時的にCount設定に切り替えて、何が誤検知しているかの確認及び対策にも使用できそうです。

理想のフルログを取得

さぁ、もう一頑張り。
requestIdで紐付けられることが分かったので実際に紐づけてみます。
S3のログを紐付けると言ったらアレです。Athenaです。

構成はAthena追加でこんな感じです。 f:id:woodykedner:20180918114552p:plain:w750

しかも、クラスメソッドさんがAthenaでのAWS WAFのフルログの分析まで書いてくれています。
dev.classmethod.jp

ということで、自分が考えることは、

  1. Lambda@edgeで出力したログのテーブル作成
  2. AWS WAFのフルログとLambda@edgeで出力したログのテーブルの結合

です。

AWS WAFのフルログのテーブル作成について補足

と書きつつ、上記のクラスメソッドさんのブログで、AWS WAFのフルログのテーブルは作成できるのですが、「select * from waflogs;」のクエリーを実行すると下記のエラーが出力されました。

HIVE_BAD_DATA: Error parsing field value '1536913320322' for field 0: For input string: "1536913320322"

どうやらtimestampの値がint型で収まらないようです。
そこで下記のtimestampをbigint型でwaflogsテーブルを作り直しました。

CREATE EXTERNAL TABLE IF NOT EXISTS waflogs
(
`timestamp` bigint,
formatVersion int,
webaclId string,
~省略~

これで「select * from waflogs;」のクエリーを実行してもエラーは無くなりました。

f:id:woodykedner:20180914181555p:plain:w750

Lambda@edgeで出力したログのテーブル作成

ログの内容はrequestIdとbodyしか無いので、簡単です。 Athenaで下記のクエリーを実行して、テーブルを作成します。

CREATE EXTERNAL TABLE IF NOT EXISTS bodylogs
(
requestId string,
body string
)
ROW FORMAT SERDE 'org.openx.data.jsonserde.JsonSerDe'
LOCATION 's3://[バケット名]/';

念の為、Athenaでselectクエリを実行して表示されるかを確認。

f:id:woodykedner:20180914173827p:plain:w750

AWS WAFのフルログとLambda@edgeで出力したログのテーブルの結合

特に難しく考えることもなく、こんな感じでクエリーを作成。
全カラムを表示すると長くなるので、bodyとhttprequestのカラムのみをピックアップ。

SELECT body,httprequest
FROM waflogs,
bodylogs
where 
    waflogs.httprequest.requestId  = bodylogs.requestId
;

Athenaでの実行結果はこちら

f:id:woodykedner:20180918201816p:plain:w750

AWS WAFのフルログとLambda@edgeで出力したログをAthenaで結合することができました。
表示カラムをbody,httprequestのカラムのみをピックアップしましたが、「*」にすれば、HTTPリクエストの全容と検知したルールが含まれた理想のフルログの完成です。

最後に

長文ながら、最後まで読んでくれた方ありがとうございます!
これに限らず、AWS WAF周りの調査内容をアップしていきます!