Challenge Description
We built an in-house To Do list for our employees using state-of-the-art OpenID Connect authentication. Our admin recently got hacked and all their to-dos got removed. Thankfully, we have an older manual backup of their extremely valuable account. Can you help us recover that backup?
Download handout (bad-todo.tar.gz)
High level overview of the steps
- Implement an OIDC provider
- Verify that logging in with it works
- Exploit the vulnerability
- Get the flag
A note about safe-fetch
The challenge uses a wrapper over fetch
called safe_fetch, which enforces every URL passed be
- on a valid domain
- not in a private IP range
- provided over HTTPS
The primary purpose of that is to avoid any potential, unintended SSRF, as well as better simulate a more realistic environment.
So, about that OIDC
OpenID Connect is a very complex and convoluted protocol. For the sake of this challenge, the implementation used by the bad-todo app is the simplest possible implementation of OIDC possible. Because of that, only rudimentary verification of input data is performed. As such, only the following 4 fields in the /.well-known/openid-configuration
are necessary:
issuer
authorization_endpoint
token_endpoint
userinfo_endpoint
If we provide a web server hosting this configuration as the authorization server, a session cookie is set and we are then redirected to the authorization_endpoint
with the following parameters:
client_id
- The user-supplied OIDC client IDredirect_uri
- The base URL of the challenge with/auth_redirect
scope=openid
- Requests only basic permissionsresponse_type=code
state=<random string>
- In real implementations, this should be a per-auth-session string, but for simplicity’s sake I just used the value of the session cookie. The purpose of this parameter is to avoid replay attacks and CSRF attacks.
In a real identity provider, this endpoint would present the user with an authentication prompt, request consent and do internal sanity checks. Once that’s done, the identity provider redirects the user to /auth_redirect
with 2 relevant fields in the query:
code
- The authentication code used to obtain a login tokenstate
- That same state string as above
The code is then used to obtain a token against token_endpoint
. Because this is a “public client”, we don’t need any extra authentication against the token endpoint. We just send it
code
- The code we got from the usergrant_type=authorization_code
redirect_uri
- The same redirect URI as we used earlierclient_id
- The client ID we passed at the start
This gives us a response with an access_token
and token_type
which we then use to authenticate to the userinfo endpoint. The response structure of the userinfo is basically undocumented, with each IDP having slightly different response fields, but one field remains the same and proper across all of them: sub
. The sub
is the IDP-specific user ID and can be an arbitrary string. Once we receive the userinfo, we set a few cookie values to identify the user as logged in, and redirect them back to the homepage of the app.
The vulnerability
This to-do list app makes use of a “database-per-user” architecture, an approach popular with apps that are built on SQLite. In that approach, every user has their own on-disk database. This is implemented in storage.js
in the challenge handout.
The base vulnerability is insufficient validation and sanitization of the sub
for naming a file on disk. This vulnerability has 2 parts to it:
sanitizePath
only checks if the path is within theSTORAGE_LOCATION
, not within a specific directory for a given identity provider. This allows for accessing data stored by other users.getStoragePath
relies onencodeURIComponent
to remove any slashes from the user-controlled input, but also splits it (for storage optimization reasons - you don’t want all files in one directory). This allows you to set the first 2 characters of yoursub
to..
and get a relative path.
Therefore, by setting our sub to ..flag
, we can access the “backed up” database of the admin user. Doing so allows us to log in and read the admin’s todo, and therefore our flag: irisctf{per_tenant_databases_are_a_cool_concept_indeed}
Final notes and further reading
Due to the nature of this app, the admin’s database has to be set as readonly, otherwise the first team to log in could have vandalized it. Therefore, if you try to take any action as the admin, you will be met with SQLite errors.
External links: