From b49949fb3d2014546516378e1afda49b09ee47ea Mon Sep 17 00:00:00 2001 From: "Rph :3" <11350302+rphsoftware@users.noreply.github.com> Date: Mon, 6 Jan 2025 12:21:15 +0100 Subject: [PATCH] iris ctf --- public/bad-todo.tar.gz | Bin 0 -> 4872 bytes src/content/blog/irisctf-2025-bad-todo.md | 78 ++++++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 public/bad-todo.tar.gz create mode 100644 src/content/blog/irisctf-2025-bad-todo.md diff --git a/public/bad-todo.tar.gz b/public/bad-todo.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..0547c63eda84e05eceee3dfc106a00271ec0be96 GIT binary patch literal 4872 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 literal 0 HcmV?d00001 diff --git a/src/content/blog/irisctf-2025-bad-todo.md b/src/content/blog/irisctf-2025-bad-todo.md new file mode 100644 index 0000000..637c84d --- /dev/null +++ b/src/content/blog/irisctf-2025-bad-todo.md @@ -0,0 +1,78 @@ +--- +title: 'IrisCTF 2025 - web/bad-todo - Author Writeup' +description: 'A writeup of the task I prepared for the 2025 edition of IrisCTF' +pubDate: 'Jan 6 2025' +--- + +### 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)](/public/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 ID +- `redirect_uri` - The base URL of the challenge with `/auth_redirect` +- `scope=openid` - Requests only basic permissions +- `response_type=code` +- `state=` - 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 token +- `state` - 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 user +- `grant_type=authorization_code` +- `redirect_uri` - The same redirect URI as we used earlier +- `client_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 the `STORAGE_LOCATION`, not within a specific directory for a given identity provider. This allows for accessing data stored by other users. +- `getStoragePath` relies on `encodeURIComponent` 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 your `sub` 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: + +- [Writeup by ireland.re - Their approach makes use of just one json payload to handle all 3 endpoints, a clever take on this solution](https://ireland.re/posts/irisctf_2025/#webbad-todo-75-solves) +- [Source code of my solution](https://gist.github.com/rphsoftware/bc7a98428fe538131a584e33cfc6e243) \ No newline at end of file