19 avril 2010

Comment obtenir un accès root en exploitant une faille XSS, le cas Apache

Contexte : L'infrastructure technique de la fondation Apache a été victime d'une attaque XSS suivie d'une intrusion dans les systèmes. Les administrateurs ont reporté cet incident en détail et avec transparence.

Ils ont fait état des erreurs qu’ils ont commises et ont établi des recommandations afin d'améliorer la sécurisation des infrastructures. Ce billet tente d'expliquer la partie XSS de l'attaque et d'offrir une analyse du point de vue du développeur, il livre enfin des recommandations pour vous aider à renforcer la sécurité de vos applications Web.

Tout a commencé par une simple déclaration d'anomalie (issue INFRA-2591) dans le JIRA de la fondation Apache, un message aussi bref qu'énigmatique contenant un lien masqué par Tinyurl :

«ive got this error while browsing some projects in jira http://tinyurl.com/XXXXXXXXX» (le Tinyurl a été brouillé intentionnellement).

Le lien recelait des intentions peu amicales : l'URL d'une page contenant une faille XSS complétée par un code JavaScript hostile. Le code JavaScript sous forme de paramètres HTTP fait deux choses : voler le cookie de session des administrateurs en l'envoyant à un serveur externe, puis, pour ne pas éveiller les soupçons, affiche une une stack trace Java dans pastie.org (site communautaire d'échange et de partage de texte) faisant croire qu'il y a eu vraiment un problème.

Ce lien a été créé sur mesure pour une JSP contenant une faille XSS. Cette JSP quelconque ne contient pas de code spécialement sensible, elle affiche seulement une boite de dialogue pour choisir des couleurs (colorPicker.jsp) :



Dans cette page, on récupère deux paramètres : element et defaultColor, ces deux paramètres sont directement recopiés dans la page sans aucun contrôle d'assainissement !



Pour exploiter cette faille, des valeurs pour element et defaultColor ont été judicieusement choisies, de façon à ce que le code ait un comportement spéciale. Si l'on observe bien les valeurs dans l'URL, elles sont presque inintelligibles, mais dès qu'on les injecte dans le code, celui-là change bizarrement de structure, on se rend compte rapidement de la supercherie : à la ligne 26 lorsque le moteur JavaScript exécute opener.document.jiraform.name, cela génère une erreur, le flux d'exécution entre dans le bloc catch dans lequel le navigateur envoie innocemment le cookie de l'utilisateur à un serveur, outrepassant carrément le
principe du Same Origin policy (SOP).

Le serveur en question n'est qu'un site Web hébergé gratuitement sur une banale plateforme d'hébergement : PHP, Mysql, etc. Aucun risque pour le pirate.




L'attaque ne s'arrête pas là, puisque le pirate a attaqué la mire de connexion par « recherche exhaustive » (brute force attack). Cette attaque n'a pas été détectée faute de présence d'outil adéquat (sa présence aurait aidé à détecter rapidement l'attaque).

Une fois introduit dans le système, le pirate a remplacé l’écran d’authentification par un autre lui permettant de collecter des mots de passe grâce à une archive JAR injectée, cela m’a tout de suite rappelé l'excellente étude de Philippe PRADOS qui démontre avec force détails la façon avec laquelle il est possible de compromettre une application Web en posant juste un fichier JAR. Les techniques sont nombreuses : injection d'une valve Tomcat, utilisation de l'AOP, de filtres J2EE, corruption de la configuration Spring ou la surcharge des parseurs XML du JDK (Un projet Google code consacré à cette étude http://code.google.com/p/macaron/ a été mis en place, on y trouve notamment un PDF d'une soixantaine de pages, rédigé en français : surprenant, didactique et très riche en informations).

Comment prévenir ce genre d'attaques ?

En tant que développeurs

Heureusement, il est possible de se prémunir contre ce genre d'attaque et de manière efficace. Il suffit d'observer quelques règles de programmation simples à mettre en œuvre :


Utilisez un filtre J2EE contre le XSS : il s'agit d'un filtre, qui se base sur des expressions rationnelles, et qui va analyser la requête afin d'y déceler des combinaisons de caractères non autorisées dans le corps de la requête (par exemple ?clientId=<script>...) et de prendre les mesures adéquates : traçage de la requête, envoi d'un mail à l'administrateur, assainissement des valeurs (déspécialisation des caractères spéciaux, etc.).

Validez votre filtre, en écrivant des tests unitaires basés sur les préventions XSS et sur les patterns XSS qu'il est facile de trouver sur la Toile.

Utilisez le flag httpOnly : httpOnly est un flag qui indique qu'un cookie ne peut être utilisé que pour les échanges HTTP. Aucun script ne pourra y accéder, même en présence de faille XSS, il sera impossible de subtiliser le cookie. Le flag httpOnly fonctionne sur tous les navigateurs récents (y compris la famille IE puisqu'il a été inventé en 2002 par Microsoft).

Le flag est concaténé à l'entête HTTP Set-Cookie de la façon suivante :

Set-Cookie: SESSIONID=XXXXX; path=/;HttpOnly

Pour activer ce flag dans Tomcat, il suffit d'ajouter useHttpOnly=True dans l'élément <Context> du fichier context.xml de l'application Web.

<Context useHttpOnly="true" docBase="...">

...

</Context>

Utilisez un Firewall applicatif ou WAF (Web Application Firewall) : bien que cela intéresse surtout les opérateurs de production. Il est important de connaitre cette famille d'outils.
Un WAF intervient au niveau Application du modèle OSI (différemment des firewalls qui analysent quant à eux le niveau réseau), il connait les protocoles et est capable de détecter des attaques en analysant le contenu des échanges (injection SQL, XSS, etc). Il se présente sous forme d'alliance ou sous forme de programme tel que mod_security (s'installe comme plugin dans le serveur Web Apache).


Utilisez un outil d'analyseur statique de code : un outil tel que Findbugs, permet de détecter des faiblesses dans le code (par exemple, récrire directement un paramètre de la requête HTTP dans la réponse, ou inclure directement ce paramètre dans un statement SQL).




En tant qu’utilisateurs

Ne cliquez jamais sur un lien rétréci (tinyurl, bitly, etc). Si vous utilisez Twitter, l'utilisation de réducteur d'URL est inévitable. Il existe, toutefois, des plugins pour les navigateurs qui décodent à la volée les liens raccourcis (http://longurl.org/tools ou http://www.longurlplease.com/ par
exemple).



Ne cliquez jamais sur un lien envoyé par une personne inconnue (en plus du risque de XSS, s'ajoute le risque d'attaque XSRF), même si vous connaissez la personne, il se peut que son identité soit usurpée.


Armez-vous d'un plugin de sécurité dans votre navigateur. Depuis que j'utilise NoScript, je ne peux plus m'en passer. À l'occasion de cet article, j'ai éprouvé l'efficacité de ce plugin sur la page piégée, le résultat ne m'a pas déçu (voir capture d'écran suivante).





Limitez la durée de vie de vos cookies : ne pas cocher «Se souvenir de moi» lorsque vous connectez.

Conclusion

Cette affaire nous a montré qu'il suffit d'une faille, d'un relâchement de la vigilance des utilisateurs pour que la sécurité d'un système informatique s'effondre comme un château de cartes. Les espaces collaboratifs d'entreprise se multipliant (l'explosion des offres SaaS et l'émergence des réseaux sociaux pour entreprises en témoignent), la compromission d'un seul compte utilisateur peut potentiellement affecter la sécurité du système d'informations : vol de documents confidentiels, accès aux dossiers clients (CRM), dégradation, etc.. Cela est lié à la sophistication grandissante des technologies qui peut déstabiliser les utilisateurs et fléchir leur attention. Il est donc primordial, de les sensibiliser ainsi que les développeurs aux problèmes de sécurité et de former ces derniers aux parades aux techniques d'attaque Web 2.0. Enfin, rendre les audits de sécurité systématiques et ne plus les sacrifier sur l’autel des « deadlines » nos assurera des livrables plus fiables et qui, à long terme, rentabiliseront largement l'investissement dans la sécurité.

11 mars 2009

JBoss TattleTale, un outil de vérification de dépendances

Actuellement, j'effectue la migration vers Maven de quelques applications historiques. La difficulté de cette tâche réside dans le dépouillement et l'inventaire des fichiers JAR desquels dépend une application. En effet, on se retrouve face à un amas de fichiers JAR parmi lesquels se trouvent des fichiers JAR doublons (avec un nom différent ou une version différente), des fichiers JAR appartenant au serveur d'applications (par exemple, servlet.jar ou connector.jar), d'autres qui ne sont pas ou plus utilisés par l'application et parfois même des fichiers JAR de bibliothèques de tests unitaires (par exemple junit.jar). Le travail consiste alors à ne garder que les JAR qui sont réellement utilisés et d'éliminer (avec la plus grande précaution) tous les autres. Il va sans dire que s’attaquer à un tel problème à « mains nues » n'est pas une mince affaire.

C'est en cherchant un outil de vérification de dépendances que je suis tombé sur l'utilitaire « JBoss Tattletale », qui à ma grande chance vient de sortir (en version bêta) des laboratoires de JBoss (voir l'annonce sur le Blog de JBoss).
Cet outil permet de vérifier les dépendances entre les fichiers JAR présents dans un même répertoire (considéré comme un classpath). Ses fonctionnalités les plus remarquables sont :
  • Identifier les dépendances entre les fichiers JAR (par exemple, hibernate-3.1.3.jar dépendant de antlr-2.7.6rc1.jar)
  • Lister les classes dont dépend un fichier JAR et lister celles qu'il expose.
  • Lister les classes dont dépend un JAR, mais qui sont absentes du classpath.
  • Lister les classes présentes dans le classepath et les fichiers JAR dans lesquels elles se trouvent
  • Alerter si une classe est présente dans plusieurs fichiers JAR.

L'outil s'utilise en ligne de commande et sa prise en main est rapide. Il génère un rapport au format HTML dans lequel on peut naviguer aisément (voir la capture d'écran ci-contre).

Cet outil m'a vraiment aidé à effectuer la vérification des dépendances lors de la phase préliminaire de migration vers Maven. Malheureusement, il ne dispose pas (pour l'instant?) de plugin Maven permettant l'automatisation de cette tâche (de son coté, ANT dispose déjà d'une tâche tattletale).
Enfin, si vous vous demandez ce que veut dire «tattletale» sa définition se trouve ici. En résumé, «tattletale» est une personne commère qui révèle les secrets. On voit bien que e nom n'a pas été choisi par hasard !
L'outil, qui me semble très prometteur, en est tout juste à ces débuts. Souhaitons-lui un bel avenir!

Liens utiles :

Téléchargement : http://sourceforge.net/project/showfiles.php?group_id=22866&package_id=311046&release_id=665534 (800ko environ)

JIRA : https://jira.jboss.org/jira/browse/TTALE


6 mars 2009

Comment lire la version d'un JAR à partir du fichier « Manifest » ?


Toute application doit avoir un numéro de version. Il permet d'identifier aisément, entre autres, la branche du code source à l'origine de sa création (pour corriger des bogues, effectuer des évolutions, etc.). La technique la plus répandue c'est d'écrire le numéro de version dans un fichier de propriétés (properties) dont le contenu pourrait ressembler à ce qui suit :

application.version=1.0b

Il est alors possible à l'application d'extraire le numéro de version pour l'afficher dans la barre de titre, par exemple, ou l'utiliser dans la trace applicative. Ce procédé vous oblige à maintenir le fichier et à veiller à incrémenter correctement le numéro de version.

Bonne nouvelle si vous utilisez Maven : vous n’aurez plus à effectuer cette tache ! En effet, Maven inscrit systématiquement le numéro de version dans le Manifest (MANIFEST.MF) des JAR qu'il produit (le numéro de version est exactement le même que celui qui figure dans le POM du projet Maven).

Comment extraire le numéro de version ?

Commençons par un code simple. Cela se fait en deux étapes : charger le fichier MANIFEST.MF, puis lire son contenu grâce à la classe java.util.jar.Manifest.

La classe suivante est « packagée » dans un jar nommé version.jar.
public class VersionUtil {

public static void main(String[] args) throws IOException {
System.out.println(readVersion());
}

public static String readVersion() throws IOException {

InputStream in = VersionUtil.class.getResourceAsStream("/META-INF/MANIFEST.MF");

Manifest manifest = new Manifest(in);

// Lire la propriété "Implementation-Version" du Manifest

String version = manifest.getMainAttributes().getValue(Attributes.Name.IMPLEMENTATION_VERSION);

return version;

}
}

Le code ci-dessus fonctionne sans erreur, mais affiche quand même un résultat erroné : 1.5.0_10. Il correspond en réalité au numéro de version du JDK. Le ficher MANIFEST.MF chargé n'était «manifestement» pas celui du JAR en question, mais celui de rt.jar (le jar qui contient les classes de base de Java). L'explication est la suivante : la méthode getResourceAsStream(..) délègue la lecture de MANIFEST.MF au chargeur de classe (classloader) de la classe VersionUtil.class, or ce chargeur de classe se trouve dans rt.jar et non dans version.jar. Pour qu'une classe puisse charger le fichier MANIFEST.MF du JAR dans lequel elle se trouve, elle recourt à une méthode utilitaire qui calcule le chemin dudit JAR. Elle déduit ensuite l'emplacement du Manifest.

L'extrait de code Java montre comment le chemin vers le MANIFEST.MF est trouvé. J'avoue que le code est un peu alambiqué, mais heureusement il est commenté. La méthode présente l'avantage de fonctionner, et pour une classe se trouvant dans un jar, et pour une classe se trouvant dans un répertoire. Pour une meilleure compréhension, j'ai mis en commentaires le contenu des variables à chaque étape, et ce, pour les deux cas précités.
private static String getPathToManifest(){

// 1 - Lire le nom de la classe
String classSimpleName = VersionUtil.class.getSimpleName() + ".class";
// classSimpleName = VersionUtil.class

// 2 - Récupérer le chemin physique de la classe
String pathToClass = VersionUtil.class.getResource(classSimpleName).toString();

// pathToClass = file:/C:/workspace/VersionUtil/bin/com/abdennebi/version/VersionUtil.class
// pathToClass = jar:file:/C:/version.jar!/com/abdennebi/version/VersionUtil.class

// 3 - Récupérer le chemin de la classe à partir de la racine du classpath
String classFullName = VersionUtil.class.getName().replace('.', '/') + ".class";
// classFullName = com/abdennebi/version/VersionUtil.class

// 4 - Récupérer le chemin complet vers MANIFEST.MF
String pathToManifest = pathToClass.substring( 0, pathToClass.length() - (classFullName.length())) + "META-INF/MANIFEST.MF";
// pathToManifest = file:/C:/workspace/VersionUtil/bin/META-INF/MANIFEST.MF
// pathToManifest = jar:file:/C:/version.jar!/META-INF/MANIFEST.MF

return pathToManifest;
}
L'exemple complet se trouve à cet endroit VersionUtil.java. Une modification pour Java 1.4 se trouve ici : VersionUtil14.java.

16 janvier 2009

Maven, considérer le répertoire source comme un répertoire de ressources

Par défaut, Maven sépare les sources java des autres ressources, telles que les fichiers properties ou les fichiers XML. Ceci, constitue en général, une bonne pratique. Cependant, si vous migrez une application et que vous ne désirez pas séparer les ressources des sources Java, il est impératif d'en informer Maven, sinon vous risquerez d'avoir des surprises (un WAR sans le moindre fichier properties).

Pour ce faire, ajoutez ce bout de code XML au fichier POM de votre projet :


...


src/main/java

