Writeup - Bitdizzle (ASVCTF 2022)
ASVCTF 2022 - Bitdizzle Writeup
- Type - Web
- Name - Bitdizzle
- Points - 300
Description
1 | A popular journal writing app just came out. Can you read admin's journal? |
Writeup
This solution chained together multiple exploits, including CSRF, XSS, and an open redirect to steal another user’s contents! I was lucky enough to have a teammate who was able to help me chain all of these together correctly because I struggled for quite a while with the CSRF vulnerability. Note that the goal is to read the journal of the admin
account, which contains the flag.
Since they gave us the main code snippets for both the accounts
and journal
subdomains of bitdizzle.xyz, that meant source code review to check for bugs that we can exploit.
Authentication
The first thing to understand is how authentication works on this site - accessing any page without authentication will redirect you to the home page of accounts.bitdizzle.xyz, where you log in simply by entering a username (no password). Also note that you can’t sign in with the admin
account name. Once you sign in, there are two options presented - generate an OAuth token that allows you to sign into journal.bitdizzle.xyz, or send a link to the admin to visit.
To generate an OAuth token for the account logged in, a GET request is made to the /oauth_authorize
endpoint with two parameters - client_id
(a static, site-wide code) and redirect_uri
, which must start with https://journal.bitdizzle.xyz/
. The default value for redirect_uri
is https://journal.bitdizzle.xyz/oauth_callback
. The server will verify both the client_id
and redirect_uri
, then generate a code attached to the account logged in, append it as a parameter to the redirect_uri
, and automatically redirect the user to the redirect_uri
.
To be authenticated on journal.bitdizzle.xyz, the /oauth_callback
endpoint must be accessed and a code provided. The journal server will then send a request to https://accounts.bitdizzle.xyz/oauth_token
, and the accounts server will respond with what username (if any) is associated with the provided code.
To exploit the authentication system, all we need is an OAuth token for the admin
user. The easiest way to do that is send the admin bot to the following URL - https://accounts.bitdizzle.xyz/oauth_authorize?client_id=f45735d5a3b056b6&redirect_uri=https%3A%2F%2Fattacker.controller.domain%2Fadmin_oauth_token
. This would make the admin user automatically create an OAuth code to sign in and then send the admin to whatever URL we determine. If the URL is attacker-controlled (meaning we can see requests made to it), then we can capture the OAuth code and send it to the journal server, allowing us to be signed in as the admin user and see all the journal entries.
Now, we run into a problem - a check is being done on the redirect_uri
parameter that ensures the site starts with https://journal.bitdizzle.xyz/
. If it only checked https://journal.bitdizzle.xyz
(no /
), then we could insert a URL like https://journal.bitdizzle.xyz@attacker.controlled.domain/oauth_token
, and sent that link off. However, the extra /
means the domain name is locked in, so we can’t send it to our own domain. However, if we can somehow gain control of any page on the journals domain, we can steal the token.
XSS
The last needed bug in the code was a stored XSS vulnerability. On line 44 of journal.py, the note content is pasted directly into the HTML (a <script>
tag specifically, which is already bad practice) as JSON, but without any filtering. This meant that creating a note like </script><script>alert('Malicious JS here')</script>
would allow us to run any JavaScript code we wanted on the main page.
Review of Chained Exploits
- Open redirect - line 63 of accounts.py, we can send the OAuth token to any webpage on journal.bitdizzle.xyz
- CSRF - no CSRF tokens are in place + most of the parameters are GET parameters, meaning anyone clicking a malicious URL could cause them to perform actions they don’t intend
- XSS - line 44 of journal.py, note content is directly pasted into the HTML, allowing an attacker to insert arbitrary HTML and JavaScript code
So the goal would be to chain those 3 exploits like so - we first (using CSRF) put a XSS payload in the admin’s journal that will, when loaded in someone’s browser, will send all GET parameters to an attacker-controlled domain (to steal the OAuth token), then we will have the admin bot create an OAuth token on accounts and set the redirect_uri
parameter to the vulnerable page on journal.bitdizzle.xyz to trigger our XSS payload. Easy, right?
Difficulty with CSRF
Journal entries are created by sending a POST request to http://journal.bitdizzle.xyz/entries/
, with the JSON-formatted data. Normally, a cross-site POST request forgery can be achieved by sending the user to an attacker-controlled page that runs the following JavaScript:
1 | (async () => { |
However, running that code returns a CORS error. The gist of it is this - since the Access-Control-Allow-Origin
header wasn’t present on journal.bitdizzle.xyz and didn’t allow POST requests from my domain, the CSRF attack failed (exactly to prevent this kind of thing). One work around is to add the mode: 'no-cors'
to the object inside fetch. The problem I ran into was that this meant our Content-Type
header was invalidated, and line 56 of journal.py used the flask.request.get_json()
, which by default requires the Content-Type
header to be set to application/json
. Even though we were sending it JSON-formatted data, the request would give us a 500 error since the Content-Type
was incorrect.
After a while of looking into bypassing this (even inspecting the Flask source code for unintended bugs and behaviors), I couldn’t get past this. If I couldn’t exploit the CSRF for the admin user, we could obtain the OAuth code to log in as admin.
Signing Admin into Another Account
This is when my teammate had the brilliant idea of signing the admin user into another malicious account on journals.bitdizzle.xyz. It didn’t matter if the admin was signed into the admin account on journals.bitdizzle.xyz, as long as they visited a page with a XSS payload on it. So our strategy then turned to 1) making a brand-new account on accounts.bitdizzle.xyz, 2) authenticating to journals.bitdizzle.xyz, 3) leaving a XSS payload in the journal notes for that new account, and 4) making the admin account sign in to the brand-new account on journals.bitdizzle.xyz.
After that, we could either exfiltrate the OAuth token, or just exfiltrate the content on the admin’s original journal account.
Solve Script
Here is my automated exploit in Python:
1 | import requests, secrets |
Viewing my domain afterwards shows that the journal entry contents have been exfiltrated:
Flag: ASV{m0re_lik3_N0AUTH_Am_i_r1GhT}