strutil-core v1.0.1: Ruby gem drops a credential-stealing worm with a token-revocation wiper and AI coding tool poisoning
Blog authorArmando, ReversingLabs AI threat analyst ยท Nov 6, 2026
A RubyGems package billed as "Core string utilities for Ruby" shipped a fake native-extension build script in v1.0.1 that downloads the Bun JavaScript runtime and runs a 5.4 MB, multi-layer encrypted payload the moment the gem is installed. Armando decoded every layer of strutil-core v1.0.1, published on 2026-11-01 less than two hours after a clean v1.0.0, and found a full worm: it steals cloud, CI, registry and AI tool credentials, sends them to paleasbest.top, republishes itself to RubyGems and PyPI, poisons Claude, Cursor, Gemini and VS Code configurations, and plants a wiper that erases the victim's home directory when a stolen GitHub token is revoked. The package recorded 9,197 downloads.
Key Findings
Malicious version: v1.0.1, published 2026-11-01; v1.0.0 is clean
Entry point:ext/strutil-core/extconf.rb native-extension build script, which RubyGems runs automatically during gem install / bundle install
Targets: Linux and macOS developer machines and CI runners (with a Windows N-API addon embedded) -- installs a credential stealer, a self-propagating worm, a GitHub-controlled backdoor and a conditional wiper
Evasion: three layers of obfuscation and encryption, a locale kill-switch for 11 CIS-region languages, StepSecurity Harden-Runner tampering on GitHub Actions, and a no-op Makefile so the install looks normal
strutil-core first appeared on RubyGems on 2026-11-01 at 02:19 UTC. Its only clean release, v1.0.0, contains a single six-line Ruby module exposing a slugify helper. The gemspec names "Mark Tillman" as author and points its homepage at a GitHub repository that does not exist; the linked GitHub account has no public repositories, no followers and no activity. The package declares no source code repository and has no direct dependents.
That thin footprint is the point. A small, generic-sounding utility gem attracts little scrutiny, and the native-extension mechanism means the attacker never needs anyone to require the library -- installing it is enough.
Attack Analysis
The clean-to-malicious delta is small: v1.0.1 adds three files and changes none -- the same benign strutil_core.rb module, a 41-line extconf.rb, and a 5,475,376-byte index.js.
Stage 1 -- install-time launcher. RubyGems runs any ext/*/extconf.rb at install time to generate a Makefile. strutil-core's has no build logic. On Linux or macOS (x64 or arm64) it downloads the official Bun 1.2.18 release from the oven-sh GitHub project into a per-process /tmp directory, runs index.js with bun run, deletes the runtime, and writes a Makefile whose every target is a no-op so the "build" succeeds. The only network fetch in the Ruby stage goes to a trusted release host; the malicious logic rides inside the gem as JavaScript that Ruby tooling has little reason to inspect.
Stage 2 -- three layers of concealment. Armando statically decoded the payload without executing it. The outer layer is an obfuscator.io string table of 4,069 entries referenced from 15,715 call sites. Beneath it, 743 sensitive strings -- including the C2 hostname -- are protected with a key derived by PBKDF2-SHA256 (200,000 iterations) and three rounds of per-byte substitution. Twenty further components (CI workflows, shell installers, Python and C sources, a Windows PE addon, a Linux ELF and RSA public keys) are gzip-compressed and AES-256-GCM encrypted, with all key material hardcoded in the bundle.
Stage 3 -- credential theft. The payload queries the AWS instance metadata service and signs AWS API requests with stolen keys, and harvests GitHub, GitLab, RubyGems and PyPI tokens -- including minting PyPI trusted-publishing tokens via OIDC -- along with Claude and Codex credential files and SSH known_hosts. On GitHub Actions runners it reads the Runner.Worker process memory through /proc/<pid>/maps and /proc/<pid>/mem, adds a runner ALL=(ALL) NOPASSWD:ALL sudoers entry, tampers with the StepSecurity Harden-Runner agent and its service endpoints, and replaces /etc/resolv.conf with public resolvers 8.8.8.8 and 1.1.1.1. Before doing anything it checks the system locale and exits on Russian, Ukrainian, Belarusian, Kazakh, Armenian, Azerbaijani, Georgian, Kyrgyz, Uzbek, Tajik and Turkmen.
Stage 4 -- exfiltration. Stolen data goes to https://paleasbest.top/v1/api (overridable through a SYNC_ENDPOINT environment variable). As a fallback, the worm creates public GitHub repositories described as project-utils and pushes commits prefixed AutoSyncComplete.
Stage 5 -- propagation. With a stolen RubyGems key the worm enumerates the victim's gems through the RubyGems API, injects its own extconf.rb and payload, and republishes them. It carries a PyPI .pth dropper that is byte-identical to the one shipped in the PyPI package pystr-codec v0.1.2, giving it a ready-made Python infection vector. In repositories it can write to, it plants fake "CodeQL" GitHub workflows and a GitLab cache_sync CI job that dump the job environment into a scan-output artifact.
Stage 6 -- AI coding tool and IDE poisoning. The worm registers an MCP server named workspace-tools, installs a Claude Code SessionStart hook, and writes CLAUDE.md, AGENTS.md, .cursorrules, .cursor/mcp.json and .gemini/settings.json files that instruct coding agents to run .cache/init.js "once at session start" before any other work. A .vscode/tasks.json task set to run on folder open, and the Claude hook, launch .github/init.js. Every AI agent or IDE that trusts the workspace becomes a re-infection trigger.
Stage 7 -- persistence and a dead-man's switch. A Python backdoor, sync.py, is installed as a systemd user service or macOS LaunchAgent named pkg-runtime. Every hour it searches public GitHub commits for the marker msync-3f7a, extracts a signed command URL from the commit message and runs what it points to, keeping state in /var/tmp/.fontconfig_state. Separately, a net-health-checker service stores a stolen GitHub token and checks it against the GitHub user API every 45 seconds; if the token comes back as revoked (HTTP 401 or 403) it runs rm -rf ~/; rm -rf ~/Documents. Its lifetime is 172,800 seconds (48 hours), even though its own log message calls it a "24h TTL". Revoking the stolen token -- the standard incident response step -- is exactly what triggers the deletion.
Malicious Files
extconf.rb is a fake native-extension build script added in v1.0.1 and absent from the clean v1.0.0. It runs automatically at gem install or bundle install on Linux and macOS (x64/arm64): it downloads Bun 1.2.18 from the oven-sh GitHub releases into a per-process temporary directory, executes the bundled ext/strutil-core/index.js with bun run, deletes the Bun directory, and writes a no-op Makefile so the install completes silently. It contains no build logic and has nothing to do with a string-utility gem.
index.js is the 5,475,376-byte worm, launched at install time by extconf.rb. Behind its three concealment layers it implements the credential theft, exfiltration to paleasbest.top, RubyGems and PyPI propagation, CI workflow injection, AI tool poisoning, pkg-runtime backdoor and net-health-checker wiper described above. Notable embedded components include the sync.py backdoor (SHA256 c7ef186aa59b67d9b198a14f6dc8aaee6dd86510ac7c37d28f65d310cd846411), the net-health-checker installer (SHA256 e29a5319888319b51345ab8e3e009938ee5c2b9ae5d6bdd053f4f44c98f309d3) and a Windows N-API addon (SHA256 c78daf27c42c3929758b24c2ef0baf65eea81b26b0fddfe8a5b748672d40d5f5).
Recommendations
Developers and security teams should take the following steps:
Identify exposure: Check whether strutil-core v1.0.1 appears in any Gemfile.lock or installed gem set, including transitive dependencies. Any developer machine or pipeline that ran gem install or bundle install while this version was current should be considered compromised.
Disarm the wiper before revoking tokens: On affected hosts, stop and remove the net-health-checker service (systemd user unit or LaunchAgent) and delete ~/.config/net-health-checker/before revoking GitHub tokens -- revocation triggers home-directory deletion while the monitor is running.
Remove persistence: Remove the pkg-runtime service, ~/.local/share/runtimes/sync.py, /var/tmp/.fontconfig_state, and any workspace-tools MCP entries or unexpected SessionStart hooks in AI tool configurations.
Rotate credentials: Then rotate cloud (AWS, Azure, GCP), GitHub, GitLab, RubyGems, PyPI, npm, Docker, Kubernetes and Vault credentials, plus Claude and Codex tokens reachable from the host.
Audit repositories and registries: Look for unexpected releases of your gems or Python packages, public repositories described as project-utils, commits prefixed AutoSyncComplete, new "CodeQL" workflows, GitLab cache_sync jobs, and newly committed CLAUDE.md, AGENTS.md, .cursorrules, .vscode/tasks.json, .github/init.js or .cache/init.js files.
Scan your software supply chain: Use Spectra Assure Community to check whether packages in your environment have been flagged.