Skip to content

Insufficient XPath allows modified SAML response to bypass 2FA requirement

⚠️ 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 #2851261 by joaxcar on 2024-11-19, assigned to GitLab Team:

Report | Attachments | How To Reproduce

Report

Summary

It is possible to abuse an altered SAML response to bypass 2FA requirements in certain configurations.

After reading about CVE-2024-45409 I decided to have a look at GitLabs internal code for similar patterns when dealing with REXML::XPath in /lib/gitlab/auth/saml/auth_hash.rb where you use a too permissive xpath here

def extract_authn_context(document)  
.   REXML::XPath.first(document, "//*[name()='saml:AuthnStatement' or name()='saml2:AuthnStatement' or name()='AuthnStatement']/*[name()='saml:AuthnContext' or name()='saml2:AuthnContext' or name()='AuthnContext']/*[name()='saml:AuthnContextClassRef' or name()='saml2:AuthnContextClassRef' or name()='AuthnContextClassRef']/text()").to_s  
end  

Essentially the issue is that an xpath starting with // will look for matching nodes anywhere in the given document. While you want this AuthnStatement to only exists inside the returned assertion

The xpath result is used here

def bypass_two_factor?  
    saml_config.upstream_two_factor_authn_contexts&.include?(auth_hash.authn_context)  
end  

This test is used to see if a new session using a SAML login is eligible to bypass 2FA requirements on the instance due to server settings https://docs.gitlab.com/ee/integration/saml.html#bypass-two-factor-authentication

The issue here is that some scenarios allow an attacker to smuggle AuthnStatement in an Extension element in an altered SAML response. This will allow the attacker to completely bypass the 2FA requirement even if no 2FA have been used.

When is this an issue

This is a scenario where an attacker could use this vector:

  • Gitlab instance is configured to enforce 2FA for all users for all sessions docs
  • Gitlab instance has SAML configured as a login option
  • The SAML config has "upstream_two_factor_authn_contexts" configured to allow upstream 2FA count as valid 2FA for a session docs
  • The instance SAML IDP is configured to use signed assertions but not signed responses

The three first ones are normal configurations for a tight SAML implementation following gitlab documentation. The fourth one does make the scenario a bit less likely. I am not familiar enough with SAML to give an opinion on the likelihood of signed response not being enforced.

But given the SAML 2.0 specs (also see this SO answer) only the Assertion is required to be signed, while signing the whole response is optional (and not always possible depending on how the SAML IdP is implemented)

Given that the four requirements should not be seen as unlikely and fall out of using Gitlab per the documentation

Attack flow

As this is a 2FA bypass the impact assumes that the attacker has gotten hold of either victim credentials or a leaked SAML response without 2FA context.

  1. Attacker visits https://gitlab.example.com and picks to sign in using SAML provider
  2. The attacker signs in to the IdP using the leaked credentials
  3. The attacker is redirected back to https://gitlab.example.com and will be asked for a 2FA code
  4. The attacker takes the SAML response that was sent and modifies is to contain an additional AuthnStatement outside the Assertion that claims that 2FA was used
  5. The attacker sends this altered SAML response to https://gitlab.example.com and will now be let in. Without providing any 2FA code

Steps to reproduce

SAML setup

  1. Get hold of a web facing server (a droplet on Digitalocean for example)
  2. SSH to the instance
  3. Install docker. On Digitalocean use these commands
