All versions of RubyGems are vulnerable to Persistent Code Injection via the gemspecs, which RubyGems generates when installing a Gem.
github.com/rubygems/rubygems/pull/165
When building a .gem
file, RubyGems will load your pure-Ruby gemspec and YAML.dump
the gemspec object. The YAML-dump gemspec is then compressed and included into the .gem
file. Upon installing a Gem, RubyGems will extract the YAML-dumped gemspec, YAML.load
the gemspec and then build a new pure-Ruby representation of the attributes within Gem::Specification
. RubyGems installs this re-generated pure-Ruby gemspec into the specifications/
directory within the GEM_HOME
.
The rational for dumping the gemspec to YAML, then building another Ruby gemspec file from the dumped YAML, is that eval()
ing Ruby is faster than calling YAML.load
. Since RubyGems loads every gemspec at start-up, being fast matters.
RubyGems builds this pure-Ruby gemspec using the to_ruby method. to_ruby
merely concatenates Ruby code into a big String, and embeds the data from the Gem::Specification
. to_ruby
relies on the ruby_code method, for wrap the gemspec data, so that they can be safely embedded into Ruby code.
Unfortunately, the ruby_code
method naively wraps Strings in %q{
}
, and performs no character-escaping. Security connoisseurs will immediately recognize this mistake as the same one which makes SQL Injection possible.
To exploit this bug, one simply needs to place a };
in a Gem::Specification
field (summary
is a good hiding spot) to escape the %q{
, then add the malicious Ruby code, and ignore the trailing }
with a #
comment.
s.summary = "A Ruby API for TF2. }; puts "Geeeeentlemen" #"
gem install rubygems-pwn
As far as I can tell, the ruby_code
method was introduced around RubyGems 0.8.0. All previous versions of RubyGems also appear to be vulnerable, since they directly inline the Gem::Specification
attributes in to_ruby
:
def to_ruby mark_version result = "Gem::Specification.new do |s|\n" result << "s.name = %q{#{name}}\n" result << "s.version = %q{#{version}}\n" result << "s.platform = %q{#{platform}}\n" if @platform result << "s.has_rdoc = #{has_rdoc?}\n" if has_rdoc? result << "s.summary = %q{#{summary}}\n"
-
Gem::Specification#to_ruby
method from RubyGems 0.2.0.
Of course, there is some user-interaction required, a user must be enticed into installing a new Gem. Once installed, the injected code is persistent since RubyGems will load all gemspecs during start-up. The injected code will also survive gem pristine
, which re-generates all installed gemspecs.
github.com/rubygems/rubygems/pull/165
The fix for this bug is rather simple, the ruby_code
method should call String#dump
or String#inspect
instead of naively wrapping the Strings in %q{ }
.
A more longeterm solution, would be for RubyGems to not store static-data as Ruby Code, but instead use a data format (YAML, XML, CSV, flat-file).