**/*.java



...

Cette déclaration informe Maven que le répertoire src/main/java contient des ressources et qu'il ne faut pas prendre en compte les fichiers Java (sinon ils risquent d'être inclus dans l'artéfact généré par Maven).

9 janvier 2009

Ajoutez un deuxième répertoire source à votre projet Maven

Actuellement, je m'occupe de la migration du code d'une application historique vers Maven. Cette application contient deux répertoires sources Java, l'un contient du code généré (des proxies PacBase), et l'autre contient le code écrit par le programmeur. Pour ce faire, il faut indiquer à Maven d'utiliser un répertoire supplémentaire à savoir src/main/javaproxie (en plus du classique src/main/java).

Maven permet de définir un répertoire source différent du répertoire source conventionnel grâce à la déclaration suivante (en prenant comme exemple src/main/javaproxies):


src/main/javaproxies


Le problème est qu'il n'est pas possible d'ajouter (nativement) un nouveau répertoire source. C'est plutôt le rôle du plugin build-helper et son goal add-source, il permet, en effet, de définir plusieurs répertoires sources de la manière suivante :




org.codehaus.mojo
build-helper-maven-plugin


add-source
generate-sources

add-source



src/main/javaproxies







En résumé, dans des situations où il est nécessaire d'utiliser un répertoire source supplémentaire, par exemple du code généré par xDoclet, des stubs et proxies Corba ou Web Services, ce plugin vous sauvera la vie !

17 décembre 2008

Connaitre la version d'un driver JDBC

Un des soucis majeurs d'un développeur Maven est de tomber sur un JAR propriétaire et par-dessus le marché sans aucune indication de version. Tel était mon cas. Actuellement, je migre une ancienne application vers Maven, elle utilise un driver DB2 dont le JAR ne contient pas le numéro de version qu'on trouve habituellement dans META-INF/MANIFEST.MF. Heureusement, JDBC prévoit la possibilité de récupérer le numéro de version d'un driver et ce, à l'aide de la classe DatabaseMetaData.

Le code suivant charge le driver JDBC, puis se connecte à la base de données (l'inconvénient est l'obligation de se connecter à une base de données, je n'ai pas trouvé mieux !), il fait ensuite appel à la méthode getDriverVersion().


import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.DriverManager;
import java.sql.SQLException;

public class Version {

public static void main(String args[]) throws InstantiationException,
IllegalAccessException, ClassNotFoundException, SQLException {
Class.forName("com.ibm.db2.jcc.DB2Driver").newInstance();
Connection con = DriverManager.
getConnection("jdbc:db2://<nom de la base>", "<utilisateur>", "<mot de passe>");
DatabaseMetaData dmd = con.getMetaData();
System.out.println("La version du driver est : " + dmd.getDriverVersion());

}
}

24 décembre 2007

Quelle est la différence entre ClassNotFoundException et NoClassDefFoundError

En tant que développeurs Java, nous avons tous rencontré ces deux exceptions. Ceci dit, elles sont souvent confondues, car elles signalent toutes les deux qu'une classe n'a pu être chargée.
Premièrement, ClassNotFoundException est une Exception alors que NoClassDefFoundError est de type Error.

D'après la javadoc ClassNotFoundException est renvoyée lorsqu'une application essaye de charger dynamiquement une classe à travers son nom, en utilisant entre autre la méthode Class.forName(), mais la classe n'est pas trouvée dans le CLASSPATH.

Exemple

Soit la classe suivante :

public class A {
public static void main(String[] args) throws ClassNotFoundException {
Class.forName("ClasseQuiNexistePas");
}
}

Son exécution prévoquera l'exception suivante :


Exception in thread "main" java.lang.ClassNotFoundException: ClasseQuiNexistePas
at java.net.URLClassLoader$1.run(Unknown Source)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(Unknown Source)
at java.lang.ClassLoader.loadClass(Unknown Source)
at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source)
at java.lang.ClassLoader.loadClass(Unknown Source)
at java.lang.ClassLoader.loadClassInternal(Unknown Source)
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Unknown Source)
at A.main(A.java:6)



NoClassDefFoundError est renvoyée lorsque une Classe A ne trouve pas une autre classe B delaquelle elle dépend, alors qu'au moment de la compilation la classe B est présente.

Exemple
Soit les deux classes A et B :

class A {
public static void main(String[] args) {
B b = new B();
}
}

public class B { }


Compilez les deux classes, ensuite supprimer le fichier B.class et lancer la classe A (java A)

Résultat

Exception in thread "main" java.lang.NoClassDefFoundError: B
at test2.A.main(A.java:6)