Unit 1: How a cloud account is laid out · Lesson 5 of 5 · 24 min
Case file: the forgotten test environment
Find the old test account and the admin role that still trusts it, the same pattern behind the 2024 Midnight Blizzard intrusion.
case file
Midnight Blizzard Test Tenant Password Spray
Microsoft · 2024
Starting in late November 2023 the Russia-linked actor Midnight Blizzard (APT29) used a low-volume password spray through residential proxies to compromise a legacy, non-production test tenant account that lacked MFA. The actor then abused a legacy test OAuth application that had elevated access to Microsoft's corporate environment, created further OAuth apps and granted itself full mailbox access, reading email of senior leadership, security and legal staff. Microsoft detected the attack on 12 January 2024.
What went wrong
A forgotten test tenant account without MFA and a test OAuth application with excessive privileges into the production corporate tenant.
What would have stopped it
- Apply MFA and conditional access to all tenants and accounts, including test and legacy ones
- Inventory and least-privilege OAuth applications; remove broad roles like full_access_as_app where not required
- Decommission or isolate non-production tenants from production permissions
- Password spray defenses: banned passwords, smart lockout, risk-based sign-in policies
What defenders could have seen
- Low-and-slow failed sign-ins across many accounts from rotating residential IPs
- New OAuth app creation or consent grants and new application role assignments
- Exchange Web Services mailbox access by an application not previously seen
sourcesmicrosoft.commicrosoft.com
mission
- You are
- Security engineer reviewing Brightwell's AWS organisation.
- Situation
- Brightwell runs several AWS accounts under one organisation. A 2019 sandbox was set up before anyone wrote rules, and nobody remembers closing it. This lab is modelled on the Midnight Blizzard case.
- Goal
- Find accounts and trust relationships that sit outside the guardrails, then quarantine the riskiest one.
- Hand in
- The account outside any guardrail, the role that trusts an unknown account, and proof it can no longer be assumed.
Guardrails cover the accounts they are attached to
List the accounts in the organisation
An organisation groups accounts so rules can be set once. The management account is called master in this lab.
aws organizations describe-organization --query 'Organization.FeatureSet' --output text ALL aws organizations list-accounts --query 'Accounts[].Name' --output text master log-archive security-tooling brightwell-staging ml-experiments
you should see Five accounts, including one called ml-experiments.
Find accounts outside every unit
Guardrail policies attach to organisational units. An account left directly under the root gets none of them.
ROOT=$(aws organizations list-roots --query 'Roots[0].Id' --output text) aws organizations list-accounts-for-parent --parent-id $ROOT --query 'Accounts[].Name' --output text master ml-experiments aws organizations list-organizational-units-for-parent --parent-id $ROOT --query 'OrganizationalUnits[].Name' --output text Security Workloads
you should see ml-experiments sits beside the management account, outside both Security and Workloads.
defender tip New accounts land in the root by default. Make moving them into a unit part of the account request, not an afterthought.
Find roles trusted from outside the organisation
A role in this account can be assumed by any account its trust policy names. Compare every trusted account with the organisation's member list.
ORG=$(aws organizations list-accounts --query 'Accounts[].Id' --output text) for r in $(aws iam list-roles --query 'Roles[].RoleName' --output text); do a=$(aws iam get-role --role-name $r --query 'Role.AssumeRolePolicyDocument.Statement[0].Principal.AWS' --output text | cut -d: -f5); case "$a" in ""|None) continue;; esac; echo "$ORG" | grep -qw "$a" && echo "$r $a inside" || echo "$r $a OUTSIDE"; done | sort break-glass-admin 123456789012 inside legacy-sandbox-sync 210987654321 OUTSIDE vendor-readonly 444455556666 OUTSIDE
you should see Two roles trust accounts that are not in the organisation. The vendor is known. 210987654321 is not.
Read what the stranger can do
aws iam list-attached-role-policies --role-name legacy-sandbox-sync --query 'AttachedPolicies[].PolicyName' --output text AdministratorAccess aws iam list-role-tags --role-name legacy-sandbox-sync --query 'Tags[].[Key,Value]' --output text created 2019-05-14 env test owner unknown
you should see Full admin rights for a test sync from 2019, with no owner. The same shape as the test application in the Midnight Blizzard case.
Quarantine before you delete
Replace the trust policy with a deny and remove admin rights. If something still depends on the role, it fails loudly and someone comes to you, instead of breaking silently after a delete.
aws iam update-assume-role-policy --role-name legacy-sandbox-sync --policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Deny","Principal":{"AWS":"*"},"Action":"sts:AssumeRole"}]}' aws iam detach-role-policy --role-name legacy-sandbox-sync --policy-arn arn:aws:iam::aws:policy/AdministratorAccess aws iam tag-role --role-name legacy-sandbox-sync --tags Key=status,Value=quarantined-2026-10-22 aws iam get-role --role-name legacy-sandbox-sync --query 'Role.AssumeRolePolicyDocument.Statement[0].Effect' --output text Deny
you should see The trust statement now says Deny and no policy is attached.
defender tip Put a date in the quarantine tag. If nobody claims the role in 30 days, delete it and record why.
Test environments, before and after
account: ml-experiments parent: root (no guardrails) role: legacy-sandbox-sync trust: 210987654321 (unknown) policy: AdministratorAccess
Outside every rule, with a door into production.
account: ml-experiments parent: Workloads (guardrails) role: legacy-sandbox-sync trust: Deny * status: quarantined, delete in 30 days
Same rules for test as for production, and no stale trust.
What a defender takes away
- 1Test and legacy environments need the same controls as production, or none of the access.
- 2Check every account a role trusts against the accounts you actually own.
- 3Quarantine, wait, then delete. Record the decision.
Questions
0 of 3 solved
Answer each one from what you find on your lab machine.