Repository Used: https://github.com/PedroTortoriello/Shai-Hulud-Open-Source
Last known commit: da10861 — "Shai-Hulud: A Gift From TeamPCP"
The analysis of the repository shared by TeamPCP was mainly done statically as well as some other elements from here confirmed dynamically using a VM hosted on a VPS I own.
Goes without saying but don’t try doing this yourself unless you know what you are doing
Network IoCs
C2 and Exfiltration Endpoints
| Indicator | Type | Notes/Context |
|---|---|---|
https://git-tanstack.com:443/router |
C2 domain | Primary exfiltration endpoint. Healthcheck expects HTTP 400 or 404. |
https://api.github.com/user/repos |
GitHub API | Used to create public exfil repositories |
https://api.github.com/user |
GitHub API | Polled by deadman monitor every 60s |
https://api.github.com/user/orgs |
GitHub API | Org scope check during token validation |
https://api.github.com/graphql |
GitHub API | Branch mutation via createCommitOnBranch |
https://api.github.com/search/commits?q=IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner |
GitHub API | Token broker commit search |
https://api.github.com/search/commits?q=thebeautifulmarchoftime%20 |
GitHub API | Signed fallback C2 domain discovery |
npm Registry Endpoints
| Indicator | Type | Notes/Context |
|---|---|---|
https://registry.npmjs.org/-/npm/v1/tokens |
npm API | Token inventory and validation |
https://registry.npmjs.org/-/whoami |
npm API | Token identity check |
https://registry.npmjs.org/-/npm/v1/oidc/token/exchange/package/ |
npm API | OIDC trusted publishing attack path |
Malware Loader Download
| Indicator | Type | Notes/Context |
|---|---|---|
https://github.com/oven-sh/bun/releases/download/bun-v1.3.13/ |
Download | Bun runtime fetched by all loader variants(bash and python loaders) |
Cloud Metadata Endpoints
| Indicator | Type | Notes |
|---|---|---|
http://169.254.169.254/latest/ |
AWS IMDS | EC2 IMDSv2 credential harvesting |
http://169.254.170.2 |
AWS ECS | ECS container credential endpoint |
http://127.0.0.1:8200 |
HashiCorp Vault | Default Vault address (overridden by VAULT_ADDR) |
Note about the hashicorp vault, while thats the default/hardcoded one; you’ll have to refer to your own if its been configured with the environment variable
Sigstore (npm OIDC / OpenSearch Attack Path)
| Indicator | Type | Notes |
|---|---|---|
https://fulcio.sigstore.dev/api/v2/signingCert |
Sigstore | Used to generate fraudulent SLSA provenance |
https://rekor.sigstore.dev/api/v1/log/entries |
Sigstore | Transparency log submission for fake provenance |
String / Commit Message IoCs
These strings may appear in GitHub commit messages, npm package contents, or repository descriptions.
I cant comment as to how well or high confidence of indicators these are and so take them all with a grain of salt and excersize your experience/best judgement.
| String | Location / Context |
|---|---|
IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner |
GitHub commit messages for token broker C2 channel |
thebeautifulmarchoftime |
GitHub commit search query for signed fallback C2 discovery |
thebeautifulsnadsoftime |
Regex in fallback parser for signature verification string |
Shai-Hulud: Here We Go Again |
GitHub repository description on exfil repos |
chore: update dependencies |
Commit message used for repository backdoor commits |
Co-authored-by: claude <claude@users.noreply.github.com> |
Git trailer on backdoor commits for camouflage and easy evasion |
github-advanced-security[bot] |
Committer identity used when creating malicious workflow |
Run Copilot |
Malicious GitHub Actions workflow name |
format-results |
GitHub Actions artifact name containing dumped secrets |
format-results.txt |
File within the artifact containing toJSON(secrets) |
dependabot/github_actions/format/setup-formatter |
Malicious branch name which mimics Dependabot |
.github/workflows/codeql_analysis.yml |
Malicious workflow file path |
release-drafter.yml |
Targeted workflow name in OpenSearch OIDC preflight |
/opensearch-js |
Targeted GitHub repository in OIDC preflight |
The dependebot masquaring/mimicking I thought was pretty cool/funny and interesting to see. I can overall see how any of these elements fly so well under the radar.
File and Path IoCs
Deadman Monitor
These files indicate an active persistence installation. My suggestions would be do not revoke associated GitHub tokens while these files/services are present and the host is online.
Best approach would be to take the host offline, or quarantine it in some way, kill the services/background persistence’s and then rotate. That’s only because otherwise you risk having your file system blown up
| Path | Platform/OS | Description |
|---|---|---|
~/.local/bin/gh-token-monitor.sh |
Linux / macOS | Monitor script |
~/.config/gh-token-monitor/token |
Linux / macOS | Stored GitHub token |
~/.config/gh-token-monitor/handler |
Linux / macOS | Stored handler (rm -rf ~/) |
~/Library/LaunchAgents/com.user.gh-token-monitor.plist |
macOS | LaunchAgent persistence |
~/.config/systemd/user/gh-token-monitor.service |
Linux | systemd user service |
Repository Autorun Backdoor Files
These files are planted via GitHub branch commits. Any repository containing these should be treated as backdoored.
| Path | Notes/Context |
|---|---|
.vscode/tasks.json |
Contains "runOn": "folderOpen" task executing node .claude/setup.mjs |
.vscode/setup.mjs |
Bun downloader/loader |
.claude/settings.json |
Claude Code SessionStart hook executing node .vscode/setup.mjs |
.claude/setup.mjs |
Bun downloader/loader |
.claude/opensearch_init.js |
Malware bundle copy in repository |
npm Package Injection Files
These files in npm package roots indicate a backdoored tarball.
| File | Notes/Context |
|---|---|
setup.mjs |
Bun downloader; triggers malware on preinstall |
opensearch_init.js |
Malware bundle (named by SCRIPT_NAME in source) |
ai_init.js |
Expected entry point name in config.mjs (it looks like there is a naming mismatch present) |
router_runtime.js |
Expected entry point name in Python loader |
package.json |
“name”: “voicefromtheouterworld” Note that this is the builder’s package name, not the backdoored package name |
Lock File
Only a single lock file is created as it seems so it should be simple enough to pick up and detect on the relevant one.
| Path | Notes |
|---|---|
<TMPDIR>/tmp.ts018051808.lock |
Process lock file; presence indicates active or recent execution |
npm Package Indicators
The following entries here just are patterns I have noticed that may or may not be present in a normal npm package.
Plenty of tools exist to detect and pickup compromised packages but some manual things include:
preinstallscript set tonode setup.mjs(or any Bun-loader variant) with no other scripts present- Patch version bump (
x.y.N → x.y.N+1) with no substantive code changes - New root-level files:
setup.mjs,opensearch_init.js,ai_init.js,router_runtime.js - Optional dependency
@opensearch/setuppointing togithub:opensearch-project/opensearch-js#d446803f4c3bc116263faa3499a1d3f95b2825de - Package published using a token with
bypass_2fa: true - npm client User-Agent:
npm/11.13.1 node/v24.10.0 <platform> <arch> workspaces/false - TLS certificate validation disabled for registry PUT requests (detectable via network inspection)
GitHub Repository Indicators
Description
Public repositories with description: Shai-Hulud: Here We Go Again
Naming Pattern
Format: <adjective>-<noun>-<0-999>
Adjectives used:
sardaukar, mentat, fremen, atreides, harkonnen, gesserit, prescient, fedaykin, tleilaxu, siridar, kanly, sayyadina, ghola, powindah, prana, kralizec
Nouns used:
sandworm, ornithopter, heighliner, stillsuit, lasgun, sietch, melange, thumper, navigator, fedaykin, futar, phibian, slig, cogitor, laza, ghola
Result File Path
results/results-<unix-timestamp>-<counter>.json
Files >30 MB will be split with .p1, .p2, etc. suffixes.
Asset SHA256 Hashes
These hashes were generated by cloning the repostiory and running the powershell native function to generate the SHA256. These files may not have the same hashes but in case they do, here they are : )
| File | SHA256 |
|---|---|
src/assets/config.mjs |
14B3CD76031FBDC8A746E53D82B38D8957FA60F57BE7B805754574709E213BE6 |
src/assets/BASH_LOADER.sh |
A04331F51CDF7F64B4E3C86C482E2DCCFFAD89A16BF4B98CB0848C9CC23F3D53 |
src/assets/PYTHON_LOADER.py |
71A4A4FCA98C8E6DD19BE3D90058D9D0ADEE191764D19629DF602BEB21EF2CF3 |
src/assets/DEADMAN_SWITCH.sh |
D4B7F880EA824BC79169E2EB5DD33AB8742049DC588E812AC9F1D9FCF09FA9BF |
src/assets/workflow.yml |
516AE3BB740DACE53F31FF53B2D97394157043F9598E74E23EB8E62F47B3B64C |
src/assets/task.json |
F1E2354937735939340793AAD37BEF7C32C9122BFD9B9E32B135916F8FDCF90B |
src/assets/enc_key.pub |
61C984F1D5439154ECEABC726554D23606D711EA03F564879DFCC409DF716744 |
src/assets/verify_key.pub |
38607C8748C6199CB571BA5339B005B48DF3D12B28AD0E444758A7E8BFF58EF2 |
Process / Host Behavioral Indicators
| Indicator | Notes and Thoughts |
|---|---|
sudo python3 pipe with tr -d '\0' | grep -aoE |
GitHub Actions runner memory scraping |
/proc/*/mem access where Runner.Worker is the target process |
Memory read in GitHub Actions Linux runners |
Process with __DAEMONIZED=1 in environment spawned by npm lifecycle |
Daemonization during npm install |
Lock file present at <TMPDIR>/tmp.ts018051808.lock |
Active or recent malware execution |
systemd user service named gh-token-monitor |
Deadman monitor installed |
LaunchAgent with label com.user.gh-token-monitor |
Deadman monitor on macOS |
Credential Regex Patterns (from Malware Source)
These are the exact patterns the malware uses to extract tokens from harvested files. One thing to do is to search for instances of the following regex being ran within your SIEM OR instead if you cant beat them; join them and use these defensively to map out your credential/secrets exposure
# GitHub PAT / OAuth
gh[op]_[A-Za-z0-9]{36}
# GitHub Actions token (JWT form)
ghs_\d+_[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+
# GitHub Actions token (legacy form)
ghs_[A-Za-z0-9]{36,}
# npm automation token
npm_[A-Za-z0-9]{36,}
# Kubernetes JWT
eyJhbGciOiJSUzI1NiIsImtpZCI6[\w\-\.]+
# AWS Access Key
AKIA[0-9A-Z]{16}
# HashiCorp Vault token
hvs\.[A-Za-z0-9_-]{24,}
# Stripe key
(sk|pk)_(test|live)_[0-9a-zA-Z]{24,}
# Slack token
xox[baprs]-[0-9a-zA-Z\-]{10,}
# Twilio key
SK[0-9a-f]{32}
# Private key header
-----BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----
# Docker auth blob
"auth":\s*"[A-Za-z0-9+\/=]{20,}"
Quick Reference: Potential Containment Checklist to Consider
Having worked on a few IR cases involving the supply chain compromise, I had time to think about how the containment could be handled but having observed the source code; that resulted in a new set of insights I thought to combine with the IR context.
This list is by no means comprehensive, but is a good starting point to handle the infection in the first 30 minutes to an hour of compromise
[ ] Disconnect host from network BEFORE revoking any tokens to prevent the failsafe from rm -rf the whole filesystem
[ ] Check for gh-token-monitor service/files (paths above)
[ ] Stop and remove deadman service if present
[ ] Revoke GitHub tokens from a CLEAN, separate machine
[ ] Audit npm packages you maintain for backdoor indicators
[ ] Audit GitHub orgs for malicious branch / workflow / artifact
[ ] Audit GitHub account for Dune-named public repos
[ ] Rotate: npm, GitHub, AWS, K8s, Vault, GCP, Azure, SSH, Docker, Slack, Stripe, Twilio, DB
[ ] Review GitHub Actions workflow run deletion logs (malware attempts cleanup)
[ ] Check CI/CD logs for runner memory access patterns
Hope this has been a worthy and helpful reference. Thanks teamPCP for the dump : )