API gateway + lambda + S3でDDoS攻撃を受けて1日あたりで$3000溶かした話


qiita夏祭りに乗り遅れてしまったので一人後夜祭

~2019年某日~
パイセン「それじゃあ、ワイ君は明日からフロントのログデータを飛ばすのにAPI gatewayとlambdaでS3に保存するようにしてな。木曜までな。その間に自分はサービンのドメイン取ったりRoute53周りの構築するから」

ワイ「これもcloud formationに書くんです?」

パイセン「serverless frameworkっていう基本的な設定はデフォルトで構築してくれる便利なものがあるんやで。これ使い」
ワイ「めっちゃ素敵やん。わかったやで」

パイセン「週初めのMTGは終わりや飯食いに行こう。上野に新しい醤油ラーメン屋ができたんや」
ワイ「いいですね〜」
パイセン「それじゃ自分は新しいロードバイク持ってきたからワイ君も付いてきてな!」
ワイ「ワイ無手なんやが?え、本気で漕初めやがった!こなくそおおおぉぉぉ!」

タクシー「ギャアアアアァァ」
パイセン「ギャアアアアァァ」
ワイ「ギャアアアアァァ轢かれてもた!!」

ワイ「救急車呼びました。大丈夫ですか?」
パイセン「もうアカン。int型でしか解釈できへん・・・」
ワイ「1+1計上して」
パイセン「11。ここで止まる訳にはいかんのや・・・這いずってでも明日出社するで・・・」
ワイ「ゆっくり養生して」
パイセン「プロジェクト遅らせたら化けて出るからな・・・」
ワイ「それは往生して」


~翌日~
ワイ「軽傷で済んだらしいけど先輩は大事取って今日は休みや・・・予定少しずれ込むかもなぁ・・・」

ワイ「まあいっか。気を取り直してできる範囲進めていくやで」
「まずAWSのGUIで構築してフロントとlambdaのコードが正しいかチェックするで。」
「API gatewayも初めて使うけどランダムでURL生成して即座に接続できるようにするのな。すっごいサービスや」
「お、期待通りにS3に保存できてるな。二つのリポジトリに書き込む以外余分な動作もしてないし、やるやんワイ」
「これをserverless frameworkに落とし込むやで。確かめ終わった構築は落としとくやで。ワイはコスト管理できる男やからな!」
「ほえ〜メソッドレスポンスの設定もよしなにやってくれるし面倒なCORSの設定も簡単や」
「ん?lambdaの側でbodyデータが上手くデコードされてないな。なんでやろ」
「差分がわかりやすいように、リソースの/ メソッドを使って試しまくるやで!」

・・・

ワイ「Content-TypeヘッダーがAPI gatewayデフォルトのapplication/jsonじゃなくてapplication/x-www-form-urlencodedを自動で指定してくれるんやな。痒いところに手が届くserverless framework最高や!」
「ヘッダが変更になる分のフロントとlambdaのコードを微修正して・・・お、定時やな。明日朝一で動作確認してもらえれば任務完了や!」

ワイ「予定より早く終わらせることができたやで。パイセンが驚く姿が目に浮かぶでぇ・・・」


~翌日~
パイセン「ワイ君すごいことになっとるで!!」
ワイ「もう見つけはったんですか。お目が高いなぁ〜ワイがやりました!」

パイセン「自分の社用のGmailが請求アラートで一杯や!!」
ワイ「ほ???????どういうこと??????????」
ワイ「早くAWSのBillingページ見ましょう!!!」

Billing「$1000」
パイセン・ワイ「$1000」
Billing「API gateway リクエスト数100,000,000」
パイセン・ワイ「一億アクセス」

パイセン「・・・身に憶えがあったりする?」
ワイ「・・・ワイがやりました」

TL;DR

API gatewayの「ルートメソッド(/ メソッド)」でAWSサービスを繋ぎこんだらDDoS攻撃の的になった。

実際には検証完了後メソッドの実装を破棄するまでの間の約1日半攻撃を受け続けたので$5000ほど溶けた。
特に掲題の構成は頑強であるため攻撃を受けても落ちない分、被害が拡大した。

説明

カスタムドメインを指定しないAPI gatewayのベースURLはREST APIを選択した場合、公式サイト にある通り

https://{restapi_id}.execute-api.{region}.amazonaws.com/{stage_name}/

上記のようになる。
そして、実際に生成されるURLは以下の通り。

https://abcde12345.execute-api.us-east-1.amazonaws.com/dev

ポイントは三点で、形式が決まっている分ある程度攻撃対象として予測が付けられるところ。

  1. ランダム生成されるrestapi_idは10桁固定。
  2. regionのパターンも数はあるが膨大ではない。
  3. stage_nameは任意だが、ありきたりなものにしてしまった。

DDoS攻撃をしかけた側は、URLの総当たりと存在しているURLかどうかを判別しているサーバーとDDoS攻撃そのものを行うサーバーを分けているらしく
CloudWatch Logsを分析すると、ご丁寧に 10分攻撃+5分の間に生存確認のためのアクセス という15分間隔で同じ回数分だけ攻撃を行っていた。
また、各サービスのアカウントまわりは社用で完全クローズドで、ログ解析してもURLが漏れたりハックされた形跡はなかった。
以上から{stage_name}を絞った上で{restapi_id}をランダム生成し、ヒットした場合は生存している限り攻撃を加える仕組みだったと想定される。

そして、攻撃されたエンドポイントでの処理自体が軽量であったこともありリクエストを全て捌いてしまった。

その結果、計5億超のAPIアクセスを許してしまい、同等数のlamdaが起動され、S3に10億ほどの余分なファイルが作られ短時間で超高額請求を受ける羽目になってしまった。。

対策

今回の事象に対して自分が思いつけてなるべく実現できそうなものをいくつか。
あまり攻撃対策の分野は得意ではないので、これより良い方法あるよとか、間違ってるよとかご意見いただけると有難いです。

  • ランダム生成URLにルートメソッド(/ メソッド)にサービスをぶら下げない
    今回がたまたまだったかは判らないが、ルート以降のメソッドを叩こうとしたログは無かった。
    このため、ランダム生成URLでも攻撃を受ける確率は下がると思われる。
    とはいえ、ルートメソッドをGETで実装したい時もあるはずなのでstage_nameをありきたりな名前にしないことくらいか

  • メソッドスロットリングのレート・バーストの値を下げる
    純粋にAPI gatewayの秒間アクセス可能な数を減らす。
    もしくは、使用量プランを指定する事でアクセス数の上限を指定できるのでこれを利用する。
    あくまで、開発環境を攻撃されないための方法となってしまうが・・・
    デフォルトの値 {レート:10000リクエスト数/秒、バースト:5000リクエスト数} のまま適用したことも被害を拡げた原因だった。

  • カスタムドメインを使う
    ランダムで生成してくれるURLを使わず自前のドメインを用意する。
    ドメイン取得は有料なので、サービスの稼働が前提だったりしないと厳しい。
    今回に限っては回避できていたかもしれない未来。

  • WAFなどのサービスを導入して接続元を分別するルールを設ける

  • CloudWatch Logsでアラームを仕込む
    監視することで即座に気付けるが作業量はもりもり増える。
    ここまでくるとserverless frameworkを使わずにAWS SAMを使った方がサービス管理がしやすくなってくる。コード量は増える。


ぶちょー「溶かした金額分を工数に変換して納期早めとくね」
パイセン・ワイ「🥺🥺 」

参考文献

【新機能】Amazon API Gatewayに「使用量プラン」機能が追加。キーごとにスロットリングやリクエストの制限が可能に : Developers.io
【AWS WAF vs AWS Shield】初心者にもわかりやすく解説 : wafcharm
AWS SAMとServerless Frameworkを比較してみた : Qiita