Lambda@Edgeで脆弱性対策

# Lambda@Edge で脆弱性対策

## あらすじ

新たなウェブサイトの脆弱性への攻撃手法が出回り始めたけど、すぐには改修できない。
システムの外部で攻撃防御を行うにはWAFを使うことになるけれど、AWS WAFは高い。
（個人のウェブサイト維持費としては。会社ならどうということはないのですが）

特定の条件に合致するリクエストを弾くだけならば Lambda@Edge でも可能では？
ということで試してみた話。
今回はApache log4jの任意のコード実行の脆弱性(CVE-2021-44228)を対象とします。
https://www.jpcert.or.jp/at/2021/at210050.html


## Lambda@Edgeとは

Amazon CloudFront の機能で、CDNエッヂで実行可能なLambda関数。
ウェブサイトへアクセスがあった都度実行させて、その結果を返すことができます。
ということはWAF的な動作も可能なはずです。


## 前提条件

脆弱性対策を行いたいウェブサイトで Amazon CloudFront (CDN) を利用していること。
Amazon CloudFrontのディストリビューション作成についてここでは触れません。
作成されている事を前提とします。


## 手順

### 1. Lambda関数の作成

まずは実行するLambda関数を作成しますが、最初に注意。
Lambda@Edge用のLambda関数は必ず `us-east-1` リージョンで作成する必要があります。
これはCloudFrontのリージョンが常に同リージョンだからです。
右上のリージョン表示が `バージニア北部 (us-east-1)` であることを確認して進めます。
AWSマネジメントコンソールから AWS Lambda の `関数の作成` を開きます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96018/47d90279-f2e1-1eda-15e6-218ef5c89e5a.png)

ソースコードを作成します。
内容は CloudFront から Lambda@Edge に送られてくるデータから User-Agent を抽出し、log4j脆弱性に対する攻撃パターンである `jndi:` という文字列を検出したら404エラーを返し、それ以外は通常通り処理する(requestを返す)という簡単なものです。
Lambda@Edgeはウェブサイト全てのアクセスに対して毎回呼び出されますから、極力軽く作りましょう。

```
def lambda_handler(event, context):
    request = event['Records'][0]['cf']['request']
    headers = request['headers']

    error_response = {
        'status': '404',
        'statusDescription': 'Not Found',
        'body': 'Not Found'
    }

    try:
        useragent = headers['user-agent'][0]['value']
        return error_response if ('jndi:' in useragent) else request
    except Exception:
        return error_response
```

このコードを画面内のコードソース /lambda_function.py 部分に貼り付けます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96018/afdfabb3-4ec8-d884-c290-1ed331b74fdf.png)

貼り付けたら `Deploy` ボタンをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96018/b0b54d53-255b-957c-2a55-692815aa5a60.png)


### 2. Lambda関数のテスト

`Deploy` ボタン隣の `Test` をクリックしテストイベントを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96018/349c28d8-e9fc-7953-76f5-0cb22afa712b.png)
イベント名は適当に名前を付け、
テンプレートから `cloudfront-ab-test` を選択します。
画面下の `保存` をクリックすると保存され画面が閉じます。

再び `テスト` をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96018/83b6b17f-a8ce-0454-7668-167058010271.png)

テストに成功したことがわかります。

### 3. IAMロールの編集

Lambda関数をLambda@Edgeから呼び出せるよう、IAMロールを編集します。
`設定` - `アクセス権限` とクリックすると `実行ロール` が表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96018/78b260bf-32d9-4a63-05d7-87d13d7f7b1d.png)
表示された画面から `ロール名` 欄の文字列をクリックすると、別画面でIAMロールの画面が開きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96018/afaa756c-8086-f08c-ff6d-84ba4a2af2e1.png)

`信頼関係` - `信頼ポリシーを編集` をクリックすると編集画面になります。
ここでポリシーを編集します。
具体的には `Principal` に `edgelambda.amazonaws.com` を追加します。
下記コードを貼り付けます。

```
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": [
                    "lambda.amazonaws.com",
                    "edgelambda.amazonaws.com"
                ]
            },
            "Action": "sts:AssumeRole"
        }
    ]
}
```

`ポリシーを更新` をクリックすれば更新完了です。
IAMロールの画面は閉じて構いません。

ここではまった:
複数のサービスを登録する場合、サービスを `,` で追加するだけでなく、それらを `[ ]` で囲む必要があります。
編集前のIAMロールには一つのサービス(lambda.amazonaws.com)が登録されているのみですから `[ ]` がありません。よってこれも自分で追記する必要があります。
jsonの基本らしいですが、古い人間はこれを理解出来ず半日が過ぎました...


### 4. Lambda@Edgeへのデプロイ

AWS Lambda の画面に戻ります。
`アクション` をクリックし、出現した選択肢から `Lambda@Edge へのデプロイ` をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96018/0ccb9570-61b6-4872-86bf-f3a3b314c19d.png)

Lambda@Edge へのデプロイ 画面が出現します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96018/057029f4-7442-3432-8053-ae9c20e5af93.png)

- `Distribution` は デプロイする CloudFrontディストリビューションを選択
- `Cache behavior` は `*` を入力
- `CloudFront event` は `Viewer request` を選択
- `Confirm deploy to Lambda@Edge` のチェックボックスをチェックする

そして `デプロイ` をクリックすると画面遷移します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96018/c55317f4-7cac-9275-4781-d3d65492a5d5.png)

このような画面が出現すればデプロイ完了です。
Amazon CloudFront は世界各地にデプロイされますから、数分かかります。
しばらく待ってから次に進みます。


### 5. 動作確認

それでは動作確認しましょう。今回はcurlで行います。

まずは通常の接続を試みます。

```
$ curl http://d2qmc8j12nwv5z.cloudfront.net/
<HTML><BODY><H1>TEST OK</H1></BODY></HTML>
$
```

問題無く接続されましたね。
次に、log4j攻撃を装い、User-Agentに細工をして接続を試みます。

```
$ curl --user-agent jndi:ldap://example.com/hoge http://d2qmc8j12nwv5z.cloudfront.net/
Not Found
$
```

404エラーで Not Found と返されました。
成功です。


# 注意点

## 更新時は再デプロイが必要

Lambda@EdgeではLambda関数の $latest は利用できません。
Lambda関数を更新してもLambda@Edgeへいっこうに反映されずに悩みました。
Lambda関数を更新したら、「毎回」手順4の `Lambda@Edgeへのデプロイ` を行う必要があります。

## 同時アクセスはLambda関数の同時実行数上限まで

Lambda関数の同時実行数は既定では1000です。
つまり同時に1000を超えるアクセスがされるとLambda関数が実行できず、CloudFrontがエラーを返してしまいます。
上限緩和申請を行っても一般にその上限は3000です。高トラヒックのウェブサイトには向きません。
まぁ、通常そのようなウェブサイトはきちんとWAFを導入するでしょうからここでは気にしないことにします。


# まとめ

Lambda@Edgeを利用してお安くお手軽に脆弱性対策ができました。
しかしながら、もちろんこれは本来の使い方ではありません。
ご利用は計画的に。