Sortir des courtiers de messages
Adam Goldschmidt
5 août 2020
0 minutes de lectureJ’ai récemment signalé deux vulnérabilités dans Apache Airflow, une bibliothèque open source qui permet aux développeurs de créer, planifier et surveiller des workflows par programmation. Ces deux vulnérabilités permettent à un attaquant de modifier le périmètre de l’attaque et d’obtenir des privilèges sur une autre machine. Elles reposent toutes deux sur l’accès préalable de l’attaquant au courtier de messages.
Dans cet article, je vais vous expliquer pourquoi il ne faut pas faire confiance aux courtiers de messages et comment j’ai pu exploiter Apache Airflow pour obtenir des privilèges sur des machines censées être protégées. Avant d’entrer dans le vif du sujet, commençons par les bases des courtiers de messages.
Qu’est-ce qu’un courtier de messages ?
Un courtier de messages est un logiciel qui permet à des services de communiquer et d’échanger des informations. Son fonctionnement peut évoquer celui d’une API, mais le courtier de messages s’appuie généralement sur une file d’attente dans laquelle les différents services peuvent déposer ou lire des messages. Ainsi, ces services peuvent communiquer de façon asynchrone, même s’ils ont été écrits dans des langages différents ou déployés sur des plateformes distinctes.
Les courtiers de messages peuvent servir de passerelle entre des applications : les expéditeurs publient des messages sans savoir où se trouvent les destinataires ni combien ils sont. Comme indiqué plus haut, un composant appelé file de messages stocke les messages jusqu’à leur traitement par un service consommateur. Ces files permettent également la programmation asynchrone : comme elles se chargent de transmettre les messages, l’expéditeur peut continuer à effectuer d’autres tâches.
Cas d’usage des courtiers de messages
Les courtiers de messages sont largement utilisés dans le développement logiciel. Ils sont utiles dès lors que l’on a besoin d’une communication fiable entre plusieurs services, d’une livraison garantie des messages ou de fonctionnalités asynchrones.
Les cas d’usage des courtiers de messages sont variés. Voici quelques exemples parmi les plus courants :
Traitement des paiements : il est essentiel que chaque paiement soit envoyé une seule fois. Le traitement de ces transactions par des courtiers de messages garantit que les informations de paiement ne seront ni perdues ni dupliquées et fournit une preuve de réception.
Tâches asynchrones : il est possible de dissocier les traitements lourds d’une requête utilisateur en cours afin de répondre immédiatement sans bloquer l’utilisateur.
Acheminement des messages vers une ou plusieurs destinations : il est plus simple et plus facile à maintenir de publier des messages sur une source unique, accessible à plusieurs services.
Maintenant que vous savez ce que sont les courtiers de messages et à quoi ils servent, voyons pourquoi et comment ils peuvent être vulnérables.
Faire trop confiance aux courtiers de messages
Tous les développeurs savent que les bases de données peuvent être compromises. Nous prenons généralement des mesures de sécurité supplémentaires, comme le chiffrement des mots de passe, afin de compliquer la tâche des attaquants, même si la base de données a été compromise.
La logique voudrait que ce soit exactement pareil pour les courtiers de messages, non ? Les données transférées finissent par atteindre différentes machines. Il faut donc les traiter avec une prudence accrue : non seulement en chiffrant les informations sensibles, mais aussi en protégeant les machines qui les utilisent.
Malheureusement, ce n’est pas le cas. Notre équipe de sécurité a découvert plusieurs exemples d’utilisation dangereuse des courtiers de messages, dont Apache Airflow. Cette utilisation dangereuse consiste à accorder trop de confiance aux courtiers de messages, par exemple en y stockant des commandes qui sont ensuite exécutées sans assainissement. Cela peut entraîner des injections de commandes ou l’exécution de code à distance sur les machines qui communiquent avec les courtiers.
Exploiter Apache Airflow
Qu’est-ce qu’Apache Airflow ?
Comme indiqué brièvement plus haut, Apache Airflow est un système complet de gestion des workflows. Avec Airflow, les workflows sont conçus et représentés sous forme de DAG (graphe orienté acyclique), dont chaque étape correspond à une tâche spécifique. Cette plateforme orientée code vous permet de faire évoluer vos workflows rapidement et efficacement.
Airflow intègre un planificateur de tâches, chargé de planifier et d’exécuter les DAG. Pour les exécuter, le planificateur s’appuie sur un composant appelé exécuteur.
Découvrir une vulnérabilité zero-day dans Airflow
Tout a commencé lorsque je recherchais des vulnérabilités de désérialisation dans des logiciels open source qui utilisent Celery — une file de tâches distribuée pour Python intégrant un courtier de messages — comme dépendance.
J’ai découvert qu’Airflow utilise Celery comme exécuteur, avec pickle par défaut. Or, je savais que le module Python pickle présente une vulnérabilité connue, que Celery avait autrefois utilisé par défaut.
Le schéma ci-dessous illustre, dans les grandes lignes, le fonctionnement d’Airflow avec Celery :

Le planificateur Airflow utilise Celery comme exécuteur. Celui-ci stocke les tâches, puis les exécute selon un calendrier défini.
Celery utilise le courtier de messages (Redis ou RabbitMQ) pour stocker les tâches. Les workers récupèrent ensuite les tâches auprès du courtier et les exécutent.
Les workers Celery d’Airflow désérialisent les données pickle stockées dans le courtier de messages. Autrement dit, si je parviens à accéder au courtier, je peux exécuter du code à distance sur les workers au moyen d’une attaque par désérialisation. Cette vulnérabilité a été référencée sous le numéro CVE-2020-11982.
Attendez, qu’est-ce qu’une attaque par désérialisation ?
La sérialisation consiste à convertir un objet en une séquence d’octets qui peut être enregistrée sur un disque ou dans une base de données, ou transmise par flux. Le processus inverse, qui consiste à recréer un objet à partir d’une séquence d’octets, s’appelle la désérialisation. La sérialisation sert couramment à la communication (partage d’objets entre plusieurs hôtes) et à la persistance (stockage de l’état d’un objet dans un fichier ou une base de données).
Une attaque par désérialisation se produit lorsqu’une application désérialise des données sans vérifier suffisamment que le résultat est sûr, ce qui permet à l’attaquant de contrôler l’état ou le déroulement de l’exécution. Je ne dis pas qu’il ne faut jamais stocker de données sérialisées dans des courtiers de messages : c’est indispensable dans de nombreux cas. Je pense toutefois qu’il faut tenir compte de ce risque lors de la conception de ce type de système et envisager des mesures de sécurité supplémentaires.
Pour en savoir plus sur les attaques par désérialisation, consultez cet article.
Forcer Airflow à désérialiser mes tâches avec pickle
J’ai commencé par créer une tâche Airflow et examiner sa structure. J’ai ajouté un DAG (un ensemble de tâches), l’ai associé à une file d’attente nommée « test », configuré Redis comme courtier Celery, puis démarré la file :
airflow worker -q test
En examinant la structure des messages Redis, j’ai découvert qu’ils étaient stockés dans une table de hachage Redis appelée unacked. Les valeurs de cette table sont structurées comme suit :
Les éléments intéressants sont les valeurs body, content-type et content-encoding. La valeur body, une fois décodée, est la suivante :
Notez que les commandes elles-mêmes sont stockées ici ! Cela sera important pour la deuxième vulnérabilité. J’ai également trouvé un ensemble nommé unacked_index. Je pensais que ses éléments correspondaient aux clés de la table de hachage, et j’ai vérifié. J’ai d’abord récupéré les clés de la table :
J’ai ensuite affiché tous les éléments de unacked_index :
Comme vous pouvez le constater, ce sont bien les mêmes valeurs, dans un ordre différent. À ce stade, j’étais presque certain que unacked_index servait à stocker les identifiants des tâches à exécuter, et que la table de hachage associait chaque identifiant à la tâche correspondante. Pour ajouter une nouvelle tâche personnalisée, je devais donc ajouter une valeur arbitraire à unacked_index, puis créer un nouvel élément dans la table de hachage, avec cette même valeur comme clé et une charge utile malveillante comme valeur.
Pour mener une attaque par désérialisation, il me fallait une charge utile malveillante. J’ai rapidement créé un script Python qui génère une charge utile qui, après désérialisation avec pickle, crée un fichier nommé malicious :
Vous trouverez ici plus d’informations sur l’exploitation de pickle en Python. Pour forcer la file à récupérer la valeur malveillante et à la désérialiser avec pickle, je devais définir content-type sur application/x-python-serialize (pickle) et content-encoding sur binary. Voici une preuve de concept pour ajouter une tâche malveillante :
Comme expliqué plus haut, voici les étapes suivies :
Ajout d’une valeur arbitraire à
unacked_indexcomme identifiant de tâcheAjout d’un nouvel élément à
unacked: l’identifiant créé précédemment comme clé et une charge utile malveillante comme valeur. Cette charge utile contenait la charge utile encodée en base64 dans la valeurbody.
Le schéma suivant illustre le processus :

J’ai lancé la file de test et, voilà ! Un nouveau fichier nommé malicious a été créé sur le worker qui exécutait la file : le périmètre de l’attaque venait de changer complètement. J’expliquerai les conséquences de cette vulnérabilité juste après avoir passé en revue la suivante !
Vulnérabilité d’injection de commandes
Passons à la deuxième vulnérabilité, une injection de commandes référencée sous le numéro CVE-2020-11981.
En examinant le code source d’Airflow, j’ai découvert que les commandes provenant du courtier de messages étaient exécutées sans aucun assainissement. Après mon « OUI ! » de joie, celui qu’on pousse généralement lorsqu’on découvre une vulnérabilité, je suis retourné dans Redis et j’ai constaté que je pouvais injecter la charge utile suivante dans la clé body :
En suivant la même logique que précédemment, voici une preuve de concept (inutile de modifier content-type ni content-encoding cette fois, car nous n’avons pas besoin d’opérations pickle) :
J’ai ensuite découvert qu’il était également possible d’injecter cette charge utile dans RabbitMQ depuis son tableau de bord. RabbitMQ n’utilise pas la même structure que Redis ; il suffit donc d’utiliser la liste JSON brute, sans encodage base64.
L’impact de cette vulnérabilité est très similaire à celui de la précédente : elle donne le contrôle total des machines worker. Pour mieux comprendre, imaginons qu’avant l’exploitation de cette vulnérabilité, l’attaquant n’ait accès qu’à une seule machine : le courtier de messages. De plus, dans certaines installations, il peut être possible d’injecter des messages dans le courtier par l’intermédiaire d’une API, sans même accéder à la machine qui l’héberge.
Une fois le contrôle total de la machine worker obtenu, il peut être possible d’exposer des secrets, de provoquer un déni de service et même d’accéder à d’autres machines de la même infrastructure. Les vulnérabilités de ce type, qui permettent aux attaquants d’élargir le périmètre de leur attaque et d’élever leurs privilèges au sein d’un réseau compromis, peuvent faire la différence entre un incident de sécurité mineur et une compromission à grande échelle.
Remédiation

Si vous choisissez de stocker des commandes dans votre courtier de messages, vous pouvez appliquer la méthode de remédiation illustrée ci-dessus. Ainsi, même si un attaquant prend le contrôle du courtier, le worker n’exécute que des commandes connues et assainit toute entrée inattendue. C’est la méthode retenue par Apache, car la logique du package exige que les commandes soient stockées dans le courtier de messages.
Pour sécuriser votre infrastructure, vous devriez également protéger votre courtier de messages, notamment en mettant en place une authentification appropriée et TLS, afin de rendre son accès plus difficile pour les attaquants.
Conclusion
Ces deux vulnérabilités montrent comment j’ai pu exécuter du code et des commandes sur le serveur de la file d’attente (ou le worker) en accédant simplement au courtier de messages. Notez que, dans sa configuration initiale, Redis ne nécessite pas de mot de passe.
L’équipe Apache a rapidement reconnu ces vulnérabilités et les a corrigées. Même si l’équipe de sécurité de Snyk ne les considère pas comme particulièrement critiques, puisqu’elles nécessitent toutes deux un accès initial à l’infrastructure, elles restent dangereuses. Il n’est pas rare qu’une vulnérabilité nécessite des privilèges initiaux ou l’exploitation de plusieurs vulnérabilités (une sorte de chaîne d’attaque).
La principale leçon que j’aimerais que vous reteniez de cet article est la suivante : Ne faites pas aveuglément confiance à vos brokers de messages. Réfléchissez à leur conception et à leur architecture, ainsi qu’aux moyens de réduire les risques liés à leur utilisation.
Lancez-vous dans les compétitions Capture The Flag
Apprenez à résoudre des défis Capture The Flag en regardant à la demande notre atelier virtuel d’initiation.