sudo apt update  
sudo apt -y install vim apt-transport-https ca-certificates curl software-properties-common  
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -;  
sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu focal stable"  
apt-cache policy docker-ce  
sudo apt install docker-ce -y  
  1. Install and launch the test SAML server using this command (replace https://gitlab.example.com with an IP or URL to your gitlab instance)
docker run --name=instance_saml_idp -p 8080:8080 -p 8443:8443 \  
  -e SIMPLESAMLPHP_SP_ENTITY_ID=https://gitlab.example.com \  
  -e SIMPLESAMLPHP_SP_ASSERTION_CONSUMER_SERVICE=https://gitlab.example.com/users/auth/saml/callback \  
  -d jamedjo/test-saml-idp  
  1. Get inside the docker container using
docker exec -it instance_saml_idp bash  
  1. Edit the file ./metadata/saml20-idp-hosted.php using vim ./metadata/saml20-idp-hosted.php and add this line inside the list of metadata options
'saml20.sign.response' => false,  
  1. Save and exit the file
    Server setup
  2. Configure SAML by opening the /etc/gitlab/gitlab.rb config file on the host for your server and add this config (replace https://gitlab.example.com with your gitlab instance URL or IP (two places in the snippet) and replace IDP_SERVER_IP with the IP to your new IdP test server from step 4)
gitlab_rails['omniauth_allow_single_sign_on'] = ['saml']  
gitlab_rails['omniauth_block_auto_created_users'] = false  
gitlab_rails['omniauth_providers'] = [  
  {  
    name: "saml",  
    label: "SAML PROVIDER",  
    args: {  
      assertion_consumer_service_url: "https://gitlab.example.com/users/auth/saml/callback",  
      idp_cert_fingerprint: "11:9b:9e:02:79:59:cd:b7:c6:62:cf:d0:75:d9:e2:ef:38:4e:44:5f",  
      idp_sso_target_url: "http://IDP_SERVER_IP:8080/simplesaml/saml2/idp/SSOService.php",  
      issuer: "https://gitlab.example.com",  
      name_identifier_format: "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent",  
      upstream_two_factor_authn_contexts:  
              %w(  
                urn:oasis:names:tc:SAML:2.0:ac:classes:CertificateProtectedTransport  
                urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorOTPSMS  
                urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorIGTOKEN  
              ),  
    }  
  }  
]
  1. save and exit the file
  2. run gitlab-ctl reconfigure and wait for the config to restart the server

First login setting up user
11. Go to your gitlab instance in a browser
12. Click the SAML login option (should look like SAML PROVIDER)
13. On the SAML login page enter username: user1 password: user1pass
14. You should land back on Gitlab getting asked to set up 2FA. Follow the instructions

The attack
15. Open a new browser or just log out the user
16. Go to your gitlab instance and do step 11-13 again, to log in using SAML
17. Now you should land on a 2FA page asking for a 2FA code to be entered
18. Open Dev-tools and refresh the page
19. Go to the network tab of Dev-tools and find a network request to /callback (this should be a POST request containing a SAML response)
20. Right click the network request and click "copy as cURL"
21. Open a terminal, create a file called curl.txt and paste the curl command in there. Save and exit
22. Download the python script here attack.py
23. Run the script like this

cat curl.txt | python3 attack.py  
  1. The script should output a cookie like this
< set-cookie: _gitlab_session=08cf5644862b14999a46a93b1a326f86; path=/; secure; HttpOnly; SameSite=None  
  1. Copy the _gitlab_session value
  2. Go back to the browser and open the Application tab in Dev-tools, find the cookie _gitlab_session and replace the value with this new value
  3. Now visit the main gitlab domain, eg. https://gitlab.example.com
  4. You should now be logged in without using 2FA
Python script

The python script will essentially just take the copied SAML response, find the real AuthnStatement in the Assertion and then copy it into a new block looking like this

<saml2p:Extensions>  
<saml2:AuthnStatement AuthnInstant="2024-11-15T11:39:03.234Z" SessionIndex="_7b28935e-c096-41ad-a229-6ac76ab23d98" xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">  
<saml2:AuthnContext>  
<saml2:AuthnContextClassRef>  
urn:oasis:names:tc:SAML:2.0:ac:classes:CertificateProtectedTransport  
</saml2:AuthnContextClassRef>  
</saml2:AuthnContext>  
</saml2:AuthnStatement>  
</saml2p:Extensions>  

This fake AuthnStatement will now be the one that is returned by the XPath query and will tell GitLab that CertificateProtectedTransport was used even if it was not. This will now bypass 2FA

Video

Here is a video of the flow of the attack. In the video I have already logged in to the IdP and thus It will not ask for my username and password. Also I use pbpaste to get my clipboard into the script, this works on MAC but on Linux you need to put the content in a file first

poc1.mov

Impact

Bypassing 2FA using modified SAML response. Allows an attacker to bypass 2FA even if 2FA is required on all sessions on the instance. Even if the attacker needs to first have access to either a SAML response or the credentials of a victim the impact here is high as the whole idea of 2FA is to protect accounts assuming breach of credentials.

What is the current bug behavior?

The XPath that checks for 2FA will hit the first match in the response and not the value inside the signed Assertion

What is the expected correct behavior?

The XPath needs to hit the AuthnStatement inside the signed Assertion as the Assertion is the only part of the Response that is mandatory to sign.

Output of checks

Results of GitLab environment info
System information  
System:		Ubuntu 24.10  
Proxy:		no  
Current User:	git  
Using RVM:	no  
Ruby Version:	3.2.5  
Gem Version:	3.5.17  
Bundler Version:2.5.11  
Rake Version:	13.0.6  
Redis Version:	7.0.15  
Sidekiq Version:7.2.4  
Go Version:	unknown

GitLab information  
Version:	17.5.0-ee  
Revision:	173959793c1  
Directory:	/opt/gitlab/embedded/service/gitlab-rails  
DB Adapter:	PostgreSQL  
DB Version:	14.11  
URL:		https://gitlab2.j15.se  
HTTP Clone URL:	https://gitlab2.j15.se/some-group/some-project.git  
SSH Clone URL:	git@gitlab2.j15.se:some-group/some-project.git  
Elasticsearch:	no  
Geo:		no  
Using LDAP:	no  
Using Omniauth:	yes  
Omniauth Providers: saml

GitLab Shell  
Version:	14.39.0  
Repository storages:  
- default: 	unix:/var/opt/gitlab/gitaly/gitaly.socket  
GitLab Shell path:		/opt/gitlab/embedded/service/gitlab-shell

Gitaly  
- default Address: 	unix:/var/opt/gitlab/gitaly/gitaly.socket  
- default Version: 	17.5.0  
- default Git Version: 	2.46.2  

Impact

Bypassing 2FA using modified SAML response. Allows an attacker to bypass 2FA even if 2FA is required on all sessions on the instance. Even if the attacker needs to first have access to either a SAML response or the credentials of a victim the impact here is high as the whole idea of 2FA is to protect accounts assuming breach of credentials.

Attachments

Warning: Attachments received through HackerOne, please exercise caution!

How To Reproduce

Please add reproducibility information to this section: