Sécurisez vos formulaires sur le web
Date de publication : 06 décembre 2009
Par
kmdkaci (Ma page perso)
Dans ce présent tutoriel, je vais tenter de vous livrer quelques exemples de failles vulnérables présentes fréquemment sur le web. Mon but n'est pas de vous présenter les différentes techniques utilisées pour pirater des données, ni de vous initier à des pratiques malsaines. Au contraire, le but de ce tutoriel est de vous sensibiliser sur les risques causés par ces failles, afin de les éviter.
Comme disait Tristan Colombo dans son article (Linux Magazine n° 116 mai 2009) : "Un développeur pressé est un développeur qui laisse des failles béantes dans son application". Effectivement pour des raisons de calendrier ou des pressions dues au cahier des charges, il est devenu coutumier de laisser quelques failles de sécurité qui traînent dans les codes.
Les exemples présentés dans ce tutoriel sont d'ordre pédagogique, simplifiés au maximum pour faciliter la compréhension, car cet article s'adresse aussi à des débutants en programmation.
8 commentaires
I. Introduction
II. Qu'est-ce qu'un formulaire ?
III. Les différentes menaces du web
III-A. Attaques par Spam
III-A-1. Robots générateurs de spams
III-A-2. Halte au spam via les formulaires
III-A-3. Captcha comme exemple
III-B. Attaque par injection SQL
III-B-1. Bien gérer les exceptions
III-C. Eviter les attaques par bombing
III-D. Limiter les tentatives d'authentification
IV. Les outils
IV-A. Firebug
IV-B. Web Developper
IV-C. WebGoat
IV-D. Wireshark
IV-D-1. Exemple d'utilisation : Décryptage d'un mot de passe
V. Conclusion
VI. Liens
VII. Remerciements
I. Introduction
Tout au long de ce tutoriel, nous allons utiliser un exemple de site Internet d'un club de sport. Les scénarios présents sont décrits par une interaction entre l'internaute et
le serveur via des formulaires susceptibles de véhiculer des failles.
Les outils utilisés ici sont le langage Java côté serveur. Le conteneur de servlets est Tomcat, installé sous Linux Debian Lenny, mais ces exemples s'appliquent aussi à d'autres environnements.
II. Qu'est-ce qu'un formulaire ?
Un formulaire est un moyen d'interagir avec un serveur. Il est généralement composé des champs de saisie, des cases à cocher ou des sélections. Les informations saisies seront ensuite envoyées vers le serveur qui va les traiter, et dans certains cas rendre une réponse à l'internaute.
Les formulaires servent à véhiculer les informations saisies par l'utilisateur généralement via le protocole HTTP qui lui emprunte un port quelconque. Un serveur sécurisé est aussi un serveur qui ferme les ports non utilisés. Si vous n'avez pas de service sur un port quelconque, il faut le fermer pour éviter des intrusions via cette porte. C'est là que la rigueur intervient pour empêcher toute manœuvre malveillante.
Pour illustrer les différentes attaques, mais aussi proposer des solutions, nous allons tout au long de ce tutoriel prendre comme cas le site internet d'un club de sport.
Voici un exemple de formulaire qui permet d'envoyer des informations au serveur. Il s'agit du nom, prénom et de la discipline sportive choisie par l'adhérent.
Voici le code HTML que décrit ce formulaire :
| inscription.html |
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>Inscription à une activité</title>
</head>
<body>
<FORM action="http://localhost:8080/formulaire/Controlleur" method="Post">
Nom<INPUT name="nom">
Prénom<INPUT name ="prenom">
<P> Adresse <TEXTAREA style="WIDTH: 184px; HEIGHT: 56px" name= "adresse" ></TEXTAREA>
Activité <SELECT name=activite>
<OPTION value="basket" selected>basket</OPTION>
<OPTION value="handball">handball</OPTION>
<OPTION value="escrime">escrime</OPTION>
<OPTION value="Gymnastique">Gymnastique</OPTION>
<OPTION value="Danse">Danse</OPTION>
<OPTION value="Musculation">Musculation</OPTION>
</SELECT></P>
<INPUT type="submit" value="Envoyer">
<INPUT type="reset" value="Reset">
</FORM>
</body>
</html>
|
Une fois que l'utilisateur actionne le bouton envoyer, les informations seront transmises sur le serveur via la méthode Post. Ces informations seront interceptées par un programme côté serveur qui va les interpréter et dans la majorité des cas, les enregistrer dans une base de données ou les transmettre par mail.
Voici le code de la servlet Controlleur.java qui va recevoir les informations envoyées par le formulaire puis les oriente vers affiche.jsp pour les afficher.
| Controlleur.java |
import java.io.IOException;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
public class Controlleur extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
doPost(request, response);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
HttpSession session = request.getSession(true);
String cNom = request.getParameter("nom");
String cPrenom = request.getParameter("prenom");
String cAdresse = request.getParameter("adresse");
String cActivite = request.getParameter("activite");
session.setAttribute("nom", cNom);
session.setAttribute("prenom", cPrenom);
session.setAttribute("adresse", cAdresse);
session.setAttribute("activite", cActivite);
RequestDispatcher dispat = request.getRequestDispatcher("affiche.jsp");
dispat.forward(request,response);
}
}
|
Une fois ces informations envoyées depuis le formulaire via la méthode Post, elles seront interceptées par la méthode doPost. Ces informations seront mises dans la session puis envoyées vers affiche.jsp pour les afficher.
Cette façon de communiquer entre l'internaute et le serveur permet une interactivité simple, mais les formulaires présentés tels quels sont loin d'être sécurisés. Nous allons aborder quelques attaques susceptibles de se produire quand il y a interaction entre internaute et serveur.
III. Les différentes menaces du web
III-A. Attaques par Spam
III-A-1. Robots générateurs de spams
Le spam est un courrier envoyé généralement à des fins commerciales et publicitaires. Mais aussi, il peut avoir un but malhonnête comme les arnaques afin de soustraire de l'argent ou des données personnelles.
Actuellement, il existe des programmes, nommés Robots qui parcourent les pages web à la recherche d'éventuels formulaires. Le principe consiste à extraire les champs des formulaires avec les différents paramètres pour ensuite les utiliser à d'autres fins, sans l'intervention d'un être humain lors du traitement.
Imaginons que le formulaire précédent, une fois validé, sera transmis par mail au secrétariat du club du sport.
Si un robot s'empare de ces informations, il pourra utiliser ce formulaire pour envoyer un texte à des fins commerciales ou publicitaires. C'est cette possibilité que nous allons essayer de vous exposer.
Reprenons le formulaire précédent. Nous allons tenter d'envoyer les mêmes informations sur le même serveur via la méthode doPost, mais cette fois sans utiliser le formulaire tel qu'il a été présenté précédemment.
Voici le code Java de la classe TestForm.java qui permet de simuler cette étape.
| TestForm.java |
package classes;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.URL;
import java.net.URLEncoder;
import java.util.Enumeration;
import java.util.Hashtable;
public class TestForm {
public void envoyerEnPost(String lien, Hashtable<String, String> paramsEnvoi) throws IOException{
OutputStreamWriter out = null;
BufferedReader in = null;
try {
StringBuilder donnees = new StringBuilder("") ;
Enumeration<String> lesCles = paramsEnvoi.keys();
Enumeration<String> lesValeurs = paramsEnvoi.elements();
while (lesCles.hasMoreElements())
{
donnees.append(URLEncoder.encode(lesCles.nextElement().toString(), "UTF-8"));
donnees.append("="+URLEncoder.encode(lesValeurs.nextElement().toString(), "UTF-8")+"&");
}
String donneeStr = donnees.toString();
donneeStr = donneeStr.substring(0, donneeStr.length()-1);
URL url = new URL(lien);
java.net.URLConnection conn = url.openConnection();
conn.setDoOutput(true);
out = new OutputStreamWriter(conn.getOutputStream());
out.write(donneeStr);
out.flush();
in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String ligne;
while ((ligne = in.readLine()) != null)
{
System.out.println(ligne);
}
}catch (Exception e)
{
e.printStackTrace();
}finally
{
out.close();
}
}
}
|
Cette classe consiste à envoyer des données sur le serveur et précisément vers la servlet située à l'adresse décrite par la variable lien. La Hashtable paramsEnvoi contient la liste des paramètres du précédent formulaire.
L'idée est de concaténer ces paramètres avec le séparateur & et de les mettre dans la variable donnees puis les envoyer via l'objet OutputStreamWriter.
L'objet InputStreamReader nous servira uniquement pour des besoins de test afin d'intercepter le résultat renvoyé par le serveur.
Voici un exemple d'une JSP décrit par le fichier testerEnvoi.jsp qui permet d'invoquer la classe précédente avec les mêmes paramètres du formulaire du club de sport.
| testerEnvoi.jsp |
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<%@page import="classes.TestForm"%>
<%@page import="java.util.Hashtable"%>
<%@page import="java.util.Enumeration"%>
<html>
<head>
<title>Test formulaire sans méthode Post</title>
</head>
<body>
<%
TestForm tester = new TestForm();
Hashtable<String, String> hashtable = new Hashtable<String, String>();
hashtable.put( "nom", new String("Koci") );
hashtable.put( "prenom", new String("Clémentine") );
hashtable.put( "email", new String("Tinna@domaine.com") );
hashtable.put( "activite", new String("Danse") );
try
{
tester.envoyerEnPost("http://localhost:8080/formulaire/Controlleur", hashtable);
}
catch(Exception ex)
{
System.out.println("Exception : " + ex );
}
%>
</body>
</html>
|
III-A-2. Halte au spam via les formulaires
Afin d'éviter l'envoi des données du formulaire par un robot, il faut ajouter une étape où l'être humain devra intervenir. Il existe plusieurs solutions pour mettre en place ce concept. L'idée la plus répondue est de mettre une image constituée à partir d'une suite de lettres et de chiffres sur le formulaire.
Le principe consiste qu'à chaque appel de la page du formulaire, une image déformée sera générée et renvoyée en même temps que le formulaire. L'internaute est obligé de saisir ce qu'il voit sur l'image pour valider l'envoi du formulaire.
Voici un exemple d'une image renvoyée par le serveur où il faut taper le mot "prapalis" pour valider l'envoi.
Pour protéger le formulaire du club de sport, nous allons ajouter une image afin que chaque utilisateur qui remplit les champs doive saisir les caractères qu'il voit sur l'image.
La classe suivante GenerImage.java permet de générer une image à partir d'une autre image en ajoutant des caractères non alphabétiques (lignes) afin que l'image soit déformée.
| GenerImage.java |
import java.util.Random;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
public class GenerImage {
public void generAleat(){
int chiffre =0;
StringBuilder cAlphabet = new StringBuilder("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789");
StringBuilder chaine = new StringBuilder("");
Random r = new Random();
for ( int i = 1; i<= 5; i++ ){
chiffre = r.nextInt(cAlphabet.length());
chaine = chaine.append( cAlphabet.charAt(chiffre) );
}
ecrireSurImage(chaine.toString());
}
public void ecrireSurImage(String chaine){
BufferedImage im = null;
try
{
im = ImageIO.read(new File("image.png"));
Graphics2D g2d = im.createGraphics();
g2d.setPaint(Color.red.darker());
g2d.setFont(g2d.getFont().deriveFont(20f));
g2d.drawString(chaine,5 , (int)(im.getHeight()/1.5));
g2d.drawLine(0, 0, im.getWidth(), im.getHeight());
g2d.drawLine(0,im.getWidth(), im.getHeight(),0);
g2d.drawLine(0,im.getWidth()/2, im.getHeight()/2,0);
g2d.drawLine(im.getHeight()/2,im.getHeight(), im.getWidth(),0);
g2d.dispose();
ImageIO.write(im, "png", new File("image1.png"));
} catch (IOException e)
{
e.printStackTrace();
}
}
}
|
Détaillons le code de cette classe :
Dans la méthode main, on génère à chaque fois un nombre entre 0 et la taille de l'alphabet. Puis on extrait le caractère correspondant dans la chaîne cAlphabet.
La variable chaine permet de constituer une chaîne de 5 caractères qu'on passe en paramètre à la méthode ecrireSurImage.
La méthode ecrireSurImage permet de transformer un texte en image grâce à la méthode drawString de la classe Graphics2D.
Afin de déformer l'image, il suffit de dessiner quelques petites barres à travers cette image avec la méthode drawLine.
Une fois notre image générée, elle sera chargée à chaque fois qu'on fait appel au formulaire.
Lors d'un submit, le serveur comparera le contenu de l'image saisi avec la valeur de la variable chaine constituée précédemment. Si les deux valeurs sont identiques, cela signifie qu'un être humain est intervenu et qu'il s'agit bel et bien d'un envoi non automatique.
Cependant, il existe des robots capables de traduire l'image même déformée vers le format texte (exemple des programmes OCR fournis avec les scanners). Mais plus on déforme l'image, plus il est difficile pour un robot de lire le contenu de l'image.
Voici à quoi ressemblera le nouveau formulaire :
Toutefois, il existe des outils puissants qui permettent de réaliser le principe décrit précédemment. Captcha est l'un de ces outils.
III-A-3. Captcha comme exemple
Captcha est un utilitaire puissant et multiplateforme qui permet de réaliser le même principe que précédemment, mais avec plusieurs particularités importantes.
Lors de votre inscription sur
developpez.com, étape nécessaire pour la contribution au forum, il y a de fortes chances que vous ayez rencontré ce type d'image.
En plus du principe décrit par l'exemple précédent, on retrouve dans ce formulaire une option qui permettra d'émettre un son du texte affiché, ainsi qu'un bouton pour recharger et un autre pour l'aide. Mais la particularité la plus importante est signalée par le texte "Stop spam. Read books". Qu'en est-il au juste ?
Contrairement au premier exemple décrit par notre formulaire, ici l'internaute reçoit deux mots au lieu d'un. La première partie servira pour valider le formulaire, chose habituelle jusqu'à présent. Mais qu'en est-il de la deuxième partie du mot ?
Afin d'aider à transcrire en numérique les journaux et ouvrages anciens, Captcha est l'une des solutions venues au secours de la numérisation.
En effet, lors de la numérisation de ces ouvrages anciens, certains mots ne sont pas reconnus. Les logiciels qui permettent de scanner les documents ne sont pas arrivés à déchiffrer ces mots. Une solution est née et consiste à recenser toutes les images des mots non reconnus afin de constituer une base de données. Puis à chaque demande du formulaire, on transfère à l'internaute un mot constitué par une image déformée et un mot extrait de la base de données des mots non reconnus. Une fois les deux mots saisis - celui généré par le serveur pour validation et celui extrait dans la base donnée des mots non reconnus - le formulaire sera validé, mais aussi, encore une fois la base de données est mise à jour pour enregistrer le mot déchiffré. Avec ce principe tout internaute ayant validé un formulaire, participe à l'aide de certains organismes ou universités afin d'améliorer la transcription des anciens ouvrages en numérique.
Pour utiliser Captcha dans ses pages web, il suffit d'utiliser une API mise à disposition et qui existe pour plusieurs langages comme JAVA, PHP, Python, PERL, Ruby, ASP.NET.
Ce système de filtrage ne garantit pas une sécurisation des formulaires à 100%. Il existe des robots pouvant scanner ces images et les déchiffrer de temps en temps. Mais cela contribue à freiner efficacement ce genre d'attaque.
Réduire les spams via les formulaires est possible, mais dire qu'on peut les anéantir complètement est un mythe.
III-B. Attaque par injection SQL
Une attaque par injection SQL consiste à exploiter une faille de sécurité d'une application qui interagit directement avec une base de données.
Ce genre d'attaque est l'une des plus utilisées sur le net. Il existe beaucoup de sites Internet, professionnels ou autres, qui laissent passer ce genre de failles.
Le principe consiste à saisir du code SQL dans les champs du formulaire à la place des données ordinaires.
Prenons un exemple simple et concret : le formulaire suivant servira à l'authentification d'un adhérent sur le site. Le login et le mot de passe sont obligatoires. Le bouton Envoyer permet d'envoyer vers le serveur les valeurs saisies. Ces informations seront traitées et comparées avec les données stockées dans une base de données côté serveur.
L'une des erreurs commises généralement par les développeurs, c'est de ne pas séparer les parties distinctes du programme, comme le fait le Modèle MVC cher à Java.
Nous allons simuler cette attaque et détailler les étapes de progression de la menace.
Cette attaque devient menaçante quand le code côté serveur ressemble à peu près au code suivant qui décrit une manière simple d'authentification.
| Authentif.java |
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String cLogin = request.getParameter("Login");
String cPassword = request.getParameter("Password");
String laConnexion = "Select * from animateur where email ='"+cLogin +"'"+ " and password='"+cPassword+"'";
try{
seConnecter();
Statement stat_req = (Statement) connecter.createStatement();
ResultSet rs1 = stat_req.executeQuery(laConnexion);
if (rs1.next()){
System.out.println("Vous êtes connectés");
}
}catch(Exception ex)
{
System.out.println("Ereur "+ex);
}
}
|
La méthode doPost récupère les paramètres du formulaire et les compare à ceux stockés en base.
Voici la requête qui permet de réaliser l'authentification en comparant les informations saisies et celles présentes dans la base.
| requête d'authentification |
String laConnexion = "Select * from animateur where email ='"+cLogin +"'"+ " and password='"+cPassword+"'";
|
Imaginons, maintenant que l'internaute saisit son login normalement, mais à la place du mot de passe saisit l'expression suivante :
toto' OR '1'='1
Dans ce cas la requête précédente devient :
| requête avec injection du code malveillant |
String laConnexion = "Select * from animateur where email ='"+cLogin +"'"+ " and password='"+toto' OR '1'='1+"'";
|
La requête n'aura pas la même signification que la première. L'expression toto' OR '1'='1 permet de retourner vrai quelques soit le mot de passe saisi, puisque '1' est toujours égal à '1'. L'accès à la base est ainsi réalisé.
Cette requête n'est qu'un exemple parmi tant d'autres. On peut même insérer toute une requête en ajoutant le mot clé Union.
Solution : Afin d'éviter ce genre d'attaque, il est utile de respecter les normes de programmation en séparant les traitements de toutes les parties distinctes du programme. Mais surtout, effectuer des validations avant toute interaction avec la base de données.
Lorsque les informations du formulaire servent à alimenter une base de données, ou une boîte mail, il est primordial d'intercepter et de valider ces données avant leur sérialisation.
L'utilisation des frameworks comme Struts est aussi une solution efficace et relativement sécurisée.
III-B-1. Bien gérer les exceptions
Si les exceptions dans le programme sont mal interceptées, cela risque de renvoyer au client un message expliquant l'erreur, et qui pourra comporter des données sensibles, comme les noms des variables, les noms des fichiers de sérialisation comme les bases de données et même les fichiers de paramétrage.
Quelle sera l'ampleur de l'impact si un nom d'une table de données s'affiche ? Ceci aidera le hacker à utiliser ces noms pour mieux concevoir ses requêtes.
| Gérer les exceptions |
try
{
Traitement 1
}
}catch(Exception ex)
{
}finally
{
Traiteemnt2
}
|
III-C. Eviter les attaques par bombing
Le mot bombing signifie bombardement. Le principe consiste à envoyer plusieurs requêtes successives sur le serveur afin de saturer une table, une boîte mail ou un disque.
Revenons au site du club de sport. Le formulaire suivant permet à l'internaute de saisir ses cordonnées, notamment son mail afin de s'inscrire à la news letter.
Imaginons que les données envoyées par ce formulaire serviront à alimenter une base de données ou un fichier qui permet de stocker ces informations. Tout ce qui est logique jusqu'à présent.
Voici la partie du code de la méthode doPost qui récupère les informations envoyées via le formulaire.
| Récupération des informations et stockage dans un fichier txt |
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String cNom = request.getParameter("nom");
String cPrenom = request.getParameter("prenom");
String cEmail = request.getParameter("email");
FileWriter writer = null;
String texte = cNom+" "+cPrenom+" "+cEmail +"\n";
try{
writer = new FileWriter("SavNewsLetter.txt", true);
writer.write(texte);
}catch(IOException ex)
{
ex.printStackTrace();
}finally
{
if(writer != null){writer.close();}
}
}
|
Rien de plus simple que ce que fait le code de la méthode doPost. Elle récupère les informations du formulaire, puis alimente le fichier texte SavNewsLetter.txt.
Pour des raisons de simplicité du tutoriel, nous avons utilisé le fichier texte à la place d'une base de données ou de la boîte mail.
Maintenant, essayons d'envoyer un grand nombre de requêtes à la servlet en utilisant la class TestForm.java qui va interagir avec la méthode doPost, et observons le fichier résultats SavNewsLetter.txt. Pour cela il suffit d'ajouter une boucle au code testerEnvoi.jsp pour envoyer plusieurs requêtes sur le serveur et de paramétrer la variable nbreRequetes comme suit :
| testerEnvoi.jsp |
<%
double nbreRequetes = 100;
for(int i=0;i<=nbreRequettes; i++)
{
TestForm tester = new TestForm();
Hashtable<String, String> hashtable = new Hashtable<String, String>();
hashtable.put( "nom", new String("Koci") );
hashtable.put( "prenom", new String("Clémentine") );
hashtable.put( "email", new String("Tinna@domaine.com") );
hashtable.put( "activite", new String("Danse") );
try
{
tester.envoyerEnPost("http://localhost:8080/formulaire/Controlleur", hashtable);
}
catch(Exception ex)
{
System.out.println("Exception : " + ex );
}
}
%>
|
Un nombre de requêtes élevé pourra remplir un espace sur disque et ainsi faire tomber le serveur.
Solution : Il existe plusieurs solutions pour éloigner ce genre d'attaque. Pour éviter ce cas de figure, il faut gérer la session d'une manière efficace. Par exemple, il suffit d'autoriser uniquement quelques envois de formulaire pour chaque session donnée, ou ne pas procéder à la sérialisation des informations quand plusieurs requêtes suspectes sont interceptées. On entend par requêtes suspectes les requêtes envoyées en nombre important sur une durée donnée.
III-D. Limiter les tentatives d'authentification
Beaucoup de sites ont une rubrique "accès privé", pour permettre à une partie des utilisateurs d'accéder à des services particuliers.
Généralement l'accès s'effectue après une authentification via un formulaire.
Prenons un exemple : Pour accéder au forum de
developpez.com, vous devez avoir un compte enregistré.
L'image suivante affiche le formulaire de connexion au forum.
D'ailleurs je vous le conseille, si vous n'avez pas encore de compte, il faut s'inscrire car le forum est une mine d'or.
Revenons à notre sujet. Comment un hacker pourra faire pour accéder à un espace privé même s'il n'a pas de compte ?
Il existe une manière parmi tant d'autres qui permet d'outrepasser les règles habituelles. L'attaque par force brute est l'une de ces méthodes très répandues. Le principe consiste à envoyer toutes les probabilités possibles de mot de passe pour un login donné, si le site autorise la multi-authentification.
Quoi que cela parait moins évident pour la plupart des sites, il existe des cas où la tentative pourra aboutir.
Ne tentez pas le coup sur
developpez.com. Le principe est tout autre. Le site est aussi sécurisé que les coffres du FMI(Fond Monétaire International).
Continuons avec le site qui décrit la salle de sport. La direction décide d'attribuer à chaque adhérent un espace privé, notamment afin de choisir les horaires, ou modifier les informations personnelles comme l'adresse mail ou les coordonnées personnelles.
Voici le formulaire HTML décrit par le fichier
authentif.jsp
| authentif.jsp |
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title> Veuillez vous authentifier</title>
</head>
<body>
<form action ="http://localhost:8080/formulaire/Controlleur" method = "Post">
<P>Login_____ : <INPUT name="login" size=15></P>
<P>Password__ : <INPUT name="password" size=15 type="password"></P>
<P><INPUT type="submit" value="s'identifier" name="button1">
<INPUT type="button" value="Reset" name="button2"></P>
</form>
</body>
</html>
|
Et voici la servlet Authentification chargée de recevoir les informations d'authentification. Le code est simplifié pour des raisons didactiques.
| Authentification.java |
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class Authentification extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
doPost(request, response);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
String cLogin = request.getParameter("login");
String cPassword = request.getParameter("password");
if (authentifier(cLogin, cPassword))
{
System.out.println("Vous êtes connectés");
}
else
System.out.println("Login ou mot de passe incorrect");
}
private boolean authentifier (String Login, String MotDePass){
return Login.equalsIgnoreCase("Koci") && MotDePass.equals("meinPass");
}
}
|
Comment un hacker peut deviner votre mot de passe ? S'il a déjà en possession votre login, sa charge de travail sera divisée en deux. Généralement c'est ce qui se produit, tous vos amis ont votre mail mais pas votre mot de passe.
Nous allons passer à notre servlet une combinaison de mots de passe. Nous supposons que nous connaissons déjà le login.
Les classes suivantes permettent de créer des combinaisons possibles de mot de passe. On entend par mot de passe une chaîne de caractères d'une taille fixe composée d'une combinaison de caractères de l'alphabet avec casse et des chiffres.
La classe suivante Probabilite permet de générer des combinaisons de chaînes de caractères possibles à partir d'une chaîne constituée par les caractères de l'alphabet.
| Probabilite.java |
import java.io.IOException;
import java.util.ArrayList;
import java.io.PrintWriter;
import java.io.FileWriter;
public class Probabilite {
public static void main(String[] args){
int tailleAlphabet = Integer.parseInt(args[0]);
int taillePass = Integer.parseInt(args[1]);
String[][] Alphabet=new String[tailleAlphabet][tailleAlphabet];
String[] AlphabetTab = new String[tailleAlphabet];
String[] AlphabetIn = {"a", "b", "c","d","e","f","g","h","i","j","k","l","m","n","o","p",
"q","r","s","t","u","v","w","x","y","z"};
String[] AlphabetIn2 = {"a", "b", "c","d","e","f","g","h","i","j","k","l","m","n","o","p",
"q","r","s","t","u","v","w","x","y","z"};
PrintWriter out;
for(int e = 0; e<tailleAlphabet; e++){
AlphabetTab[e] = AlphabetIn[e];
}
try
{
out = new PrintWriter(new FileWriter("rapport.txt"));
for(int e1=0;e1<tailleAlphabet;e1++){
for(int e2=0;e2<tailleAlphabet;e2++){
Alphabet[e1][e2] = AlphabetIn[e1] + AlphabetIn2[e2];
ArrayList<String> carArraylist = new ArrayList<String>();
carArraylist.add(Alphabet[e1][e2]);
GenerProbab propabsM = new GenerProbab (taillePass);
propabsM.traiter(AlphabetTab,carArraylist, out);
}
}
out.println("Fin de traitements ");
out.close();
}catch(IOException ex)
{
System.out.println("Erreur "+ ex);
}
}
}
|
| GenerProbab.java |
import java.io.IOException;
import java.io.PrintWriter;
import java.util.*;
public class GenerProbab {
private double taillePass;
public GenerProbab(int taillePass) {
this.taillePass = taillePass;
}
public void traiter(String[] alphabet, ArrayList<String> aTableau2,
PrintWriter out) throws IOException {
ArrayList<String> TAbFinal = new ArrayList<String>();
for (int e = 0; e < alphabet.length; e++) {
for (int j = 0; j < aTableau2.size(); j++) {
TAbFinal.add(aTableau2.get(j) + alphabet[e]);
System.out.println(TAbFinal.get(TAbFinal.size() - 1));
out.println(TAbFinal.get(TAbFinal.size() - 1));
}
}
System.gc();
String laChaine = (String) TAbFinal.get(TAbFinal.size() - 1);
int laTaille = laChaine.length();
if (laTaille < taillePass) {
traiter(alphabet, TAbFinal, out);
}
}
}
|
Pour des besoins de test, les deux classes ont étés simplifiées. Il suffit d'adapter le code pour que les deux classes soient appelées pour utiliser le login d'une personne afin de générer les différentes possibilités du mot de passe.
Pour exécuter le programme précédent, il faut passer à la classe Probabilite qui contient la méthode main, deux paramètres qui sont respectivement la taille de l'alphabet et la taille maximale du mot de passe.
Dans la réalité, la taille de l'alphabet pour générer un mot de passe est fixe et contient tous les caractères minuscules et majuscules, les chiffres de 0 à 9, ainsi que quelques caractères spéciaux.
Exemple :
| Probabilité pour un alphabet de 26 car et password de 10 car maximum |
|
Cette commande permet de générer tous les cas possibles du mot de passe d'une taille maximum de 10 caractères à partir de l'alphabet contenant les 26 caractères [a-z].
Les classes précédentes se basent sur l'objet ArrayList, donc sur une structure de données stockée en mémoire. Le traitement génère un nombre de probabilité en fonction des capacités mémoire de votre ordinateur.
Cependant vous pouvez définir la taille louée par la JVM ou l'augmenter de la façon suivante :
| Avec gestion optimisée de la JVM |
java Probabilite 26 10 -Xms128M -Xmx512M -XX:MaxPermSize =256m
|
Une autre solution consiste à utiliser une structure de données qui permet la sérialisation de tous les cas possibles, comme les fichiers textes. Il existe aussi plusieurs algorithmes qui permettent de réaliser ce genre de traitements.
Solution : Comment éviter ce genre d'attaque ?
Afin d'éviter une multitude de requêtes, il est primordial de limiter le nombre de tentatives de connexion à un nombre donné dans un délai fixe. Exemple, n'autoriser que 3 connexions consécutives au bout de 3 minutes données. Cela implique qu'il faut prendre en compte la gestion de la session des utilisateurs.
Cette faille est très répandue sur le net, y compris auprès des professionnels. Le Webmail de Yahoo était un exemple. Vous pouvez lire la news publié par
developpez.com à cette adresse :
Exploitation en masse d'une faille vieille de deux ans sur le webmail Yahoo
L'exemple suivant est généré par google Mail après 3 tentatives de mot de passe échouées pour un login donnée.
Effectivement, après 3 tentatives d'échec de connexion, googlemail génère un Captcha pour s'assurer que ce n'est pas un programme automatique qui essaye de se connecter, mais il y a bel et bien une intervention humaine.
La méthode décrite par la classe Probabilite a ses limites, surtout si le développeur et l'utilisateur respectent les règles de codages et de création des mots de passe.
Si le mot de passe est de 5 caractères, en utilisant un alphabet comme décrit dans la classe, c'est-à-dire 26 caractères minuscules on obtient plus de 18200 cas allant du mot "aaa" à "zzzzz".
Il est clair que les conditions seront plus compliquées si les normes de codage des mots de passe sont respectées. Imaginez le temps d'exécution et le nombre de combinaisons pour un alphabet de tous les caractères possibles avec un mot de passe d'une longueur dépassant 15 caractères.
Afin de compliquer les choses au hacker, il est utile de coder les mots de passe en utilisant des chaînes longues avec des minuscules, majuscules et chiffres, et de changer le mot de passe régulièrement.
IV. Les outils
Afin de renforcer la sécurité du site, il est utile de procéder aux tests d'une manière rigoureuse, en simulant les différentes attaques possibles.
A cet effet, sous Linux, il existe plusieurs outils qui permettent de tracer les pages web et de simuler des attaques. Certains de ces outils ont des versions sous d'autres systèmes d'exploitation.
Dans ce tutoriel, nous allons nous attarder sur deux plugins de Firefox ainsi que les fameux programmes WebGoat et wireshark.
IV-A. Firebug
Firebug est un plugin de Firefox qui permet de tracer sa page web, allant d'une simple consultation du code, à l'affichage des erreurs en passant par le débogage du code JavaScript.
Son installation est simple. Il suffit d'ajouter l'extension Firebug du navigateur, en la téléchargeant
ici.
L'image suivante montre Firebug analysant le code source d'une page web.
IV-B. Web Developper
Web Developper est une vraie boîte à outils qui permet de manipuler les pages web en profondeur. Son installation est simple, comme pour Firebug, il suffit d'ajouter l'extension Web Developper dans le navigateur (téléchargeable
ici).
Entre autres, cet outil permet de gérer JavaScript, les cookies et CSS, et les modifier. On peut même réaliser des conversions des actions
Post en
Get et vice versa. Il permet aussi de debugger le code, d'afficher les commentaires et manipuler les formulaires.
Cet outil est vraiment très puissant, mais aussi très utile lors des développements web.
L'image suivante montre les différentes possibilités de manipulation d'une page web avec Web Developper.
IV-C. WebGoat
WebGoat est une application puissante, écrite en Java. Cet outil permet de simuler des attaques contre des applications web, au point qu'il rend les choses amusantes.
WebGoat se présente sous forme de différentes leçons pour exploiter des vulnérabilités comme Injection SQL ou de commande, détournement de session, manipulation des Web Services, détournement des requêtes AJAX, décryptage des mots de passe envoyés via le protocole http, ainsi que plein d'autres astuces.
Chaque leçon contient un ou plusieurs exemples pratiques. Pour voir la solution, il suffit de cliquer sur le menu Show Solution.
Les copies d'écrans suivantes montrent un aperçu général de la page d'accueil de WebGoat, l'autre montre la liste générale des attaques vulnérables proposées.
Installation:
Pour installer WebGoat, il suffit de télécharger le zip
ici, le décompresser dans un répertoire. Toutefois, assurez-vous que vous avez les droits de lecture et écriture sur le répertoire.
Pour information WebGoat installe un serveur Tomcat.
Pour exécuter WebGoat, allez dans le répertoire d'installation et lancer la commande suivante dans un terminal :
| Lancement de webgoat |
$ sh ./webgoat.sh start8080
|
Lancer votre navigateur et saisissez le lien suivant :
$ http://localhost:8080/webgoat/attack
|
Une fenêtre vous invite à rentrer votre login et mot de passe. Saisissez le couple guest/guest et la page d'accueil s'affiche.
Vous pouvez commencer vos cours grâce à un menu répertoriant tout type de vulnérabilité comme le montre la figure précédente.
Le principe de Webgoat est d'interagir avec le serveur Tomcat installé fourni automatiquement avec l'application.
Afin de mieux utiliser WebGoat, il est recommandé d'installer wireshark.
IV-D. Wireshark
wireshark est un outil qui permet d'analyser le trafic réseau. S'il est implémenté dans la plupart des distributions, c'est parce que c'est un outil conçu au départ pour Linux/Unix dont l'installation est très simple. Il existe aussi des versions pour Mac OS X et Windows.
Avec une distribution basée sur Debian, il suffit de l'installer à partir des paquets synaptiques ou se contenter de la commande :
| Installation de wireshark |
|
Lors de la première utilisation, il faut commencer par initialiser la variable d'environnement contenant le chemin vers les librairies avec la commande :
| Lors de la première utilisation |
|
Pour démarrer wireshark il suffit de lancer la commande suivante dans le terminal :
Toutefois, il faut éviter de lancer wireshark sous le compte root.
Passons aux choses plus amusantes ! Nous allons prendre un exemple qui permet d'exploiter une faille réseau, en utilisant WebGoat et wireshark.
IV-D-1. Exemple d'utilisation : Décryptage d'un mot de passe
Nous allons démontrer combien c'est facile de déchiffrer un mot de passe, passé via un réseau surveillé.
Nous allons reproduire la leçon Insecure Login qui consiste à voir en clair un mot de passe envoyé via le protocole http.
Une fois que vous avez démarré WebGoat et wireshark, ouvrez la page
| Lancement de Webgoat |
http://localhost:8080/webgoat/attack
|
Allez dans la rubrique Insecure Communication puis Insecure Login. Un formulaire vous propose de saisir votre nom et votre mot de passe comme le montre la figure suivante :
Cliquez sur le bouton submit, regardez attentivement la trace du flux réseau sur votre terminal ou dans la fenêtre de wireshark. Vous allez voir afficher le login, mais aussi le mot de passe en clair.
Les figures suivantes montrent ces informations sur l'interface wireshark et dans le terminal.
Nous remarquons facilement le login
jack et le mot de passe
sniffy sur les deux images.
Le protocole HTTP passe les informations en clair et si le mot de passe n'est pas crypté, il sera visible pour un hacker qui surveille votre réseau.
Pour comprendre comment éviter ce genre de faille, voici un exemple réalisé avec le site
developpez.com, au moment où je me connecte sur le forum avec mon login
kmdkaci et mon mot de passe, tout en lançant la capture du flux réseau grâce à
wireshark.
Les captures d'écrans suivantes montrent mon login, mais on remarque que le mot de passe est déduit à partir d'une fonction de
hachage.
Solution :
Afin d'éviter que les mots de passe soient envoyés en clair, il est primordial de procéder au hachage ou cryptage des chaînes. Il existe plusieurs méthodes qui permettent d'effectuer ces opérations.
- Avec JavaScript, vous pouvez créer une fonction de hachage qui transformera le mot de passe en une suite de caractères incompréhensibles comme le montre la figure précédente, et cela en se basant sur des algorithmes comme MD5.
Pour rappel, le
MD5 (Message Digest 5) est une méthode utilisée en cryptographie qui permet d'obtenir une empreinte d'un message quelconque.
L'un des soucis que l'on pourra rencontrer est que JavaScript est du coté client, votre algorithme est visible par l'internaute, même si la méthode MD5 reste relativement sécurisée, on préfère plutôt la méthode basée sur
SHA-1.
- Il existe aussi une possibilité d'utiliser une applet java comme formulaire d'entrée, dont le code comporte un algorithme de cryptage.
L'applet pourra interagir directement avec la base de données, en envoyant les informations cryptées, ou remettre à JavaScript ces informations cryptées pour les envoyer sur le serveur.
Pour rappel, l'applet Java peut communiquer avec JavaScript grâce aux plugins Netscape, comme JSObject. Voir
le forum
- Il reste la solution d'utilisation du protocole HTTPS qui demeure la méthode la plus sécurisée.
V. Conclusion
Nous avons vu, tout au long de ce tutoriel quelques failles de sécurité qui peuvent être laissées dans les codes, y compris chez les professionnels. Ces failles peuvent s'avérer vulnérables, et peuvent constituer une menace dangereuse pour le système d'informations.
Comme vous l'avez sûrement remarqué, ce tutoriel ne traite que quelques failles de sécurité. Il est impossible de citer tous les types d'attaques susceptibles de se propager sur le net. Mais ces quelques lignes donnent quelques aperçus de failles existantes et surtout vont permettre d'inciter les développeurs à devenir vigilants.
VI. Liens
VII. Remerciements
Un grand merci à
Bérénice MAUREL pour la correction orthographique et sa patience, ainsi que
Baptiste Wicht pour la relecture technique et ses conseils.

