Introduction

Dans le cadre de mon activité de Recherche et Développement (R&D) au sein du Pôle d’Expertise Technique d’ACCEIS, je suis amené à travailler un projet de recherche autour de la sécurité d’Unicode. Au sein de ce projet, toujours en cours, je tente d’aborder les thématiques suivantes : le fonctionnement d’Unicode, les problématiques de conception et liées à l’utilisation d’Unicode, les attaques ciblant ou utilisant Unicode, comment l’environnement informatique et les outils de développement peuvent permettre de détecter et se protéger de ces attaques.

Le 01 et 02 Avril 2022 s’est tenu le BreizhCTF 2k22, édition 2022 de l’évènement de cybersécurité Rennais incluant les activités suivantes :

  • Ateliers découverte des métiers de la cybersécurité ;
  • Hack & Job : rencontre entreprises et candidats ;
  • Rumps : présentations brèves (de 5 minutes) de sujets en lien avec la cybersécurité ;
  • Discours de clôture :
    • Guillaume POUPARD, rétrospective des 15 dernières années de la cybersécurité française à travers le prisme de l’ANSSI ;
    • Alex BERGER, comment la série TV le Bureau des Légendes est utile à la sensibilisation à la cybersécurité au plus grand nombre ;
  • CTF : compétition de hacking informatique.

C’est donc à l’occasion de cet évènement que j’ai pu présenter cette Rump de 5 minutes mettant en lumière deux exemples d’attaques abusant de la complexité d’Unicode.

Répertoire Github contenant le support de présentation.

La présentation

Collision par transformation de casse

Le premier exemple expose une attaque de prise de contrôle du compte par réinitialisation de mot de passe (Account takeover via password reset) en utilisant une collision par transformation de casse (Case transformation collision) comme vecteur inspiré par l’article Hacking Unicode Like a Boss de Charlie ERIKSEN (28/02/2020, Bugcrowd Blog).

Afin d’illustrer l’attaque, le bloc de code NodeJS suivant sera utilisé :

app.post('/api/password/reset', function(req, res) {
  var email = req.body.email;
  db.get('SELECT id, email, FROM users WHERE email = ?',
    [email.toLowerCase()],
    (err, user) => {
      if (err) {
        console.error(err.message);
        res.status(400).send();
      } else {
        generateTemporaryPassword((tempPassword) => {
          accountRepository.resetPassword(user.id, tempPassword, () => {
            messenger.sendPasswordResetEmail(email, tempPassword);
            res.status(204).send();
          });
        });
      }
    });
});

On observe le comportement suivant :

  1. L’adresse email renseignée par l’utilisateur est convertie en minuscule afin de normaliser la comparaison (on peut imaginer que toutes les adresses email en base de données sont stockées en minuscule pour la consistance)
  2. L’adresse email ainsi formatée est comparée aux entrées présentes dans la base de données afin de vérifier si elle y existe
  3. Si elle existe, un mot de passe temporaire ou plutôt un lien de réinitialisation unique sera généré pour l’utilisateur correspondant
  4. Un email sera envoyé à l’adresse email telle que fournie par l’utilisateur

Dans le doux monde de l’ASCII tout va bien, mais dans le rude monde de l’Unicode un loup pourrait se présenter dans la bergerie.

Au moins deux mauvaises pratiques s’additionnent ici :

  1. La pseudo-normalisation des chaînes de caractère en utilisant la fonction .toLowerCase() (le cœur du problème qui sera étudié par la suite)
  2. L’envoi d’email à l’adresse email fournie par l’utilisateur plutôt que celle récupérée en base de données

Comme mentionné dans la RFC 2142, les adresses email ne sont pas sensibles à la casse. C’est certainement avec cette idée en tête que le développeur à naïvement tenté d’utiliser la fonction .toLowerCase() afin de pouvoir effectuer une comparaison consistante. L’intention était donc de normaliser ce que l’utilisateur aurait pu fournir comme format (code>Admin@hackceis.pw, code>ADMIN@hackceis.pw, code>admin@HACKCEIS.fr, etc.) en un format consistant (code>admin@hackceis.pw).

unicode normalization

Cher lecteur, il est maintenant venu le temps pour moi de vous poser cette question : pouvez vous faire la différence entrebr>admin@HACKCEIS.pw et code>admin@HACKCEIS.pw ?

Vous ne voyez pas, n’est-ce pas ?

unicode homoglyphs

Ce sont des homoglyphes, des caractères distincts qui se ressemblent étrangement mais ce n’est pas la confusion visuelle qui est intéressante ici. Ce qui est intéressant pour cette attaque, c’est que bien que deux caractères sont différents, une fois convertis en minuscules ceux-ci sont identiques.

  • Le caractère Unicode U+004B (Latin Capital Letter K) deviendra le caractère U+006B (Latin Small Letter K) en
    minuscule.
  • Le caractère Unicode U+212A (Kelvin Sign) deviendra le caractère U+006B (Latin Small Letter K) en minuscule.

Autrement dit en langage mathématique, la transformation de casse (majuscule ou minuscule) en ASCII est une application bijective (en omettant les caractères invariants), alors que c’est une application surjective en Unicode, ce qui introduit un biais cognitif pour qui ne connaît pas les spécificités d’Unicode.

Voici une illustration en NodeJS :

> a = "K";
'K'
> b = "K";
'K'
> a == b
false
> a.toLowerCase() == b.toLowerCase()
true

Pour revenir à l'exemple de réinitialisation de mot de passe, voici ce qui se passera en cas d'attaque de la part d'un acteur malveillant :

  1. L’attaquant fournit l’adresse email admin@HACKCEIS.pw (avec le symbole du Kelvin U+212A) qui est convertie en admin@hackceis.pw (collision par transformation de casse)
  2. La condition est validée, admin@hackceis.pw existe dans la base de données
  3. Le mot de passe temporaire ou le lien de réinitialisation unique est généré pour l’utilisateur Administrateur
  4. L’email permettant de réinitialiser le mot de passe de l’Administrateur est envoyé à l’attaquant

L’attaque de prise de contrôle du compte a réussi.

Fractionnement du nom d’hôte

Le second exemple expose une attaque de Fractionnement du nom d’hôte (Hostname splitting) inspiré par la
présentation Host/Split, Exploitable Antipatterns in Unicode Normalization de Jonathan BIRCH (03-08/08/2019 à la
BlackHat USA 2019).

Cette fois-ci l’objet d’intérêt ne sera plus la transformation de casse mais les méthodes de normalisation.

En utilisant la méthode de normalisation NFKD (Normalization Form Compatibility Decomposition)) ou NFKC (Normalization Fork Compatibility decomposition followed by canonical Composition) le caractère Unicode (U+2100, Account Of) est transformé en a (U+0061) + (U+002F)+ c (U+0063). En bref, :arrow_right: a/c.

Voici deux illustrations en Ruby appliquées à une URL :

url = 'https://acceis.c℀hat.client.com'
url.unicode_normalize(:nfkd) # => "https://acceis.ca/chat.client.com"
url.unicode_normalize(:nfkc) # => "https://acceis.ca/chat.client.com"

url = 'https://acceis.medi℀areer.client.com'
url.unicode_normalize(:nfkd) # => "https://acceis.media/career.client.com"
url.unicode_normalize(:nfkc) # => "https://acceis.media/career.client.com"

Cela permet de transformer une URL pointant vers un nom de domaine légitime en une URL pointant vers un nom de domaine maîtrisé par un attaquant.

Au moment où cette attaque a été identifiée, certains navigateurs web étaient vulnérables, le navigateur qui recevait un en-tête HTTP Location avec un tel nom de domaine le normalisait et redirigeait l’utilisateur vers le domaine normalisé permettant ainsi de détourner les secrets pouvant être contenus dans le corps de la requête.
Cette vulnérabilité est depuis bien longtemps corrigée dans les navigateurs web mais cette attaque peut toujours être utilisée pour contourner des mécanismes de sécurité basés sur des listes blanches dynamiques ou des expressions régulières. Par exemple contourner une CSP ayant la valeur suivante Content-Security-Policy: default-src 'self' *.cdn.client.com, des fonctions de vérification pour des redirections (ex : redirect_url.match?(/.+.client.com/)), autres filtres, contournement SSRF, contournement de WAF, etc..

L’utilisation du caractère Unicode U+2100 pour cette attaque est contraignante. En effet, cela oblige l’attaquant à deux choses :

  • Utiliser un nom de domaine malveillant ayant un TLD finissant en A : .ca, .media, .ninja, .pizza, .mba, .moda, etc.
  • Cibler un domaine ou sous-domaine de la victime commençant par C : company.com, chat.client.com, career.client.com, etc.

Néanmoins le caractère Unicode U+2100 n’est pas le seul permettant fractionner les URLs de manière intéressante.

  • (U+2101) ➡️ a/s, beaucoup d’autres similaires
  • (U+2048) ➡️ ?!, passage en paramètre
  • (U+FF0F) ➡️ /, utilisation similaire à U+2100 mais sans les contraintes, caractère le plus pratique pour le  hostname splitting
  • (U+FF03) ➡️ #, passage en ancre
  • (U+FF20) ➡️ @, passage de nom d’utilisateur user@cdn.client.com ➡️ user@cdn.client.com
  • (U+FF1A) ➡️ :, passage d’identifiants, user:pass@secret.client.com ➡️ user:pass@secret.client.com, peu utile
  • (U+2488) ➡️ 1., 192.168.⒈1 ➡️ 192.168.1.1, contournement de sécurité, exemple : SSRF avec expression régulière (/(\b25[0-5]|\b2[0-4][0-9]|\b[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}/)
    interdisant les adresses IP
unicode joke

À propos de l’auteur

Article et présentation rédigés par Alexandre ZANNI aka noraj, Ingénieur en Test d’Intrusion chez
ACCEIS.