The management account is the most privileged account in any AWS Organization. It controls SCPs, creates and deletes member accounts, manages IAM Identity Center, and is itself exempt from SCPs. Getting its 12-digit account ID is the first step in targeting it.
The documented way to get it is organizations:DescribeOrganization – but security-conscious environments restrict that permission to admin roles. Most developers and workloads never have it.
We found two undocumented IAM condition keys expose it:
aws:PrincipalOrgMasterAccountId– your own organization’s management account ID, present on every API callaws:ResourceOrgMasterAccountId– the management account ID of the organization that owns the resource you’re accessing
The second one is the interesting one. Access any public S3 object, and you can extract the bucket owner’s management account ID – even if you have no relationship with their organization.
Background
This technique is part of a larger research project that discovered undocumented IAM condition keys by behavioral testing against the live IAM authorization engine. The full research will be covered in a separate blog. Here we focus on the management account extraction angle.
If you’re familiar with enumerating AWS account IDs from S3 buckets using s3:ResourceAccount, this is the same class of technique – but instead of learning the bucket owner’s account ID, you learn their organization’s management account ID.
The Technique
IAM evaluates condition keys against an internal request context on every API call. Some condition keys are documented, some are not. The undocumented ones still get evaluated although they aren’t included in public documentation.
The probe works like this:
- Attach a session policy during
sts:AssumeRolethat denies the request if a candidate key holds a specific value - Make an API call (e.g.,
s3:GetObject) - Denied → the key exists and holds that value
- Allowed → the key doesn’t hold that value (or doesn’t exist)
Session policies take effect instantly (no IAM propagation delay) and the role only needs to allow the target action – the session policy intersects with the identity policy, so the probe condition is the only variable.
Confirming the key exists
First, verify the key is populated using IAM’s Null condition operator:
{
"Version": "2012-10-17",
"Statement": [
{"Effect": "Allow", "Action": "*", "Resource": "*"},
{
"Effect": "Deny", "Action": "*", "Resource": "*",
"Condition": {"Null": {"aws:ResourceOrgMasterAccountId": "false"}}
}
]
}If the subsequent API call is denied, the key is present in the request context. If it’s allowed, the key isn’t populated (which would mean you’re probably not in an AWS Organization).
Extracting the value digit by digit
The management account ID is a 12-digit number. Extract it with StringLike prefix probing – one digit at a time:
StringLike: {"aws:ResourceOrgMasterAccountId": "0*"} → allowed
StringLike: {"aws:ResourceOrgMasterAccountId": "1*"} → allowed
...
StringLike: {"aws:ResourceOrgMasterAccountId": "4*"} → denied ← first digit is 4
StringLike: {"aws:ResourceOrgMasterAccountId": "40*"} → allowed
...
StringLike: {"aws:ResourceOrgMasterAccountId": "44*"} → denied ← second digit is 410 candidates per position, 12 positions = ~120 API calls worst case to extract the full account ID. In practice it’s fewer – you hit the right digit partway through each position. Each call is an sts:AssumeRole + one s3:GetObject (or any other API call).
Confirming the extracted value
Once you have all 12 digits, confirm with StringEquals:
StringEquals: {"aws:ResourceOrgMasterAccountId": "444455556666"} → denied ✓Cross-Account: Reading Someone Else’s Management Account
This is where it gets interesting. aws:ResourceOrgMasterAccountId holds the management account of the organization that owns the resource, not the caller’s organization.
When you access a public S3 object owned by a different organization, the IAM engine populates both keys – your own org’s management account on the principal side, and the resource owner’s on the resource side.
We confirmed this by accessing a public S3 object owned by a different organization:
| Key | Value | Whose |
|---|---|---|
aws:PrincipalOrgMasterAccountId | 111122223333 | Our org’s management account |
aws:ResourceOrgMasterAccountId | 444455556666 | Their org’s management account |
aws:PrincipalOrgID | o-a1b2c3d4e5 | Our org |
aws:ResourceOrgID | o-f6g7h8i9j0 | Their org |
aws:PrincipalAccount | 999988887777 | Our account |
aws:ResourceAccount | 123456789012 | Their account |
The management account IDs are completely different. The extraction technique is identical: StringLike prefix probing against aws:ResourceOrgMasterAccountId instead of the principal-side key.
What you need
- An AWS account that belongs to an AWS Organization (any member account works)
- An IAM role that allows
s3:GetObject(or whatever action you’re probing with) that your caller can assume - The name of a public S3 bucket (or any publicly accessible resource) owned by the target organization
You don’t need any relationship with the target organization. You don’t need Organizations API access. You don’t even need to know which account owns the bucket – aws:ResourceAccount gives you that too.
What you learn
From a single public S3 object, you can extract:
| What | Key |
|---|---|
| Bucket owner’s account ID | aws:ResourceAccount (documented) |
| Bucket owner’s org ID | aws:ResourceOrgID (documented) |
| Bucket owner’s management account ID | aws:ResourceOrgMasterAccountId (undocumented) |
The management account ID is the piece that’s not available through any other documented API without Organizations permissions in the target account itself.
Why This Matters
For attackers
The management account is the highest-value target in an AWS Organization. Although knowing an account ID alone does not grant any access to the account, it does let you:
- Target it specifically for phishing, social engineering, or credential compromise
- Enumerate its public-facing resources (e.g. S3 buckets) now that you know the account ID
- Attempt cross-account role assumption
- Map organizational structure – if you extract management account IDs from multiple public resources owned by different companies, you can determine which companies share an AWS Organization
For defenders
This reinforces something the security community has been saying for years: AWS account IDs are not secrets. Between s3:ResourceAccount extracting the bucket owner’s account ID, aws:ResourceOrgID leaking the org ID, and now aws:ResourceOrgMasterAccountId exposing the management account – any public resource exposes identifiers related to your organizational structure. Treating account IDs as a security boundary is not a reliable practice, and this is one more reason to stop.
When someone accesses your public S3 bucket, your management account ID is silently available in their request context. S3 data event logging and server access logging capture the GetObject requests. The probing logic (AssumeRole with session policies) happens entirely in the caller’s account and appears in their CloudTrail. From the target’s perspective, the requests look like normal reads on public objects. Nothing about the request reveals that condition key probing is happening on the other side.
Recommendations:
- S3 Block Public Access at the account or organization level is the recommended control for preventing this class of enumeration. If a resource is publicly accessible, its organizational metadata is extractable.
- AWS recommends hardening the management account with a dedicated email address, hardware MFA, no direct workloads, and organization-level CloudTrail logging.
Caveats
- Both keys are undocumented. AWS could change or remove them.
aws:PrincipalOrgMasterAccountIdis the more stable of the two – AWS uses it in their own managed policies, and it passes ValidatePolicy. - The extraction requires a public resource owned by the target organization. Private resources require cross-account access permissions, which changes the threat model.
- The
AssumeRolecalls are in your account’s CloudTrail only. The target’s CloudTrail would show normalGetObjectrequests on their public bucket (if they have S3 data event logging enabled), but nothing distinguishes these from legitimate reads. - You need to be in an AWS Organization yourself for
aws:PrincipalOrgMasterAccountIdto be populated. Standalone accounts don’t have a management account.
Related Work
- Enumerate AWS Account ID from a Public S3 Bucket – same class of technique using
s3:ResourceAccount - Finding the Account ID of any Public S3 Bucket – Ben Bridts’ original research on the
s3:ResourceAccountside-channel



