Skip to main content

Utiliser la boucle d’événements de Node.js pour les attaques temporelles

Écrit par

16 février 2016

0 minutes de lecture

Il y a un peu plus de 3 ans, quelques amis et moi avons créé un groupe appelé pasten pour participer à la compétition Capture The Flag (CTF) du Chaos Computer Club. Il s’agit d’un CTF de type Jeopardy, où les équipes participantes doivent résoudre des défis liés à la sécurité dans différentes catégories : exploitation, rétro-ingénierie, Web, analyse forensique et cryptographie. Les CTF sont amusants et instructifs, et je vous recommande vivement d’y participer. C’est encore plus amusant quand on gagne, comme cette année !

Cette année, parmi les autres défis de la compétition, figurait une application Node.js vulnérable à une attaque temporelle intéressante qui exploitait la boucle d’événements propre à Node.js. Dans cet article, nous analysons le défi, expliquons les risques et montrons comment un attaquant peut mettre au jour et exploiter ce type de vulnérabilité. J’espère également que cela vous aidera à éviter des vulnérabilités similaires dans votre propre code.

Attaques temporelles

Avant d’examiner le défi, expliquons ce qu’est une attaque temporelle.

Imaginez un service qui, lorsqu’un utilisateur saisit un mot de passe incorrect associé à une adresse e-mail existante, répond : « La cinquième lettre de votre mot de passe est incorrecte, veuillez réessayer. » Absurde, n’est-ce pas ? Fournir une telle information permet à un attaquant de tester toutes les possibilités du mot de passe caractère par caractère, et rend l’intrusion triviale. Pourtant, c’est exactement ce qui se produit lorsqu’on utilise une comparaison de chaînes de caractères naïve pour vérifier des mots de passe ou des jetons d’authentification.

La comparaison native de chaînes de caractères, y compris avec l’opérateur == de JavaScript, parcourt généralement deux chaînes de même longueur, compare les caractères un à un et s’arrête dès qu’ils diffèrent. Ainsi, si je comparais foo à bar, la boucle ne ferait qu’une itération, tandis que la comparaison de foo avec fox porterait sur 3 caractères et prendrait plus de temps.

Exemple de fonction d’authentification vulnérable :

function isAuthenticated(user, token) {
  var correctToken = FetchUserTokenFromDB(user);
  return token === correctToken;
}

La comparaison d’un seul caractère est très rapide, mais elle prend suffisamment de temps pour être détectable. Des recherches montrent qu’un attaquant peut mesurer des événements avec une précision de 15 à 100 µs sur Internet, et de 100 ns sur un réseau local. Les attaquants peuvent exploiter ces techniques : même de petits délais peuvent ainsi révéler à l’utilisateur quel caractère était incorrect.

Ce type d’attaque est appelé attaque temporelle. Elle peut être menée chaque fois que les données saisies influent sur le temps de traitement. Pour l’empêcher, il faut que la comparaison des chaînes prenne le même temps, quel que soit le mot de passe saisi, par exemple en effectuant un xoring des deux mots de passe et en vérifiant si le résultat est égal à zéro.

Non vulnérable aux attaques temporelles :

var mismatch = 0;
for (var i = 0; i < a.length; ++i) {
  mismatch |= (a.charCodeAt(i) ^ b.charCodeAt(i));
}
return mismatch;

Le défi : Sequence Hunt

Maintenant que nous avons ces informations, examinons le défi. Tout a commencé par un lien vers une page contenant le texte suivant :

Sequence HuntBienvenue dans ma chasse aux suites d’entiers. Vous devez calculer cinq nombres compris entre 0 et 100 en implémentant les algorithmes ci-dessous. Lorsque vous pensez avoir la bonne solution, envoyez-les ici en tant que paramètres GET.Vous pouvez également obtenir quelques informations vous concernant ici.Je m’attends à une forte charge, j’ai donc conçu cette application autour de boucles d’événements I/O asynchrones.Amusez-vous bien !

[MODIFICATION]Quelqu’un m’a envoyé ce lien. Comme chaque vérification prend un temps considérable, le serveur attend désormais que trois secondes se soient écoulées depuis la réception de la requête afin d’empêcher ces attaques.[/MODIFICATION]

AlgorithmesÀ PUBLIER

Le texte ici renvoie vers /check, où nous devons envoyer les bons paramètres pour obtenir le flag, et IOLoops renvoie vers /info, qui affiche ce qui suit :

vous êtes 37.120.106.140nombre de requêtes sur votre boucle d’événements I/O : 1

Que savons-nous jusqu’ici ?

  1. Un algorithme inconnu prend 5 nombres compris entre 0 et 100 en entrée.

  2. Pour obtenir le flag, nous devons fournir les bons nombres.

  3. Le temps d’exécution de l’algorithme dépend probablement des données saisies, ce qui pourrait permettre une attaque temporelle.

  4. Le serveur attend toujours au moins 3 secondes avant de renvoyer la réponse, afin de contrer les attaques temporelles.

Maintenant, nous devons trouver comment capturer le flag. Comme le défi s’intitule Sequence Hunt, j’utiliserai sequencehunt.com comme nom de domaine fictif dans mes exemples. N’envoyez aucune attaque vers ce domaine, qui n’a aucun lien avec le défi ! Vous pouvez lancer l’exemple d’application vulnérable que j’ai écrit. Son comportement devrait être similaire à celui de l’application du CTF.

Exploration

Une vérification rapide confirme qu’une requête check met bien environ 3 secondes à renvoyer une réponse, quelles que soient les valeurs fournies. Il est donc impossible de tester toutes les combinaisons par force brute. Il faudrait essayer 100^5 (10 milliards) de combinaisons, ce qui prendrait des milliers d’années…

$ curl "https://sequencehunt.com/check?val0=1&val1=1&val2=1&val3=1&val4=1"
you are 37.120.106.140
At least one value is wrong!

$ time curl "https://sequencehunt.com/check?val0=1&val1=1&val2=1&val3=1&val4=1"
you are 37.120.106.140
At least one value is wrong!
0.00s user
0.01s system
0% cpu
3.174 total

En testant la page /info, nous remarquons que chaque requête /check incrémente le compteur de boucles I/O, tout comme la requête /info elle-même.

$ curl "https://sequencehunt.com/info"
you are 37.120.106.140<br>request count on your IOLoop: 1

$ curl "https://sequencehunt.com/info"
you are 37.120.106.140<br>request count on your IOLoop: 2

$ curl "https://sequencehunt.com/info"
you are 37.120.106.140<br>request count on your IOLoop: 3

$ curl "https://sequencehunt.com/check?val0=1&val1=1&val2=1&val3=1&val4=1"
you are 37.120.106.140<br>At least one value is wrong!

$ curl "https://sequencehunt.com/info"
you are 37.120.106.140<br>request count on your IOLoop: 5

Envoyer des requêtes /check avec différents nombres aléatoires ne révèle aucune différence statistiquement significative dans les temps de réponse. L’algorithme semble s’exécuter en moins de 3 secondes ; toutes les requêtes sont donc simplement renvoyées après le délai minimum de 3 secondes.

Cependant, nous avons remarqué que l’envoi d’une requête à /info alors qu’une requête /check est en cours prend beaucoup plus de temps. Nous avons ainsi une piste à explorer liée au temps de réponse. Pour comprendre la suite, prenons un moment pour parler de la boucle d’événements de Node.js.

La boucle d’événements de Node.js

Conçu pour être évolutif, Node.js (et JavaScript en général) est un environnement asynchrone piloté par les événements. Lorsqu’un morceau de code doit déclencher une opération potentiellement bloquante, comme ouvrir un fichier ou écrire sur le réseau, il enregistre une fonction de rappel, déclenche l’opération concernée, puis se termine. Une fois l’opération terminée (par exemple, lorsque le fichier est ouvert), la boucle d’événements appelle la fonction de rappel.

Le modèle piloté par les événements est extrêmement évolutif, car les threads ne restent jamais à « attendre ». Il pose toutefois problème lorsqu’une fonction met longtemps à s’exécuter. Par défaut, le serveur Node.js n’exécute qu’un seul thread par cœur ; une fonction de longue durée occupe donc tout le cœur, tandis que les autres attendent leur tour dans la file. Dans un navigateur, c’est ce qui provoque le message d’erreur « le script met trop de temps à s’exécuter » que vous avez peut-être déjà vu. Sur un serveur, les requêtes restent simplement bloquées.

Pour mieux comprendre la boucle d’événements de Node.js, regardez l’excellente présentation de Philip Roberts à JSConf, ou lisez l’un de ces articles.

Résoudre l’énigme

Dans notre cas, la boucle d’événements nous a fourni les informations temporelles dont nous avions besoin. Tant que /check était en cours d’exécution, la boucle d’événements ne reprenait pas le contrôle, ce qui bloquait la requête /info. En revanche, la fonction setTimeout se contente d’enregistrer un événement planifié et de céder le contrôle, ce qui permet de traiter rapidement les requêtes /info. J’ai publié sur GitHub le code d’un serveur simple qui illustre ce phénomène, si vous souhaitez le tester en local.

Maintenant que nous avions un moyen d’obtenir des informations temporelles (autrement dit, un canal auxiliaire), il ne nous restait qu’à envoyer une requête /check puis, sans attendre la réponse, une requête /info, et à mesurer le temps nécessaire pour obtenir cette réponse.

Diagramme de séquence montrant un attaquant qui chronomètre les requêtes de vérification et les réponses tandis que la boucle d’événements du serveur est bloquée et que setTimeout retarde la réponse.

Pour automatiser la recherche et compenser les variations du réseau, l’étape suivante consistait à écrire un script Python qui, pour un ensemble de valeurs :

  • Ouvre n threads

  • Envoie une requête check et mesure le temps nécessaire pour obtenir la réponse à une requête /info parallèle

  • Fait la moyenne des temps et renvoie le résultat

J’ai utilisé n=5, ce qui s’est avéré suffisant. Il peut être nécessaire d’augmenter cette valeur selon la qualité de la connexion Internet et les caractéristiques temporelles de l’algorithme. Voici les résultats de la première exécution : notez la différence significative pour la valeur 4.

[0, 0, 0, 0, 0]: 0.4933500289916992
[1, 1, 1, 1, 1]: 0.3603970050811768
[2, 2, 2, 2, 2]: 0.4297104358673095
[3, 3, 3, 3, 3]: 0.4705570697784423
[4, 4, 4, 4, 4]: 0.9154952526092529 <<<<<<<<
[5, 5, 5, 5, 5]: 0.4637355804443359
[6, 6, 6, 6, 6]: 0.3557830333709716
[7, 7, 7, 7, 7]: 0.4418128013610840
[8, 8, 8, 8, 8]: 0.4297045707702637

Comme la différence de temps n’apparaissait que lorsque 4 occupait la première position, l’étape suivante consistait à parcourir les autres paramètres. Le deuxième chiffre était 12.

[4, 10, 10, 10, 10]: 0.840841388702392
[4, 11, 11, 11, 11]: 0.859339499473571
[4, 12, 12, 12, 12]: 1.228700304031372 <<<<<<<<
[4, 13, 13, 13, 13]: 0.907831811904907

Et ainsi de suite, jusqu’à trouver tous les bons paramètres : [4, 12, 77, 98, 35]

$ curl 'https://sequencehunt.com/check?val0=4&val1=12&val2=77&val3=98&val4=35'
you are 37.120.106.140
Congratulations, here is your prize:
32C3_round_and_round_and_round_IT_goes

Flag capturé.

En résumé

Les attaques temporelles représentent une véritable menace et touchent aussi bien les petites que les grandes entreprises (comme Google). Même de faibles différences dans le temps d’exécution peuvent être détectées avec un échantillon relativement réduit (à l’échelle d’Internet), puis exploitées par les attaquants pour obtenir des informations et affiner leurs attaques par force brute. Dans Node.js en particulier, la boucle d’événements peut fournir un autre signal exploitable pour les attaques temporelles.

Voici quelques conseils pour protéger votre application contre ce type de faille :

  • Lors de la mise en œuvre des vérifications d’authentification, veillez à ce que le processus d’authentification dure toujours le même temps. Vous pouvez utiliser secure-compare ou d’autres packages pour effectuer des comparaisons à temps constant.

  • Vérifiez qu’ils ne présentent pas de vulnérabilités connues.

  • Testez la présence de vulnérabilités temporelles à l’aide d’outils.

Enfin, si vous souhaitez mettre vos compétences à l’épreuve et relever ces défis, essayez de participer l’année prochaine au Capture The Flag du CCC !

Lancez-vous dans les compétitions Capture The Flag

Apprenez à résoudre des défis de capture du drapeau en regardant à la demande notre atelier virtuel d’initiation.

Lire la suite

Blog

Les modèles de pointe ont trouvé les vulnérabilités. Seul l’attaquant a trouvé les chaînes d’exploitation.

L’analyse statique a détecté les failles, mais seuls des tests d’attaque en conditions réelles ont prouvé comment elles pouvaient être enchaînées pour provoquer des compromissions. Comparaison d’Evo COS, de Claude Security et de Claude Code Security.

feature insights context
Blog

Les attaques autonomes sont déjà là. La défense doit suivre leur rythme.

Les attaquants autonomes réduisent la fenêtre de défense. Découvrez comment la découverte, la correction, la validation et la prévention continues peuvent aider les équipes de sécurité à suivre le rythme.

Blog

Votre backlog de vulnérabilités n’est plus une dette technique, c’est une surface d’attaque

Un backlog de vulnérabilités qui s’allonge est plus qu’une dette technique : c’est une surface d’attaque. Découvrez pourquoi les anciennes hypothèses de risque, les attaquants automatisés et les vulnérabilités en chaîne exigent une nouvelle approche.