Insufficient XPath allows modified SAML response to bypass 2FA requirement
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.
- Attacker visits
https://gitlab.example.com
and picks to sign in using SAML provider - The attacker signs in to the IdP using the leaked credentials
- The attacker is redirected back to
https://gitlab.example.com
and will be asked for a 2FA code - The attacker takes the SAML response that was sent and modifies is to contain an additional
AuthnStatement
outside theAssertion
that claims that 2FA was used - 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
- Get hold of a web facing server (a droplet on Digitalocean for example)
- SSH to the instance
- 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
- 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
- Get inside the docker container using
docker exec -it instance_saml_idp bash
- Edit the file
./metadata/saml20-idp-hosted.php
usingvim ./metadata/saml20-idp-hosted.php
and add this line inside the list of metadata options
'saml20.sign.response' => false,
- Save and exit the file
Server setup - Configure SAML by opening the
/etc/gitlab/gitlab.rb
config file on the host for your server and add this config (replacehttps://gitlab.example.com
with your gitlab instance URL or IP (two places in the snippet) and replaceIDP_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
),
}
}
]
- save and exit the file
- 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
23. Run the script like this
cat curl.txt | python3 attack.py
- The script should output a cookie like this
< set-cookie: _gitlab_session=08cf5644862b14999a46a93b1a326f86; path=/; secure; HttpOnly; SameSite=None
- Copy the
_gitlab_session
value - 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 - Now visit the main gitlab domain, eg.
https://gitlab.example.com
- 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
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: