|
| 1 | +## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework |
| 2 | +## |
| 3 | + |
| 4 | +require 'ruby_smb/dcerpc/client' |
| 5 | +require 'pry-byebug' |
| 6 | + |
| 7 | +class MetasploitModule < Msf::Auxiliary |
| 8 | + include Msf::Exploit::Remote::SMB::Client |
| 9 | + include Msf::Exploit::Remote::SMB::Client::Authenticated |
| 10 | + include Msf::Auxiliary::Report |
| 11 | + include Msf::OptionalSession::SMB |
| 12 | + |
| 13 | + def initialize(info = {}) |
| 14 | + super( |
| 15 | + update_info( |
| 16 | + info, |
| 17 | + 'Name' => 'SMB Password Change', |
| 18 | + 'Description' => %q{ |
| 19 | + Change the password of an account using SMB. This provides several different |
| 20 | + APIs, each of which have their respective benefits and drawbacks. |
| 21 | + }, |
| 22 | + 'License' => MSF_LICENSE, |
| 23 | + 'Author' => [ |
| 24 | + 'smashery' |
| 25 | + ], |
| 26 | + 'References' => [ |
| 27 | + ['URL', 'https://github.com/fortra/impacket/blob/master/examples/changepasswd.py'], |
| 28 | + ], |
| 29 | + 'Notes' => { |
| 30 | + 'Reliability' => [], |
| 31 | + 'Stability' => [], |
| 32 | + 'SideEffects' => [ IOC_IN_LOGS ] |
| 33 | + }, |
| 34 | + 'Actions' => [ |
| 35 | + [ 'RESET', { 'Description' => "Reset the target's password without knowing the existing one (requires appropriate permissions)" } ], |
| 36 | + [ 'RESET_NTLM', { 'Description' => "Reset the target's NTLM hash, without knowing the existing password. This will not update kerberos keys." } ], |
| 37 | + [ 'CHANGE', { 'Description' => 'Change the password, knowing the existing one.' } ] |
| 38 | + ], |
| 39 | + 'DefaultAction' => 'RESET' |
| 40 | + ) |
| 41 | + ) |
| 42 | + |
| 43 | + register_options( |
| 44 | + [ |
| 45 | + OptString.new('NEW_PASSWORD', [false, 'The new password to change to', '']), |
| 46 | + OptString.new('TARGET_USER', [false, 'The user to change the password of. If not provided, will change for the account provided in SMBUser', ''], conditions: ['ACTION', 'in', %w[RESET RESET_NTLM]]), |
| 47 | + OptString.new('NEW_NTLM', [false, 'The new NTLM hash to change to', '']) |
| 48 | + ] |
| 49 | + ) |
| 50 | + end |
| 51 | + |
| 52 | + def connect_samr(domain_name, target_user) |
| 53 | + vprint_status('Connecting to Security Account Manager (SAM) Remote Protocol') |
| 54 | + @samr = @tree.open_file(filename: 'samr', write: true, read: true) |
| 55 | + |
| 56 | + vprint_status('Binding to \\samr...') |
| 57 | + @samr.bind(endpoint: RubySMB::Dcerpc::Samr) |
| 58 | + vprint_good('Bound to \\samr') |
| 59 | + end |
| 60 | + |
| 61 | + def connect_samr |
| 62 | + vprint_status('Connecting to Security Account Manager (SAM) Remote Protocol') |
| 63 | + @samr = @tree.open_file(filename: 'samr', write: true, read: true) |
| 64 | + |
| 65 | + vprint_status('Binding to \\samr...') |
| 66 | + @samr.bind(endpoint: RubySMB::Dcerpc::Samr) |
| 67 | + vprint_good('Bound to \\samr') |
| 68 | + end |
| 69 | + |
| 70 | + def run |
| 71 | + case action.name |
| 72 | + when 'CHANGE' |
| 73 | + run_change |
| 74 | + when 'RESET' |
| 75 | + run_reset |
| 76 | + when 'RESET_NTLM' |
| 77 | + run_reset_ntlm |
| 78 | + end |
| 79 | + |
| 80 | + # Don't disconnect the client if it's coming from the session so it can be reused |
| 81 | + unless session |
| 82 | + simple.client.disconnect! if simple&.client.is_a?(RubySMB::Client) |
| 83 | + disconnect |
| 84 | + end |
| 85 | + end |
| 86 | + |
| 87 | + def authenticate(anonymous_on_expired: false) |
| 88 | + if session |
| 89 | + print_status("Using existing session #{session.sid}") |
| 90 | + client = session.client |
| 91 | + self.simple = ::Rex::Proto::SMB::SimpleClient.new(client.dispatcher.tcp_socket, client: client) |
| 92 | + simple.connect("\\\\#{simple.address}\\IPC$") # smb_login connects to this share for some reason and it doesn't work unless we do too |
| 93 | + else |
| 94 | + connect |
| 95 | + begin |
| 96 | + begin |
| 97 | + smb_login |
| 98 | + rescue Rex::Proto::SMB::Exceptions::LoginError => e |
| 99 | + if anonymous_on_expired && |
| 100 | + (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) || |
| 101 | + e.source.is_a?(::WindowsError::ErrorCode) && [::WindowsError::NTStatus::STATUS_PASSWORD_EXPIRED, ::WindowsError::NTStatus::STATUS_PASSWORD_MUST_CHANGE].include?(e.source)) |
| 102 | + # Password has expired - we'll need to anonymous connect |
| 103 | + opts = { |
| 104 | + :username => '', |
| 105 | + :password => '', |
| 106 | + :domain => '', |
| 107 | + :auth_protocol => Msf::Exploit::Remote::AuthOption::NTLM |
| 108 | + } |
| 109 | + disconnect |
| 110 | + connect |
| 111 | + smb_login(opts: opts) |
| 112 | + else |
| 113 | + raise |
| 114 | + end |
| 115 | + end |
| 116 | + |
| 117 | + rescue Rex::Proto::SMB::Exceptions::Error, RubySMB::Error::RubySMBError => e |
| 118 | + fail_with(Module::Failure::NoAccess, "Unable to authenticate ([#{e.class}] #{e}).") |
| 119 | + end |
| 120 | + end |
| 121 | + |
| 122 | + report_service( |
| 123 | + host: simple.address, |
| 124 | + port: simple.port, |
| 125 | + host_name: simple.client.default_name, |
| 126 | + proto: 'tcp', |
| 127 | + name: 'smb', |
| 128 | + info: "Module: #{fullname}, last negotiated version: SMBv#{simple.client.negotiated_smb_version} (dialect = #{simple.client.dialect})" |
| 129 | + ) |
| 130 | + |
| 131 | + begin |
| 132 | + @tree = simple.client.tree_connect("\\\\#{simple.address}\\IPC$") |
| 133 | + rescue RubySMB::Error::RubySMBError => e |
| 134 | + fail_with(Module::Failure::Unreachable, |
| 135 | + "Unable to connect to the remote IPC$ share ([#{e.class}] #{e}).") |
| 136 | + end |
| 137 | + |
| 138 | + connect_samr |
| 139 | + |
| 140 | + end |
| 141 | + |
| 142 | + def run_reset |
| 143 | + authenticate(anonymous_on_expired: false) |
| 144 | + |
| 145 | + @server_handle = @samr.samr_connect |
| 146 | + domain_sid = @samr.samr_lookup_domain(server_handle: @server_handle, name: datastore['SMBDomain']) |
| 147 | + @domain_handle = @samr.samr_open_domain(server_handle: @server_handle, domain_id: domain_sid) |
| 148 | + user_rids = @samr.samr_lookup_names_in_domain(domain_handle: @domain_handle, names: [datastore['TARGET_USER']]) |
| 149 | + rid = user_rids[datastore['TARGET_USER']][:rid] |
| 150 | + user_handle = @samr.samr_open_user(domain_handle: @domain_handle, user_id: rid) |
| 151 | + |
| 152 | + user_info = RubySMB::Dcerpc::Samr::SamprUserInfoBuffer.new( |
| 153 | + tag: RubySMB::Dcerpc::Samr::USER_INTERNAL4_INFORMATION_NEW, |
| 154 | + member: RubySMB::Dcerpc::Samr::SamprUserInternal4InformationNew.new( |
| 155 | + i1: { |
| 156 | + password_expired: 0, |
| 157 | + which_fields: RubySMB::Dcerpc::Samr::USER_ALL_NTPASSWORDPRESENT | RubySMB::Dcerpc::Samr::USER_ALL_PASSWORDEXPIRED |
| 158 | + }, |
| 159 | + user_password: { |
| 160 | + buffer: RubySMB::Dcerpc::Samr::SamprEncryptedUserPasswordNew.encrypt_password( |
| 161 | + datastore['NEW_PASSWORD'], |
| 162 | + simple.client.application_key |
| 163 | + ) |
| 164 | + } |
| 165 | + ) |
| 166 | + ) |
| 167 | + @samr.samr_set_information_user2( |
| 168 | + user_handle: user_handle, |
| 169 | + user_info: user_info |
| 170 | + ) |
| 171 | + end |
| 172 | + |
| 173 | + def run_change |
| 174 | + authenticate(anonymous_on_expired: true) |
| 175 | + |
| 176 | + @samr.samr_unicode_change_password_user2(target_username: datastore['SMBUser'], old_password: datastore['SMBPass'], new_password: datastore['NEW_PASSWORD']) |
| 177 | + |
| 178 | + rescue RubySMB::Error::RubySMBError => e |
| 179 | + fail_with(Module::Failure::UnexpectedReply, "[#{e.class}] #{e}") |
| 180 | + rescue Rex::ConnectionError => e |
| 181 | + fail_with(Module::Failure::Unreachable, "[#{e.class}] #{e}") |
| 182 | + rescue Msf::Exploit::Remote::MsSamr::MsSamrError => e |
| 183 | + fail_with(Module::Failure::BadConfig, "[#{e.class}] #{e}") |
| 184 | + rescue ::StandardError => e |
| 185 | + raise e |
| 186 | + ensure |
| 187 | + @samr.close_handle(@domain_handle) if @domain_handle |
| 188 | + @samr.close_handle(@server_handle) if @server_handle |
| 189 | + @samr.close if @samr |
| 190 | + @tree.disconnect! if @tree |
| 191 | + end |
| 192 | +end |
0 commit comments