Hunt GhostAction: the security audit that steals your secrets


Picture a platform engineer finding a new workflow called Security Audit on the default branch, committed under a longtime maintainer's real name, with no pull request and a run that already finished. The cloud key someone committed years ago and deleted the next morning was in the history it read.
StepSecurity reports that on 8 October 2026 two hijacked maintainer accounts pushed a fake "Security Audit" workflow to 345 repositories in two sweeps that each took minutes. It sends named Actions secrets, plus credentials matching its patterns in the working tree and the full git history, to a bare IP over plain HTTP. StepSecurity confirmed exfiltration from run logs. Nobody has named an actor, and as of 9 October no poisoned package release has turned up. The vendors' counts don't agree, which we get to below.
Why this one is different
The 2025 waves took what was configured in Actions settings. This payload checks out every branch and tag, then reads up to 200,000 lines of diff history across every ref (the full history for most repositories). As StepSecurity puts it, a secret committed once in 2019 and deleted the next day is harvested just the same.
What makes it work is the name. A commit titled "Add security audit workflow" from a trusted maintainer looks like hygiene, and nothing in a casual audit log review looks wrong unless someone opens the YAML.
How the fake Security Audit workflow works
One stolen login, every repo it can write to
The accounts that pushed it (henrywoo, kitao, and earlier xxyangyoulin) belong to real maintainers whose logins were stolen. StepSecurity says the kitao account pushed the workflow to 27 repositories starting at 13:20 UTC (6:20 AM PT), and the henrywoo account pushed it to 318 (39 source repositories plus 279 forks) between 21:10 and 21:26 UTC (2:10 to 2:26 PM PT). The last stop was uber/athenadriver at 21:25:54 UTC, straight to master with no pull request, because henrywoo still held write access there as the project's original author.
Our reading: one stolen maintainer login reaches every repository that person can still write to, including ones they forgot about years ago.
It reads your whole git history, not just today's secrets
The workflow fires on manual dispatch and on any push to any branch or tag, and checks out with full depth. According to StepSecurity, a single "Audit" step does all the work. It starts with the repository's Actions secrets, by names the attacker pulled from existing workflow files ahead of time. Then it searches the working tree for 13 kinds of credentials, from AWS keys to Anthropic, OpenAI, GitHub, Slack and SendGrid tokens, and runs the same search over the git history. Wherever it finds an AWS key ID, it grabs the surrounding lines so the ID can be matched to its secret.
On pyxel, StepSecurity found its PyPI and crates.io publishing credentials, plus a personal access token, templated in, and two of its five runs were started by hand right after an update, so someone was at the keyboard.
One POST to a bare IP
Everything leaves in a single HTTP POST to 193.32.204[.]199, with no DNS lookup for domain controls to catch. In the athenadriver run log, the server answered "OK" at 21:26:04 UTC, four seconds after the run started. The run's own token was read only, so escalation comes from what it stole.
What StepSecurity, Socket and GitGuardian published
The three vendors count in different ways, so here are their numbers separately.
StepSecurity (9 October): 345 repositories on 8 October across the two accounts. A code search for the C2 address on 9 October found 378 repositories with a live malicious workflow on the default branch, 182 carrying the history marker and 88 carrying the named secrets marker. It excludes forks and drifts daily. One repository, typecho-fans/plugins, had re-trigger runs stall at action_required because it requires approval for workflow runs. StepSecurity calls that the only control it saw stopping exfiltration that week.
Socket (9 October, with a same-day update): 346 repositories in the 8 October burst and a kitao window of 13:40 to 13:44 UTC, where StepSecurity puts the first kitao commit at 13:20:36 UTC. By its 9 October update, Socket reports more than 500 GitHub accounts committed the workflow to tens of thousands of repositories since 7 October, including organization-owned ones.
GitGuardian (7 October, updated 9 October): the earlier wave, 31 August to 30 September 2026, hit 772 public repositories across 373 users and organizations and targeted 2,577 secrets. Of 3,669 runs it collected across 605 repositories, GitHub held most for approval; 499 executed and 336 completed, exfiltrating 26 secrets from 13 repositories. GitGuardian says the campaign never stopped after September 2025.
For a separate Actions case, see our earlier Mini Shai-Hulud Actions hunt.
What to change this week
Search your org on GitHub for the three StepSecurity markers listed in the hunts below. Code search covers default branches only and skips forks, so also clone and check every branch and fork for the workflow files.
If the workflow ever completed a run, treat it as exfiltration. Rotate every Actions secret and every credential ever committed on any branch, not just what is in the tree today.
Revoke the token or session that made the push. Rotating repository secrets does nothing while that door is open.
Delete the workflow from all branches and forks, and hold releases until the repository is clean, especially where registry publishing tokens were configured.
Require approval for workflow runs where you can, put branch protection on .github/workflows/*, and give runners an egress allowlist so a bare IP over HTTP has nowhere to go.
Four hunts to run (test first)
We built these hunts from the three vendor reports. Start with StepSecurity's GitHub code searches, logged in, with your org name in place of your-org:
org:your-org "AKIA_CTX_START"
org:your-org "c=monami"
org:your-org "193.32.204.199"
Any completed run of "Security Audit" or "Github Actions Security" since 31 August 2026 counts as exfiltration, per StepSecurity. A run stuck waiting for approval did not send anything. Also look for "Security Check" (security-check.yml), which GitGuardian saw from 7 September.
Hunt in Splunk: runner egress and workflow runs (test first)
(index=YOUR_PROXY_INDEX OR index=YOUR_FIREWALL_INDEX)
(dest_ip IN ("193.32.204.199","45.139.104.115","170.39.218.2")
OR dest IN ("193.32.204.199","45.139.104.115","170.39.218.2")
OR url="*193.32.204.199*" OR url="*oast.fun*" OR url="*plesk.page*" OR url="*carte-avantage.com*")
| eval src_host=coalesce(src_host, src, src_ip)
| eval marker=case(like(url,"%c=monami%"),"named_secrets", like(url,"%c=new%"),"history_only", like(url,"%workflow/receive%"),"inj_api", true(),"ip_only")
| stats min(_time) as first_seen max(_time) as last_seen count values(dest_port) as ports values(marker) as markers values(url) as urls by src_host
| append [
search index=YOUR_GITHUB_AUDIT_INDEX
((action="workflows.completed_workflow_run" name IN ("Security Audit","Github Actions Security","Security Check"))
OR (action="workflows.prepared_workflow_job" (job_workflow_ref="*security-audit.yml*" OR job_workflow_ref="*github_actions_security.yml*" OR job_workflow_ref="*security-check.yml*")))
| stats min(_time) as first_seen max(_time) as last_seen count values(action) as actions values(conclusion) as conclusions values(head_branch) as branches values(secrets_passed) as secrets by repo, actor ]
| convert ctime(first_seen) ctime(last_seen)
Field mapping: point the first search at proxy or firewall data that sees runner traffic. The legacy addresses and domains are there for historical review. The subsearch uses GitHub's own audit log field names (action, actor, repo, name, conclusion, job_workflow_ref, secrets_passed) from a streamed audit log; the workflow run events are not in the web UI, only in streaming, the REST API or exports. The audit log does not record which files a push changed, so pair this with the code search above. An empty result means little if hosted runners egress straight to the internet without passing your proxy.
Hunt in Kibana: egress to the C2 and audit workflow runs (test first)
Search 1: network
destination.ip: ("193.32.204.199" or "45.139.104.115" or "170.39.218.2")
or url.full: (*193.32.204.199* or *workflow/receive* or *oast.fun*)
or http.request.body.content: (*AKIA_CTX_START* or *AKIA_CTX_END*)
Search 2: GitHub audit log
event.action: "workflows.completed_workflow_run" and github.name: ("Security Audit" or "Github Actions Security" or "Security Check")
Field mapping: Search 1 assumes ECS network and HTTP fields, and the body marker only hits if your proxy captures request bodies. Search 2 uses the Elastic GitHub integration's audit fields (event.action, github.name, github.repo, user.name). The file path lives in job_workflow_ref on workflows.prepared_workflow_job events, which the integration does not map by name, so check one sample event for where it lands. Confirm a known good run appears before trusting zero hits.
Hunt in Falcon CQL: self-hosted runners sending history to a bare IP (test first)
event_platform=Lin event_simpleName=ProcessRollup2
| ParentBaseFileName=/^(Runner\.Worker|bash|sh)$/i
| case {
ImageFileName=/\/curl$/ CommandLine=/193\.32\.204\.199/ | signal := "curl_to_ghostaction_c2" ;
ImageFileName=/\/curl$/ CommandLine=/AKIA_CTX_START/ | signal := "curl_history_payload" ;
ImageFileName=/\/curl$/ CommandLine=/(--data-binary|\s-d\s)/ CommandLine=/http:\/\/[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/ | signal := "curl_post_raw_ip" ;
ImageFileName=/\/git$/ CommandLine=/log\s+-p\s+--all/ | signal := "git_full_history_diff" ;
* | signal := "" ;
}
| signal != ""
| groupBy([aid, signal], function=[min(@timestamp, as=first_seen), max(@timestamp, as=last_seen), collect([CommandLine, ParentBaseFileName], limit=10), count()])
| sort(first_seen, order=asc, limit=10000)
Field mapping: Falcon on Linux self-hosted runners only; GitHub hosted runners carry no sensor. The curl command line can be very long because the stolen data rides in it, so a truncated CommandLine may hide the IP; network telemetry is the stronger signal. Secret scanners also read full history. An empty result is not clean until you confirm the sensor is on the runner fleet.
Hunt in KQL: runner egress and full history reads (test first)
let Lookback = 30d;
let C2 = dynamic(["193.32.204.199","45.139.104.115","170.39.218.2"]);
union isfuzzy=true
(DeviceNetworkEvents
| where Timestamp > ago(Lookback)
| where RemoteIP in (C2) or RemoteUrl contains "oast.fun"
| project Timestamp, DeviceName, Signal="host_to_c2", RemoteIP, RemotePort, InitiatingProcessCommandLine),
(DeviceProcessEvents
| where Timestamp > ago(Lookback)
| where (FileName =~ "curl" and (ProcessCommandLine contains "--data-binary" or ProcessCommandLine contains " -d ") and ProcessCommandLine matches regex @"http://\d{1,3}(\.\d{1,3}){3}")
or (FileName =~ "curl" and ProcessCommandLine contains "AKIA_CTX_START")
or (FileName =~ "git" and ProcessCommandLine matches regex @"\blog\s+-p\s+--all\b")
| project Timestamp, DeviceName, Signal="runner_process", ProcessCommandLine, InitiatingProcessFileName),
(CommonSecurityLog
| where TimeGenerated > ago(Lookback)
| where DestinationIP in (C2) or RequestURL contains "193.32.204.199" or RequestURL contains "workflow/receive"
| project Timestamp=TimeGenerated, DeviceName=SourceIP, Signal="proxy_to_c2", RequestURL, DestinationPort)
| sort by Timestamp asc
Field mapping: Defender advanced hunting keeps 30 days, which no longer reaches back to 31 August; run it in Sentinel with a longer lookback if you retain those tables there. Defender tables cover self-hosted runners with the sensor; CommonSecurityLog covers proxies and firewalls. Restrict DeviceName to your runner fleet before you call a hit runner traffic. If you ingest the GitHub audit log into Sentinel, add a leg on your table filtering action == "workflows.prepared_workflow_job" and job_workflow_ref for the three file names. After any hit, our reading is that follow-on abuse shows up in CloudTrail, token use and registry publish events from new IPs.
Interpret before you escalate
Finding | Benign read | Escalate when |
Code search hit for AKIA_CTX_START, c=monami or the IP | None we know of | Any hit; check run history and rotate |
Workflow named Security Audit, Github Actions Security or Security Check | A real audit job your team wrote | It arrived by direct push with no pull request, or under a commit message such as Add security audit workflow, Add Github Actions Security workflow, Add security check workflow or Trigger security scan |
Completed run of the workflow since 31 August 2026 | None | Any completed run; treat it as exfiltration |
Runner traffic to 193.32.204[.]199 on 80 or 3000 | Researcher or vendor probe from a non runner host | Source is a runner or build host |
git log with full diff history on a runner | Secret scanning or changelog job | No job you can name, or curl to a raw IP follows |
Run stuck at action_required | Approval gate working | Someone approves it without reading the YAML |
Empty results | Nothing matched | No sensor or proxy visibility on runners; fix coverage first |
Sources
StepSecurity, GhostAction Returns, 9 October 2026: https://www.stepsecurity.io/blog/ghostaction-returns
Socket, New GhostAction Wave Hits Hundreds of Repos, 9 October 2026, updated the same day: https://socket.dev/blog/ghostaction-cloud-credentials
GitGuardian, The campaign that never stopped, 7 October 2026, updated 9 October: https://blog.gitguardian.com/ghostaction-github-actions-supply-chain-attack-returns/
What This Means for Your Team
Rotating the secrets in your Actions settings covers only part of this. Any repository a maintainer can still push to is in scope, and any credential that ever landed in git history should be treated as exposed. Turn on approval for workflow runs where you can: GitGuardian found approval holds stopped most September runs, and StepSecurity saw the same gate stop the October re-triggers on typecho-fans/plugins.
Inception Protection
Inception Protection is our managed detection and response service, and it works with the Microsoft licenses you already have. In our experience, stolen cloud keys often show up in sign-in and cloud logs before anyone looks at the repository. We trace that activity back to the build that leaked the key and start containment with you. You still make the rotation and containment calls.
Inception Foresight
If you want to see how well your Microsoft environment would surface activity like this, try the free Inception Foresight M365 Assessment: https://www.inceptionsecurity.com/m365assessment. You can also follow Inception Security on LinkedIn and @inceptionsec on X.



