corCTF 2023 - Freecloud

  • Author: jazzpizazz
  • Date:

Freecloud

challenge

Freecloud is a medium level misc challenge. It could have passed as web but as it involved cracking a hash and the vulnerabilities were CVE based I moved it to misc. By clicking the “Host it yourself” link you are able to download the source. The webapp is written in Django and contains a files app. The app dynamically generates an admin url on startup. So the admin panel can not be found on /admin/. Looking at the source for the files app the following can be noted:

1def get_context_data(self, **kwargs):
2        context = super().get_context_data(**kwargs)
3        state = self.request.GET.get('page', 'templates/upload.html')
4        context['page'] = state
5        return context

The url parameter page that defaults to the value templates/upload.html is passed to the context that can be used in the rendered template. We can see that the template that is being used is called base.html:

1class FileFieldFormView(FormView):
2    form_class = FileFieldForm
3    template_name = 'templates/base.html'

This template checks if the page parameter is set, and if it is, it includes the template:

1<body>
2    <div class="container">
3      {% if page %}
4        {% include page %}
5      {% endif %}
6    </div>
7</body>

You would think that you could just include any file as a template but luckily Django has some security measures in place. For example it only looks for templates in specified template directories. We can find in settings.py that the template folder is set to media/ :

1TEMPLATES = [
2    {
3        'BACKEND': 'django.template.backends.django.DjangoTemplates',
4        'DIRS': ["media/"],

The media folder looks like this:

structure

So basically any files we upload could be loaded in the base template by setting the page param to uploads/ourfile however, we can only upload images files as an ImageField is used to validate and store the uploaded file. The source in [views.py](http://views.py) shows two additional interesting things:

 1def post(self, request, *args, **kwargs):
 2        form_class = self.get_form_class()
 3        form = self.get_form(form_class)
 4        files = request.FILES.getlist('file')
 5
 6        if form.is_valid():
 7            # Should in theory make it possible to upload multiple files at once
 8            for file in files:
 9                UploadedFile(file=file).save()
10            return self.form_valid(form)
11        return self.form_invalid(form)
  1. files gets request.FILES.getlist('file') so basically it gets a list of files from the file input.

  2. if the form is valid, the code loops through each file and saves the file as an UploadedFile.

  3. There is a comment noting that in theory it should be possible to upload multiple files at once

Looking at the Pipfile we can identify the Django version:

1[packages]
2django = "==4.1.5"

Looking at the Snyk page for this specific version we find that there are 4 vulnerabilities present. One of which is “Arbitrary File Upload” or CVE-2023-31047. The description is:

1Affected versions of this package are vulnerable to Arbitrary File Upload by bypassing of validation of all but the last file when uploading multiple files using a single forms.FileField or forms.ImageField.

We know the Django version is vulnerable, and we know that it should be possible to upload multiple files. Lets try to actually pull this off! Looking into Django SSTI we can basically copy the following template from the internet:

1{% include 'admin/base.html' %}
2{% load log %}{% get_admin_log 10 as log %}{% for e in log %}
3{{e.user.get_username}} : {{e.user.password}}{% endfor %}

The first line includes the admin base html which includes a header with a link to the admin page. This way we can effectively leak the admin URL. The second part of the payload attempts to leak the admin username and hashed password from admin logs (basically created whenever an admin makes a change in the admin).

The simplest way to approach this is by just manipulating the DOM to add the multiple attribute to the existing input:

dom

Call your template a.html and your image z.jpg as they are added in alphabetical order, and after pressing upload we get the following screen:

upload

When visiting /media/uploads/a.html we get served our file so we indeed bypassed the validation for our first file completely and succesfully upload it to the server, lets try to inject it through the page parameter:

ssti

We get the username jazzyboii (sorry for cringe, I didn’t want it easily guessable) and a hashed password. Using hashcat with the rockyou wordlist (or any other basic wordlist) we can easily crack this password: pbkdf2_sha256$390000$rh3UHPXCfZl5G26wleMpHM$sEUqvLkqT3OjqzNJRdKFrYaL8NSSiq1InB1R1kN8z+Q=:barbie how relevant!!

The home button in our template indeed links to the admin page, however when logging in we get a CSRF related error:

csrf

This was not part of the challenge and has something to do with how our instancer uses HTTPS, which required some config changes in Django. A big fuckup from my side, but by the time I noticed this one team had already solved a challenge. By the time I came up with a fix there were already two solves, and we decided to keep the challenge as is. The way to work around this issue was kinda trivial and all you needed to do was strip the Origin header in any authenticated POST requests.

Now that we have access to the admin we can import files by specifying a URL. The idea here is to get a file from the filesystem and basically copy it to the uploads folder from where we can retrieve it. However using the file scheme results in an error:

scheme

Lets have a look at the relevant code:

 1class ImportService:
 2    '''
 3    Service for handling file imports
 4    '''
 5    @staticmethod
 6    def validate_file_link(file_link):
 7        scheme_blocklist = ["data", "dict", "file", "ftp", "glob", "gopher"]
 8
 9        parsed_url = urlparse(file_link)
10        
11        if parsed_url.hostname == "127.0.0.1":
12            raise ValidationError("Invalid hostname")
13            
14
15        if parsed_url.scheme in scheme_blocklist:
16            raise ValidationError("Invalid scheme")
17        
18        return file_link
19
20    @staticmethod
21    def download_remote_file(validated_link):
22        target = urllib.request.urlopen(validated_link)
23        return target.read()

The ImportService has a method for validating file links and for downloading the actual file. There is a blocklist that disables any useful schemes. The provided link is passed to urlparse after which we check if the scheme is in our blocklist. Googling for any vulnerabilities in urlparse we can find CVE-2023-24329 described as:

The vulnerability exists due to insufficient validation of URLs that start with blank characters within urllib.parse component of Python. A remote attacker can pass specially crafted URL to bypass existing filters

The vulnerable Python versions are listed as Python: 3.7 - 3.11.2, the Dockerfile confirms this challenge runs on 3.9.2 which is indeed a vulnerable version. Lets change our input to file:///etc/passwd (note the space in front) and this time the attempt passes:

sice

Lets change the input to file:///flag.txt and import the file. This basically reads /flag.txt and stores its content in /media/uploads/flag.txt from where we can grab the flag:

1$ curl https://misc-freecloud-freecloud-bb9dc68a8914c059.be.ax/media/uploads/flag.txt
2corctf{0nly_m1sc_m4y_c0nt41n_CV3s}

The flag is: corctf{0nly_m1sc_m4y_c0nt41n_CV3s}