
Analyse technique approfondie de CVE-2022-22965 (Spring4Shell) avec configuration de l'environnement, analyse de débogage pas à pas et décomposition de la chaîne d'exploitation pour une pratique en laboratoire éducatif.
Spring4Shell est le nom d'une CVE existant sur Spring Core du Spring Framework.
Avec un score CVSS 3.x de 9.8, la vulnérabilité est classée au plus haut niveau de risque (critique). Cette vulnérabilité permet à un attaquant d'exécuter du code d'exploitation à distance et de contrôler le serveur vulnérable.
Avec la popularité de Spring Core sur Internet et la gravité de l'impact de Spring4Shell, cette vulnérabilité est considérée par les experts comme ayant un impact non moins important que Log4shell.
Spring4Shell n'affecte pas toutes les applications web utilisant Spring Framework sur Internet ; elle exige que l'application web possède les éléments suivants :
Mon environnement de configuration aura les paramètres suivants :
Installer Apache Tomcat
Comme mentionné ci-dessus, j'utilise Kali 2021.4a et Apache Tomcat 9.0.45. Si vous ne savez pas comment installer Apache Tomcat et souhaitez le faire sur Kali Linux, vous pouvez consulter ce lien.
Note : Remplacez le lien https://mirror.kiu.ac.ug/apache/tomcat/tomcat-9/v9.0.45/bin/apache-tomcat-9.0.45.tar.gz par https://archive.apache.org/dist/tomcat/tomcat-9/v9.0.45/bin/apache-tomcat-9.0.45.tar.gz
Choisir un IDE
Nous avons besoin d'un IDE pour coder le projet, l'empaqueter en fichier .war et surtout pour le débogage. J'utilise Intellij, vous pouvez tout à fait utiliser Eclipse ou Netbeans,... Tant que cet IDE supporte Java.
Créer un projet simple contenant la vulnérabilité
Mon projet est extrêmement simple, composé de :
un modèle HelloWorld.java

un contrôleur HelloWorldController.java

une vue hello.jsp

Construire le fichier .war
Pour empaqueter le projet, procédez comme suit : Build -> Build Artifacts -> helloworld:war -> Build.
Attendez que la construction soit réussie ; le projet aura alors un dossier out supplémentaire. Allez dans ./out/artifacts/your_war_name/ et vous verrez un fichier your_war_name.war ; ce fichier .war est le projet web après compilation et empaquetage, et peut être déployé sur des servlets Java comme Apache Tomcat.
Si Build Artifacts est grisé (impossible de construire les artefacts), cela signifie que les artefacts de construction n'ont pas été configurés pour ce projet. Allez dans : File -> Project Structure -> Artifacts -> Supprimez tous les artefacts existants -> Add (signe +) -> Web Application: Exploded -> From Modules... -> OK (fin de création de l'exploded) -> Add (signe +) -> Web Application: Archive -> For ‘helloworld:war exploded’ -> OK. Ensuite, refaites Build Artifacts.
Déployer et configurer le débogage
Déploiement
Pour déployer un fichier .war sur Apache Tomcat, il suffit de copier le fichier .war dans le dossier /webapps à l'intérieur du dossier Apache Tomcat (ex: pour moi, je copie le fichier helloworld.war (renommé pour plus de simplicité) dans /opt/tomcat/apache-tomcat-9.0.45/webapps/). Ensuite, démarrez le serveur Tomcat de deux manières (pour Linux) :
Après le déploiement, accédez à http://localhost:8080/helloworld
Configuration du débogage
Pour configurer le débogage (à distance) de Tomcat, procédez comme suit :
Côté serveur :
ouvrez le fichier catalina.sh et remplacez la valeur localhost par l'ip_may_ao du paramètre JPDA_ADDRESS

redémarrez le serveur Tomcat avec : /opt/tomcat/apache-tomcat-9.0.45/bin/catalina.sh jpda start. À ce moment, en plus d'ouvrir le port 8080 pour le serveur HTTP, Tomcat ouvrira également le port 8000 pour que nous puissions nous connecter et déboguer.
Note : Dans cette partie de débogage, j'exécute Intellij sur Windows 10 et Tomcat sur une machine virtuelle Kali, donc il est nécessaire de changer JDPA_ADDRESS ; si vous configurez à la fois Intellij et Windows 10 sur la même machine, vous n'avez pas besoin de le changer.
Côté Intellij :
allez dans Run -> Edit Configurations... -> Add (signe +) -> Remote JVM Debug
Donnez un nom -> modifiez Host et Port avec l'ip et le port que vous avez modifiés dans le fichier catalina.sh -> OK -> Shift + F9 (lancer le débogage)

Tout d'abord, je vais analyser le projet que j'utilise pour le débogage ; comme mentionné ci-dessus, ce projet se compose simplement de :
J'ai un exemple comme suit :

L'application a récupéré les informations des paramètres de la requête POST et créé un objet helloWorld{"person":"Leo", "message":"Hi there"}, cet objet helloWorld est l'entrée de la fonction helloPost. L'application effectuera les opérations décrites ci-dessus pour renvoyer cette réponse à l'utilisateur.
Le processus de conversion des paramètres du corps de la requête POST en objet helloWorld est entièrement effectué automatiquement par Spring. Alors, comment fait-il et valide-t-il les paramètres saisis ?
Cette image a été capturée pendant le débogage, l'exemple a été réalisé avec une requête dont le corps était "class.module.classLoader.resources.context.parent.pipeline.first.directory=webapps/ROOT" (j'appelle la partie gauche (stack trace) (1) et la partie droite (2)) :

Dans (1), j'ai mis en évidence les points à noter (en regardant de bas en haut). Spring applique applyPropertyValue à l'objet helloWorld à partir des paramètres de la requête POST. Et si les paramètres sont simplement person=Leo&message=Hi%20there, Spring pourra trouver que le mot-clé 'person' correspond à helloWorld.person, 'message' correspond à helloWorld.message.
Mais Spring permet également d'envoyer des objets via une requête HTTP (dire envoyer un objet via HTTP est un peu exagéré, mais c'est l'idée). Je suppose que ma propriété person n'est plus une chaîne mais un objet Person, et Person comporte deux sous-propriétés : name (string) et age (int). Et pour envoyer les informations sur cet objet Person au serveur, cela se fera comme suit : "person.name=Leo&person.age=23".
Ainsi, le format des paramètres devient A.B.C.D… = X au lieu de A=X. Et pour traiter le format A.B.C.D… = X, par exemple un paramètre comme A.B.C = X, Spring fera simplement ceci : transformer la chaîne en getA.getB.setC(X).
Je n'expliquerai pas ce qu'est getA, mais je donnerai un exemple avec le cas où le corps de la requête est "person.name=Leo&person.age=23". Spring cherchera dans l'objet helloWorld s'il possède la propriété person et la méthode getPerson ; si oui, Spring appelle helloWorld.getPerson(). À ce moment, Spring obtient un objet de type Person, que j'appellerai temporairement person1, puis Spring cherche dans person1 s'il a une propriété 'name' et une méthode setName (car après 'name' il y a un signe '=') ; si oui, Spring appelle setName(Leo) pour person1.
Pour person.age, Spring ne recommencera pas depuis le début pour trouver person puis age ; il réutilisera les objets précédents, dans ce cas helloWorld et person1.
Après les étapes ci-dessus, le serveur aura un objet helloWorld{person:{name:"Leo", age:23}} (en ignorant temporairement la propriété message).
Alors, comment Spring peut-il trouver les propriétés de chaque objet, par exemple la propriété 'person' de l'objet helloWorld ?
Regardez le début de (1) avec la fonction CachedIntrospectionResults(beanClass), cette fonction liste les propriétés de beanClass. Vous voyez dans (2) que lorsque beanClass est model.HelloWorld, il y a 3 propriétés. Alors que le modèle HelloWorld que j'ai créé n'a que deux propriétés : 'person' et 'message'. Donc la fonction a renvoyé une propriété supplémentaire 'class', et si vous développez la ligne 'class', vous verrez que le type de propriété est 'java.lang.class'.
Donc nous pouvons influencer un objet class de type java.lang.classclass -> C'est la source de ce Spring4Shell.
En rapport avec la source ci-dessus, il existe un CVE-2010-1622 lié à cette source. L'auteur de CVE-2010-1622 a exploité cette source avec la payload class.classLoader.URLs[0] = X.
Parce que dans la classe java.lang.class, il y a une méthode getClassLoader() qui retourne un objet ClassLoader, et ce ClassLoader peut influencer le tableau URLs de Tomcat (utilisé pour charger les ressources). Et en pouvant influencer les URLs, un attaquant peut modifier la valeur de URLs[0] en une adresse URL pour effectuer un accès à distance à un fichier Jar malveillant (contrôlé par l'attaquant).
Pour corriger cette faille, Spring a mis en place un filtre (blacklist) dans la fonction CachedIntrospectionResults(beanClass) :

Si 'beanClass' == Class.class (java.lang.class), alors pd doit être différent de 'classLoader' et 'protectionDomain', et la preuve est qu'après que CachedIntrospectionResults ait chargé toutes les propriétés de java.lang.class, les deux propriétés 'classLoader' et 'protectionDomain' sont absentes :

Mais à la place, depuis JDK 9, Class.class a ajouté une propriété 'module' et dans Class.module il y a une propriété classLoader :

→ Ainsi, en utilisant JDK 9 ou version ultérieure, il est possible de contourner la blacklist de Spring !!!
Sur la base du PoC public de la faille Spring4Shell, on peut voir que la payload utilisée a la forme :
class.module.classloader.resources.context.parent.pipeline.first
⇔ Class.getModule().getClassLoader().getResources().getContext().getParent().getPipeline().getFirst()
Sur la base du débogage, on peut voir la chaîne de gadgets suivante :
java.lang.class.getModule() -> java.lang.module.getClassLoader() ->
org.apache.catalia.loader.ParallelWebappClassLoader.getResources() ->
org.apache.catalia.webresources.StandardRoot.getContext() ->
org.apache.catalia.core.StandardContext.getParent() ->
org.apache.catalia.core.StandardHost.getPipeline() ->
org.apache.catalia.core.StandardPipeline.getFirst() ->
org.apache.catalia.valves.AccessLogValue.
Et la classe AccessLogValue a les propriétés suivantes :

On peut appeler un objet AccessLogValue et cet AccessLogValue influence l'écriture des logs de Tomcat.
→ On peut créer un fichier sur le serveur en définissant les propriétés de l'objet AccessLogValue sur le serveur Tomcat. Pour ce faire, dans le PoC, ils ont défini les propriétés Prefix, Suffix, Pattern, Directory et fileDateFormat. Et la payload de la requête sera la suivante :
“class.module.classLoader.resources.context.parent.pipeline.first.pattern=%25%7Bprefix%7Di%20java.io.InputStream%20in%20%3D%20%25%7Bc%7Di.getRuntime().exec(request.getParameter(%22cmd%22)).getInputStream()%3B%20int%20a%20%3D%20-1%3B%20byte%5B%5D%20b%20%3D%20new%20byte%5B2048%5D%3B%20while((a%3Din.read(b))!%3D-1)%7B%20out.println(new%20String(b))%3B%20%7D%20%25%7Bsuffix%7Di&class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp&class.module.classLoader.resources.context.parent.pipeline.first.directory=webapps/ROOT&class.module.classLoader.resources.context.parent.pipeline.first.prefix=shell&class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=”
Liste des points d'arrêt
Pour faciliter le débogage, vous pouvez placer des points d'arrêt aux emplacements suivants :

Cette analyse n'a normalement pas de conclusion, cette section est ajoutée juste pour faire genre !!!
Si vous cherchez un correctif, le voici ici.