42 Gems in One Hour, and a Root CA for 30 Exchanges

On 5 October 2026, a RubyGems account named reqthrottle_3474 — profile display name “Ghost Dev” — published 42 malicious gems in roughly one hour. Two independent analyses landed the same day: SafeDep’s writeup by Kunal Singh, and an OpenSourceMalware report tagged #rubygems-btc-shell. They agree on the account, the payload file, the command-and-control host, and the delay. Where they differ is instructive, and we will come to that.

The naming strategy is two-layered. Twenty-six gems are plausible-sounding crypto developer utilities that never existed — eth-keystore-utils, bip39-wordlist-utils, btc-wallet-tools, hdkey-derive-helper, lightning-invoice-utils, merkle-proof-lite. The last sixteen are typosquats of real gems: bitcion, bitciin, bitcoij-ruby, etheremu.rb, etherdum.rb, kecack, keccka, solaan-ruby, and three mutations each of lightning-invoice and crypto-toolbox. Four more pose as rate-limiting middleware (req-throttle-mini, throttle-requests, rate-limit-mini, request-guard), which is where the account name comes from and what the malicious extension directory is called: ext/req_throttle_mini/.

The install hook nobody audits

Ruby has an install-time execution path that gets far less scrutiny than npm’s preinstall and postinstall, and it is not optional in the same way. A gemspec may declare a native extension; during gem install or bundle install, RubyGems runs that extension’s extconf.rb through the Ruby interpreter to produce a Makefile. All 42 gems declare one:

extensions:
- ext/req_throttle_mini/extconf.rb

The file ends with ordinary-looking mkmf and create_makefile calls, so the build succeeds and the install completes without a visible error. Everything above those lines is the attack. This is the same trust boundary we described in the Angular typosquat wave and the DirtyBlanket Express worm: adding the dependency is the exploit. No import, no code path reached, no build step of your own. And unlike npm, where --ignore-scripts disables the class wholesale, a Ruby native extension is a legitimate and common thing for a gem to have.

It checks whether you are worth attacking

What makes this campaign more than ordinary squatting is the gating. Per OpenSourceMalware’s analysis, before doing anything the payload tests whether it is running in CI, in a sandbox, in a directory that looks like analysis infrastructure, or under a generated-looking test username. If any of those match, it does nothing. It then looks for the opposite signal — evidence of a real workstation: SSH keys, a Git configuration, npm credentials, RubyGems credentials, Bundler data. Only if that passes does it fork into the background, detach from the terminal, and sleep for 20 to 40 minutes.

Three consequences follow, and they are the practical story:

  • Your scanner is the intended audience for the silence. A CI-based dependency check, a sandboxed install in a container, or an automated malware pipeline sees a gem that builds cleanly and exits. Benign-by-observation is now a deliberate output, not an absence of evidence. SafeDep states this plainly: the gems do nothing in CI or in a sandbox.
  • The delay breaks causal attribution. By the time a reverse shell opens, the developer has installed other things, run a test suite, switched branches. Nothing in the process tree points back at bundle install.
  • The workstation checks are also a value filter. A machine with SSH keys, Git config and registry credentials is a machine worth a shell. The detection logic and the targeting logic are the same code.

Two variants, one C2

Eleven gems carry a base64-encoded reverse shell. It connects to 45.138.12[.]177, attaches the socket to /bin/sh, and retries every 30 seconds on failure. Here the two reports diverge on a detail worth recording rather than smoothing over: SafeDep lists ports 8089 and 8090, OpenSourceMalware describes port 8090 only. Either both ports are in use across the eleven samples or one report generalised from a subset; we did not obtain the gem artefacts to adjudicate, and we are not going to guess. Block both.

The other thirty-one are downloaders. The payload stores its URL as a hex string XOR-encoded with a four-byte key, which both reports publish in decoded form; it resolves to hxxp://45.138.12[.]177:8092/wgkit.tar.gz. The fetch is a plain curl into /tmp/.w1.tgz, extracted to /tmp/.w1/, with wg_install.sh executed under bash and the files deleted afterward.

Two environment variables in that code are the tell of a working operator rather than a copy-paste kit: WG_KIT_URL overrides the download URL, and WG_FAST cuts the sleep from 20–40 minutes to two seconds. Those exist so the author can test the malware without waiting half an hour. They are also, usefully, high-fidelity detection strings.

The second stage is a TLS interception kit

SafeDep states that the contents of wg_install.sh are not known to them. OpenSourceMalware unpacked the archive and published its file listing, and this is the part that reframes the campaign. The archive contains inject_proxy.py, a local HTTP/HTTPS interception proxy bound to 127.0.0.1:8899; _grab.py, which collects wallet material, scans user files and monitors the clipboard; a browser extension (manifest.json, content.js, background.js); a root certificate, wg-ca.crt; and a tls/ directory holding roughly thirty per-host certificates.

That certificate list is the design document. It includes binance.com, coinbase.com, kraken.com, bybit.com, okx.com, kucoin.com, bitfinex.com, gate.io, gemini.com, crypto.com, upbit.com, bitget.com, htx.com, mexc.com, lbank.com, bitstamp.net, bitbank.cc, coinone.co.kr, bit2me.com, bitoasis.net, several smaller venues, plus localhost and 127.0.0.1. Install the root CA into the local trust store, point traffic at the local proxy, and the browser shows a valid padlock on an exchange page whose withdrawal address has been rewritten in flight. The stated objective is exactly that: swap withdrawal addresses while the victim-facing page looks untouched, and sweep seed phrases, private keys and wallet files on the side.

So the gem is not the malware. The gem is a delivery mechanism for a man-in-the-middle appliance that happens to run on the victim’s own machine, and the trust it needs is granted by a certificate it installs itself.

What we verified ourselves, and what it means

We queried RubyGems.org directly. The gem APIs for bitcion, req-throttle-mini, eth-keystore-utils, kecack and wallet-backup-tool all return HTTP 404; the owner API for reqthrottle_3474 returns an empty array. The public profile for the account is still live and reports Total gems: 0 alongside Total downloads: 7,295.

Those two numbers next to each other are the whole incident-response question. The gems are gone, which closes new installs. The download counter does not reset on yank, and RubyGems does not break it out per gem for removed packages. We therefore cannot say how many of those 7,295 were human developers versus mirrors, registry crawlers, security scanners and the researchers who analysed the campaign — and given that two vendors plus an automated detection engine pulled these samples the same day, a meaningful share is certainly automated. What we will not do is round it down to zero. A campaign that deliberately refuses to run in CI, waits half an hour, and ships exchange certificates was not built to harvest crawler traffic.

What to do

  • Search for the indicators, not the names. Grep lockfiles, Gemfile.lock, shell history, and host telemetry for 45.138.12.177, wgkit.tar.gz, /tmp/.w1, wg_install.sh, WG_KIT_URL, WG_FAST and the extension path ext/req_throttle_mini/extconf.rb. Names are disposable; these are not.
  • Audit your local trust store, now. This is the step most teams will skip. Enumerate non-standard root certificates on developer machines and look for anything resembling wg-ca.crt, plus any process listening on 127.0.0.1:8899. A rogue root CA survives removing the gem.
  • Treat a reverse-shell hit as full credential compromise. SafeDep’s guidance is correct and worth repeating: if a workstation installed one of these, the machine and every credential on it are compromised. Rotate SSH keys, registry tokens, cloud keys and — because the kit explicitly hunts them — move any wallet seed material to hardware that never touched the host.
  • Stop treating a clean sandbox result as a verdict. Environment-aware malware inverts the meaning of “nothing happened.” Pair dynamic analysis with static review of install-time hooks, and flag gems that declare native extensions they have no plausible need for — a BIP-39 wordlist library does not require a C compiler.
  • For agent and coding-agent fleets, assume resolution is execution. An agent that installs a gem it inferred from a README or an error message runs extconf.rb with the agent’s privileges on a host that, by construction, has SSH keys and registry credentials sitting right where the payload looks for them. That is the same install-time boundary behind GitSpawn’s RCE across seven coding agents, and these gates were written for exactly the machine an agent runs on.

Verification note: we read the SafeDep analysis (Kunal Singh, 5 October 2026) and the OpenSourceMalware report (5 October 2026) in full, taking the account name, gem list and versions, the extconf.rb extension path, the decoded downloader payload, the C2 address, the 20–40 minute delay, the environment-check behaviour and the second-stage archive listing from those two primary analyses. We independently queried the RubyGems.org API for five of the 42 gem names (all HTTP 404), the owner API for reqthrottle_3474 (empty), and the account’s public profile page, from which the “Ghost Dev” display name, 0 gems and 7,295 total downloads are taken directly. We did not download, unpack or execute any gem, the wgkit.tar.gz archive, or any payload, and we make no claim beyond the published analyses about what the second stage does at runtime. The port discrepancy between the two reports (8089/8090 versus 8090) is reproduced as found and not resolved. Defanged indicators are written in bracket notation deliberately.

Sources: