Let Me Speak to Your Manager (Account)

Let Me Speak to Your Manager (Account)

Dan Gansel September 25, 2026

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 call
  • aws: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:

  1. Attach a session policy during sts:AssumeRole that denies the request if a candidate key holds a specific value
  2. Make an API call (e.g., s3:GetObject)
  3. Denied → the key exists and holds that value
  4. 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 4

10 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:

KeyValueWhose
aws:PrincipalOrgMasterAccountId111122223333Our org’s management account
aws:ResourceOrgMasterAccountId444455556666Their org’s management account
aws:PrincipalOrgIDo-a1b2c3d4e5Our org
aws:ResourceOrgIDo-f6g7h8i9j0Their org
aws:PrincipalAccount999988887777Our account
aws:ResourceAccount123456789012Their 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:

WhatKey
Bucket owner’s account IDaws:ResourceAccount (documented)
Bucket owner’s org IDaws:ResourceOrgID (documented)
Bucket owner’s management account IDaws: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:PrincipalOrgMasterAccountId is 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 AssumeRole calls are in your account’s CloudTrail only. The target’s CloudTrail would show normal GetObject requests 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:PrincipalOrgMasterAccountId to be populated. Standalone accounts don’t have a management account.
Contents

Further Reading

Let Me Speak to Your Manager (Account)

Let Me Speak to Your Manager (Account)

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…
Configuration-Focus

Introducing the new Configurations experience in Upwind

Compliance should not be a fire drill! Ask a security team how audit season goes and you will often hear a version of the same story. Someone pulls a list of cloud accounts. Someone else exports findings into a spreadsheet that is already outdated by the time it is shared. Screenshots get pasted into a…
What You Could Build If IAM Let You

What You Could Build If IAM Let You: New Policies From Undocumented Condition Keys

In the previous post, we mapped 36 condition keys that the IAM engine evaluates but has never documented. The decomposition model, the service-specific resource identifiers, the organizational metadata - all of it sitting in the request context, invisible unless you probe for it. That post was about discovery. This one is about what you can…
Add the Upwind RSS Feed to Slack
Connect the Upwind RSS Feed to your Slack.
Follow the how-to here.
Threat RSS
Add the Upwind RSS Feed to Slack
Connect the Upwind RSS Feed to your Slack.
Follow the how-to here.
Main RSS