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

01

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.

learner@cloud-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.

02

Find accounts outside every unit

Guardrail policies attach to organisational units. An account left directly under the root gets none of them.

learner@cloud-lab
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.

03

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.

learner@cloud-lab
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.

04

Read what the stranger can do

learner@cloud-lab
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.

05

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.

learner@cloud-lab
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

Forgotten
account: ml-experiments
parent:  root (no guardrails)

role: legacy-sandbox-sync
trust: 210987654321 (unknown)
policy: AdministratorAccess

Outside every rule, with a door into production.

Governed
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

  1. 1Test and legacy environments need the same controls as production, or none of the access.
  2. 2Check every account a role trusts against the accounts you actually own.
  3. 3Quarantine, wait, then delete. Record the decision.

Questions

0 of 3 solved

Answer each one from what you find on your lab machine.