Hervé Godquin

Aller au contenu | Aller au menu | Aller à la recherche

Mot-clé - encodage

Fil des billets - Fil des commentaires

mardi 14 janvier 2020

CVE-2019-19844 : Django account takeover, revue de code et bonne année

Déjà je tiens à vous souhaiter à toutes et à tous une excellente année 2020. Maintenant que ceci est dit passons au sujet qui nous intéresse : la description d'une petite vulnérabilité intéressante.

Aujourd'hui j'ai choisi la CVE-2019-19844 touchant Django. Cette vulnérabilité touche le formulaire de reset de mot de passe dont le code source (vulnérable) est disponible ici : https://github.com/django/django/blob/4cec3cc82a09b1a60a72e5437a7f0e9e0c7d203c/django/contrib/auth/forms.py

Voici les fonctions posant problèmes :

    def get_users(self, email):
        """Given an email, return matching user(s) who should receive a reset.
        This allows subclasses to more easily customize the default policies
        that prevent inactive users and users with unusable passwords from
        resetting their password.
        """
        active_users = UserModel._default_manager.filter(**{
            '%s__iexact' % UserModel.get_email_field_name(): email,
            'is_active': True,
        })
        return (u for u in active_users if u.has_usable_password())
    
    def save(self, domain_override=None,
             subject_template_name='registration/password_reset_subject.txt',
             email_template_name='registration/password_reset_email.html',
             use_https=False, token_generator=default_token_generator,
             from_email=None, request=None, html_email_template_name=None,
             extra_email_context=None):
        """
        Generate a one-use only link for resetting password and send it to the
        user.
        """
        email = self.cleaned_data["email"]
        if not domain_override:
            current_site = get_current_site(request)
            site_name = current_site.name
            domain = current_site.domain
        else:
            site_name = domain = domain_override
        for user in self.get_users(email):
            context = {
                'email': email,
                'domain': domain,
                'site_name': site_name,
                'uid': urlsafe_base64_encode(force_bytes(user.pk)),
                'user': user,
                'token': token_generator.make_token(user),
                'protocol': 'https' if use_https else 'http',
                **(extra_email_context or {}),
            }
            self.send_mail(
                subject_template_name, email_template_name, context, from_email,
                email, html_email_template_name=html_email_template_name,
            )

On peut avouer que comme ça la vulnérabilité n'est pas forcément très facile à détecter. En effet le problème est assez particulier, et montre que certains points de bonnes pratiques peuvent avoir des implications sécuritaires pouvant être importantes.

Les gros problèmes sont là : '%s__iexact' % UserModel.get_email_field_name(): email, et ici : return (u for u in active_users if u.has_usable_password()). J'avoue même comme cela ça ne saute pas aux yeux :). Du coup je vous invite à lire ceci : http://unicode.org/reports/tr36/#Recommendations_General.

Oui il s'agit bel et bien d'un problème d'encodage de caractères, en soit les petits curieux qui ont été voir la description de la CVE avaient sans doute trouvés :

Django before 1.11.27, 2.x before 2.2.9, and 3.x before 3.0.1 allows account takeover. A suitably crafted email address (that is equal to an existing user's email address after case transformation of Unicode characters) would allow an attacker to be sent a password reset token for the matched user account. (One mitigation in the new releases is to send password reset tokens only to the registered user email address.)

Je vous laisse découvrir le patch par vous même : https://github.com/django/django/commit/5b1fbcef7a8bec991ebe7b2a18b5d5a95d72cb70?diff=unified

Alors qu'est-ce que tout cela peut nous apprendre au final (à part qu'il faut patcheer son django). En soit ça vous donne de nouvelles idées de tests pour vos pentests / bug-bounties, mais surtout si vous faîtes de la revue de code cela est assez intéressant.

Premièrement car normalement ces fonctions devraient être revue "en profondeur" si on en croit l'OWASP :

"Are all the untrusted inputs validated? Input data is constrained and validated for type, length, format, and range."

Mais un peu plus que cela, car en effet en soit, tout est bon : ce qu'il manque c'est juste un respect des bonnes pratiques Unicode. Dites à votre avis, combien de développeurs, même voir d'auditeurs les connaissent ? Lors d'un audit de type revue de code au planning serré, cela est-il toujours contrôlé ? De même lors de pentest d'ailleurs ?

Donc voilà ce que cela nous apprends : Je suis sûr qu'il y a bien d'autres cas comme cela, alors à vos git pour les contrôles :).

J'avoue que je commence gentiment pour ce premier post de l'année, mais en regardant les dernières vulnérabilités dans le monde du libre, je n'avais pas trop l'inspiration, c'était soit Django, soit OpenSSL avec la CVE-2019-1551 qui je l'avoue ne m'a pas inspiré. Je vous laisse observer ce magnifique fichier PERL : https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=419102400a2811582a7a3d4a4e317d72e5ce0a8f (y avait aussi un XSS dans wordpress mais bon ...).

Je vais tenter de préparer un nouvel article pour la semaine prochaine, sur une vulnérabilité un peu plus "costaud" ça sera sympatique.

 

Have fun !

P.S : comme toujours si vous voulez proposer des améliorations, corriger des fautes, passez par le gitlab des paranoiac-thoughts : https://www.paranoiac-thoughts.com/netpsycho/blog_post/blob/master/20200114_django