Smartcard authentication allows impersonation of arbitrary user using user's public certificate
Summary
Knowing the public certificate of any user is enough to impersonate him, simply by sending unauthenticated request to https://gitlab-instance.example.com/-/smartcard/verify_certificate?client_certificate=[URL-encoded version of certificate]
(at least on our instance).
Steps to reproduce
For the attacker :
- grab the public certificate of an existing user (internal directory, OSINT, logs, phishing, ...)
- encode it in PEM, replace newlines by spaces, urlencode
- visit
https://gitlab-instance.example.com/-/smartcard/verify_certificate?client_certificate=[URL-encoded version of certificate]
- you're authenticated as this user
For testing, you first have to :
- spin up a PKI, start a gitlab (EE) instance
- activate smartcard authentication giving it the CA certificate
- generate a user certificate with
/CN=userlogin/emailAddress=user@example.com
. - You don't need the private key to authenticate to your gitlab instance with above hint.
Helper function to urlencode the certificate :
import urllib
def verify_certificate_url(certificate):
return "https://gitlab-instance.example.com/-/smartcard/verify_certificate?" + urllib.parse.urlencode({"client_certificate":certificate.replace("\n", " ")})
print(verify_certificate_url("""-----BEGIN CERTIFICATE-----
MII...
-----END CERTIFICATE-----"""))
Example Project
N.A.
What is the current bug behavior?
Authentication flow for a legitimate user :
1. POST https://gitlab-instance.example.com/-/smartcard/auth -> 302 location: https://smartcard.gitlab-instance.example.com/-/smartcard/extract_certificate + Set-Cookie: _gitlab_session=ABC
2. GET https://smartcard.gitlab-instance.example.com/-/smartcard/extract_certificate -> 421 location: https://gitlab-instance.example.com/-/smartcard/verify_certificate?client_certificate=[encoded certificate]
3. GET https://gitlab-instance.example.com/-/smartcard/verify_certificate?client_certificate=[encoded certificate] -> 302 location: https://gitlab-instance.example.com/ + Set-Cookie: _gitlab_session=XYZ
4. GET https://gitlab-instance.example.com/ and now I'm authenticated.
The attacker can start at step 3 with the public certificate of another user, and authentication still work. It should not.
What is the expected correct behavior?
Failure of request with 401 code.
Relevant logs and/or screenshots
(none)
Output of checks
this bug does not happen on gitlab.com
Results of GitLab environment info
Expand for output related to GitLab environment info
System information System: Debian 11 Proxy: no Current User: git Using RVM: no Ruby Version: 3.0.6p216 Gem Version: 3.4.14 Bundler Version:2.4.16 Rake Version: 13.0.6 Redis Version: 7.0.12 Sidekiq Version:6.5.7 Go Version: unknown GitLab information Version: 16.2.3-ee Revision: 7ac2b0a343c Directory: /opt/gitlab/embedded/service/gitlab-rails DB Adapter: PostgreSQL DB Version: 13.11 URL: https:// - redacted - HTTP Clone URL: https:// - redacted -/some-group/some-project.git SSH Clone URL: git@ - redacted -:some-group/some-project.git Elasticsearch: no Geo: no Using LDAP: no Using Omniauth: yes Omniauth Providers: GitLab Shell Version: 14.23.0 Repository storages: - default: unix:/var/opt/gitlab/gitaly/gitaly.socket GitLab Shell path: /opt/gitlab/embedded/service/gitlab-shell
gitlab.rb snippet :
gitlab_rails['smartcard_enabled'] = true
gitlab_rails['smartcard_ca_file'] = "/etc/gitlab/ssl/my-internal-CA.crt"
gitlab_rails['smartcard_client_certificate_required_host'] = "secure.gitlab-instance.example.com"
gitlab_rails['smartcard_client_certificate_required_port'] = 443
Results of GitLab application Check
Expand for output related to the GitLab application check
Checking GitLab subtasks ...Checking GitLab Shell ...
GitLab Shell: ... GitLab Shell version >= 14.23.0 ? ... OK (14.23.0) Running /opt/gitlab/embedded/service/gitlab-shell/bin/check Internal API available: OK Redis available via internal API: OK gitlab-shell self-check successful
Checking GitLab Shell ... Finished
Checking Gitaly ...
Gitaly: ... default ... OK
Checking Gitaly ... Finished
Checking Sidekiq ...
Sidekiq: ... Running? ... yes Number of Sidekiq processes (cluster/worker) ... 1/1
Checking Sidekiq ... Finished
Checking Incoming Email ...
Incoming Email: ... Reply by email is disabled in config/gitlab.yml
Checking Incoming Email ... Finished
Checking LDAP ...
LDAP: ... LDAP is disabled in config/gitlab.yml
Checking LDAP ... Finished
Checking GitLab App ...
Database config exists? ... yes All migrations up? ... yes Database contains orphaned GroupMembers? ... no GitLab config exists? ... yes GitLab config up to date? ... yes Cable config exists? ... yes Resque config exists? ... yes Log directory writable? ... yes Tmp directory writable? ... yes Uploads directory exists? ... yes Uploads directory has correct permissions? ... yes Uploads directory tmp has correct permissions? ... yes Systemd unit files or init script exist? ... skipped (omnibus-gitlab has neither init script nor systemd units) Systemd unit files or init script up-to-date? ... skipped (omnibus-gitlab has neither init script nor systemd units) Projects have namespace: ... 8/3 ... yes 9/4 ... yes 10/5 ... yes 13/6 ... yes 11/7 ... yes 11/8 ... yes 11/9 ... yes 11/10 ... yes 11/11 ... yes 11/12 ... yes 11/13 ... yes 11/14 ... yes 18/15 ... yes 18/16 ... yes 18/17 ... yes 21/18 ... yes 23/19 ... yes 22/20 ... yes 20/22 ... yes 20/23 ... yes 26/24 ... yes 26/25 ... yes 26/26 ... yes 26/27 ... yes 25/28 ... yes 25/29 ... yes 25/30 ... yes 110/31 ... yes 112/32 ... yes 35/33 ... yes 12/34 ... yes 12/35 ... yes 12/36 ... yes 12/37 ... yes 12/38 ... yes 35/39 ... yes 19/40 ... yes 15/41 ... yes 15/42 ... yes 15/43 ... yes 15/44 ... yes 15/45 ... yes 15/46 ... yes 15/47 ... yes 15/48 ... yes 15/49 ... yes 15/50 ... yes 15/51 ... yes 15/52 ... yes 15/53 ... yes 15/54 ... yes 15/55 ... yes 15/56 ... yes 35/57 ... yes 16/58 ... yes 143/59 ... yes 17/60 ... yes 29/61 ... yes 29/62 ... yes 29/63 ... yes 35/64 ... yes 16/65 ... yes 154/66 ... yes 153/67 ... yes 153/68 ... yes 153/69 ... yes 14/70 ... yes 16/71 ... yes 318/72 ... yes 16/73 ... yes 36/74 ... yes 177/76 ... yes 167/77 ... yes 167/78 ... yes 177/79 ... yes 17/80 ... yes 17/81 ... yes 17/82 ... yes 17/83 ... yes 17/84 ... yes 17/85 ... yes 17/86 ... yes 34/87 ... yes 34/88 ... yes 181/89 ... yes 32/90 ... yes 32/91 ... yes 16/92 ... yes 33/93 ... yes 28/94 ... yes 28/95 ... yes 109/96 ... yes 36/97 ... yes 199/98 ... yes 19/99 ... yes 12/100 ... yes 19/101 ... yes 220/102 ... yes 222/103 ... yes 226/104 ... yes 226/105 ... yes 19/106 ... yes 307/108 ... yes 235/109 ... yes 12/111 ... yes 240/112 ... yes 28/113 ... yes 243/114 ... yes 10/117 ... yes 251/118 ... yes 29/119 ... yes 254/120 ... yes 243/121 ... yes 258/122 ... yes 240/123 ... yes 143/124 ... yes 35/125 ... yes 219/126 ... yes 219/127 ... yes 35/129 ... yes 219/130 ... yes 235/131 ... yes 7/132 ... yes 35/133 ... yes 277/134 ... yes 15/135 ... yes 282/136 ... yes 284/137 ... yes 226/138 ... yes 284/140 ... yes 29/141 ... yes 291/142 ... yes 291/143 ... yes 291/144 ... yes 291/145 ... yes 296/146 ... yes 298/147 ... yes 302/148 ... yes 307/149 ... yes 307/150 ... yes 310/151 ... yes 322/152 ... yes 315/154 ... yes 7/155 ... yes 321/156 ... yes 28/157 ... yes 326/159 ... yes 28/160 ... yes 143/161 ... yes 409/162 ... yes 346/165 ... yes 348/166 ... yes 12/167 ... yes 39/168 ... yes 39/169 ... yes 35/170 ... yes 39/171 ... yes 358/172 ... yes 307/174 ... yes 181/175 ... yes 368/177 ... yes 44/178 ... yes 44/179 ... yes 368/180 ... yes 368/181 ... yes 375/182 ... yes 375/183 ... yes 368/184 ... yes 7/185 ... yes 380/186 ... yes 368/187 ... yes 384/188 ... yes 254/189 ... yes 387/190 ... yes 15/191 ... yes 15/192 ... yes 302/193 ... yes 251/194 ... yes 7/195 ... yes 284/196 ... yes 402/197 ... yes 315/198 ... yes 28/199 ... yes 413/200 ... yes 254/201 ... yes 300/202 ... yes 413/203 ... yes 375/204 ... yes 413/205 ... yes 29/206 ... yes 300/207 ... yes Redis version >= 6.0.0? ... yes Ruby version >= 2.7.2 ? ... yes (3.0.6) Git user has default SSH configuration? ... yes Active users: ... 31 Is authorized keys file accessible? ... yes GitLab configured to store new projects in hashed storage? ... yes All projects are in hashed storage? ... yes Elasticsearch version 7.x-8.x or OpenSearch version 1.x ... skipped (Advanced Search is disabled) All migrations must be finished before doing a major upgrade ... skipped (Advanced Search is disabled)
Checking GitLab App ... Finished
Checking GitLab subtasks ... Finished
Possible fixes
Original proposal
The server should have an way to bind the second and the third request in authentication flow, for example by signing a challenge sent to the user at step 1 with a private key held on the server (it could be an ephemeral key, defined at the application startup and destroyed when the application closes).
Updated proposal
Encrypt the client certificate when redirecting from smartcard.self-hosted.com/extract_certificate
to self-hosted.com/verify_certificate
How does the fix work?
The steps of smartcard auth flow:
- user loads the sign-in page and selects smartcard authentication
- user is redirected to
smartcard.self-hosted.com/extract_certificate
, where client certificate is requested - user is redirected back to
self-hosted.com/verify_certificate
withclient_certificate
in the param.
Step 2 relies on NGINX and OpenSSL to get the client certificate from the connection. This endpoint:
- do a challenge and response validating that the caller (or attacker) has possession of the private key material
- validate that the public certificate hasn't been spoofed / manipulated and is signed with the CA configured in GitLab
In step 3:
- previously, an attacker could tamper with the
client_certificate
parameter as it is passed in plaintext - with the fix in place, the parameter is encrypted, preventing the manipulation of the parameter.
Credits
Issue found by Lucas Serrano from PEReN (@LSerranoPEReN).