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:
issuerauthorization_endpointtoken_endpointuserinfo_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_redirectscope=openid- Requests only basic permissionsresponse_type=codestate=<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_coderedirect_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:
sanitizePathonly 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.getStoragePathrelies onencodeURIComponentto 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 yoursubto..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: