Сбор учётных данных
Перед тем как ознакомиться с данным материалом настоятельно рекомендую ознакомиться с предыдущей статьей.
Один из столпов социального инжиниринга — это атака по сбору учетных данных. В ходе нее перехват учетной информации пользователя происходит за счет подмены оригинального сайта клонированной версией, где пользователь и вводит свои данные. Эта техника эффективна против организаций, которые предоставляют в интернете доступ к интерфейсу однофакторной аутентификации. Как только вы получили учетные данные пользователя, можете применять их для получения доступа к аккаунту на оригинальном сайте. Это зачастую приводит к прорыву сетевого периметра организации.
Go обеспечивает отличную платформу для выполнения подобных атак, потому что он быстро устанавливает новые серверы, позволяя также легко настраивать маршрутизацию и парсинг вводимой пользователем информации. В сборщик учетных данных можно добавлять множество настроек и возможностей, но в нашем примере будем придерживаться основ.
Для начала нужно сделать клон сайта, имеющего форму авторизации. Здесь можно рассмотреть множество вариантов. На практике вы будете делать копию сайта, используемого вашей мишенью. В своем примере мы будем клонировать ресурс Roundcube. Roundcube — это открытый клиент электронной почты, который применяется не так часто, как коммерческие решения наподобие Microsoft Exchange, но вполне годится для демонстрации принципа. Для запуска Roundcube мы задействуем Docker, так как он существенно упрощает процесс.
Вы можете запустить собственный сервер Roundcube, выполнив приведенный далее код. Делать это не обязательно, так как исходный код примера содержит клон данного сайта. Тем не менее для полноты информации мы включаем и этот вариант:
Эта команда запускает экземпляр Roundcube Docker. Перейдя по адресу http://127.0.0.1:80, вы увидите форму авторизации. Обычно для клонирования сайта и всех необходимых ему файлов используется wget, но задействованный в реализации Roundcube JavaScript лишает нас этой возможности. Вместо этого применим для сохранения Google Chrome. Структура каталога примера приведена в коде ниже.
Файлы в каталоге public представляют неизмененный сайт. Вам потребуется изменить исходную форму авторизации, чтобы перенаправлять вводимые данные, отправляя их своему серверу вместо действительного. Для начала откройте public/index.html и найдите элемент формы, используемый для POST-запроса авторизации. Он должен выглядеть так:
В этом теге нужно отредактировать атрибут action, направив его на свой сервер. Для этого измените action на /login и сохраните. Теперь эта строка должна выглядеть так:
Для корректного отображения формы авторизации и перехвата имени пользователя с паролем сначала потребуется разместить эти файлы в каталог public. Затем нужно будет написать для /login функцию HandleFunc, которая и будет выполнять перехват. Вам также потребуется сохранить полученные учетные данные в файле с помощью логирования.
Все это можно обработать буквально в нескольких строках кода, и в коде ниже вы увидите итоговую программу целиком.
Первое, на что следует обратить внимание, — это импорт github.com/Sirupsen/logrus. Это структурированный пакет для логирования, который мы предпочитаем задействовать вместо стандартного пакета Go log. Он предоставляет более богатые возможности настройки логирования для лучшей обработки ошибок. Чтобы использовать этот пакет, нужно, как обычно, вначале выполнить go get.
Затем мы определяем функцию-обработчик login(). Надеемся, что данный паттерн вам знаком. Внутри этой функции запись перехваченных данных реализуется с помощью log.WithFields(). При этом отображаются текущее время, пользовательский агент и IP-адрес источника запроса. Помимо этого, выполняется вызов FormValue(string) для перехвата переданных значений имени пользователя (_user) и пароля (_pass). Эти значения мы получаем из index.html, также определив расположение элементов ввода формы для каждого имени пользователя и пароля. Ваш сервер должен явно соответствовать именам полей в том виде, в каком они присутствуют в форме авторизации.
Приведенный далее фрагмент, извлеченный из index.html, показывает соответствующие вводные элементы, чьи имена для наглядности выделены жирным:
В функции main() мы начинаем с открытия файла, в котором будут храниться перехваченные данные. Затем используем log.SetOutput(io.Writer), передавая ей только что созданный дескриптор файла для настройки пакета логирования, чтобы он производил запись в этот файл. Далее создаем новый маршрутизатор и добавляем функцию-обработчик login().
Перед запуском сервера нужно выполнить еще одно действие: сообщить маршрутизатору о необходимости предоставлять статические файлы из каталога. Таким образом, ваш сервер Go явно знает, где находятся все статические файлы — изображения, JavaScript, HTML. Go упрощает этот процесс и обеспечивает защиту против атак по обходу каталогов. Начиная изнутри, мы используем http.Dir(string) для определения каталога, из которого нужно предоставлять файлы. Результат передается в качестве ввода в http.FileServer(FileSystem), которая создает для данного каталога http.Handler. Все это прикрепляется к маршрутизатору с помощью PathPrefix(string). Использование / в качестве префикса пути будет соответствовать всем запросам, которые еще не нашли соответствия. Обратите внимание на то, что по умолчанию возвращаемый из FileServer обработчик поддерживает индексацию каталогов, что может спровоцировать утечку информации. Это можно отключить, но здесь мы данный вопрос рассматривать не будем.
В завершение, как и прежде, мы запускаем сервер. Собрав и выполнив код Сервера сбора учетных данных, откройте браузер и перейдите на http://localhost:8080. Попробуйте отправить через форму имя пользователя и пароль. Затем выйдите из программы и откройте credentials.txt:
Только взгляните на эти логи! Здесь видно, что были отправлены имя Oleg и пароль p@ssw0rd1!. Наш вредоносный сервер успешно обработал POST-запрос формы, перехватив введенные учетные данные и сохранив их в файл для просмотра офлайн. Будучи атакующим, вы могли бы затем использовать эти данные против целевой организации и продолжить внедрение в ее систему.
Далее мы проработаем вариацию этой техники по сбору учетных данных. Вместо ожидания отправки формы создадим кейлогер для перехвата нажатий клавиш в реальном времени.
Кейлогинг с помощью WebSocket API
WebSocket API (WebSockets) — это полнодуплексный протокол, чья популярность на протяжении последних лет возросла, поскольку теперь он поддерживается во многих браузерах. Этот протокол предоставляет веб-серверам и их клиентам способ эффективно взаимодействовать друг с другом. Что еще более важно, он позволяет серверу отправлять сообщения клиенту, не требуя опроса.
WebSockets применяются для создания приложений реального времени, таких как чаты и онлайн-игры. Но их можно задействовать и для вредоносных действий, например для внедрения кейлогера в приложение с целью перехвата всех нажимаемых пользователем клавиш. Для начала представьте, что нашли приложение, уязвимое для межсайтового выполнения сценариев (брешь, через которую сторонний агент может выполнять произвольный JS-код в браузере жертвы), или взломали сервер, получив возможность изменять исходный код этого приложения. При любом из этих вариантов вы сможете внедрить удаленный JS-файл. Мы с вами создадим инфраструктуру сервера для обработки WebSocket-соединения со стороны клиента и регистрации входящих нажатий клавиш.
В целях демонстрации для тестирования полезной нагрузки мы используем JS Bin (http://jsbin.com). JS Bin — это онлайн-песочница, где разработчики могут тестировать свой HTML- или JS-код. Перейдите на этот ресурс в браузере и вставьте следующий HTML в столбец слева, полностью заменив исходный код:
В правой части экрана отобразится форма. Вы могли заметить, что включили тег script с атрибутом src, установленным как http://localhost:8080/k.js. Это будет JS-код, реализующий создание WebSocket-соединения и отправку пользовательского ввода на сервер.
Нашему серверу потребуется выполнить два действия: обработать WebSocket и предоставить JS-файл. Давайте в первую очередь покончим с JavaScript, ведь книга, в конце концов, посвящена Go. (Инструкции по написанию JS-кода с помощью Go имеются в репозитории https://github.com/gopherjs/gopherjs/.)
Вот JS-код:
Он обрабатывает события нажатия клавиш. Каждое такое нажатие этот код отправляет через WebSocket на ресурс по адресу ws://{{.}}/ws. Напомним, что значение {{.}} является полем ввода шаблона Go, отражающего текущий контекст. Этот ресурс представляет WebSocket URL, который будет вносить информацию о местоположении сервера на основе переданной в шаблон строки. Мы вернемся к этому через минуту. Для этого примера сохраним JS в файл logger.js.
Вас может смутить то, что мы вроде собирались предоставлять его как k.js. HTML-код, который мы показали ранее, тоже явно использует k.js. Что это значит? Это значит, что на деле logger.js является не JS-файлом, а шаблоном Go. Мы будем применять k.js в маршрутизаторе в качестве паттерна для сопоставления. При его совпадении сервер будет отображать шаблон из файла logger.js, заполненный контекстными данными, представляющими хост, к которому подключается WebSocket. Код сервера, реализующий этот процесс, показан в коде ниже.
Рассмотрим приведенный код подробнее. Прежде всего, обратите внимание на то, что мы используем еще одну стороннюю библиотеку, gorilla/websocket, с помощью которой обрабатываем коммуникации WebSocket. Это полноценный мощный пакет, упрощающий процесс разработки, наподобие уже знакомого вам gorilla/mux. Не забудьте сначала выполнить из терминала go get github.com/gorilla/websocket.
Затем переходим к определению нескольких переменных. Мы создаем экземпляр websocket.Upgrader, который будет добавлять в белый список каждый источник. Допуск всех источников обычно считается плохой практикой в плане безопасности, но здесь мы не придаем этому значения, поскольку работаем с тестовым экземпляром, который будем запускать на локальных рабочих станциях. Для использования в реальных вредоносных действиях источник нужно будет ограничить конкретным значением.
В функции init(), выполняющейся автоматически перед main(), мы определяем аргументы командной строки и пытаемся спарсить шаблон Go, расположенный в файле logger.js. Обратите внимание на то, что мы вызываем template.ParseFiles("logger.js"). Проверяем ответ, чтобы убедиться в успешном парсинге файла. Если все правильно, то спарсенный шаблон будет сохранен в переменной jsTemplate.
На данный момент мы еще не предоставляли контекстуальных данных шаблону и не выполняли его. Это произойдет чуть позже. Сначала идет определение функции serveWS(), которая будет использоваться для обработки WebSocket-коммуникаций. С помощью вызова upgrader.Upgrade(http.ResponseWriter, *http.Request, http.Header) мы создаем экземпляр websocket.Conn. Метод Upgrade() расширяет HTTP-соединение для использования протокола WebSocket. Это означает, что любой обрабатываемый данной функцией запрос будет расширен для использования WebSocket. Мы взаимодействуем с этим соединением внутри бесконечного цикла for, вызывая для чтения входящих сообщений conn.ReadMessage(). Если JS-код будет работать должным образом, то сообщения должны состоять из перехваченных символов нажатых клавиш. Эти сообщения и удаленный IP-адрес клиента записываются в stdout.
Мы разобрали самую сложную часть пазла создания обработчика WebSocket. Далее идет создание еще одной функции-обработчика serveFile(). Она будет извлекать и возвращать содержимое JS-шаблона, заполненного включенными контекстными данными. Для этого мы установим заголовок Content-Type как application/javascript. Это сообщит подключающимся браузерам, что содержимое тела HTTP-ответа должно рассматриваться как JavaScript. Во второй и последней строке обработчика выполняется вызов jsTemplate.Execute(w, wsAddr). Помните, как мы парсили logger.js в функции init() во время бутстрэппинга сервера? Результат был сохранен в переменной jsTemplate. Данная строка кода обрабатывает тот самый шаблон. Мы передаем ей io.Writer (в нашем случае используется w, http.ResponseWriter) и контекстные данные типа interface{}. Тип interface{} означает, что можно передать любой тип переменной, будь то строка, структура или что-то другое. В данном случае мы передаем строковую переменную wsAddr. Если вернуться назад к функции init(), то можно заметить, что эта переменная содержит адрес WebSocket-сервера и устанавливается через аргумент командной строки. Говоря кратко, она заполняет шаблон данными и записывает его как HTTP-ответ. Довольно хитро!
Мы реализовали функции-обработчики serveFile() и serveWS(). Теперь нужно только настроить маршрутизатор для сопоставления шаблонов, чтобы передавать выполнение правильному обработчику. Как и ранее, это делается в функции main(). Первый обработчик сопоставляется с URL-шаблоном /ws, выполняя функцию
serveWS для апгрейда и обработки WebSocket-соединений. Второй маршрут сопоставляется с шаблоном /k.js, выполняя функцию serveFile(). Таким образом сервер передает отрисованный JS-шаблон клиенту.
Теперь давайте этот сервер запустим. Если открыть HTML-файл, то мы увидим сообщение connection established. Оно регистрируется, так как JS-файл был успешно отрисован в браузере и запросил WebSocket-соединение. Если ввести учетные данные в элементы формы, то они будут выведены в stdout на сервере:
У нас все получилось! На выводе мы видим список всех нажатых при заполнении формы клавиш. В данном случае это набор пользовательских учетных данных. Если у вас возникли сложности, убедитесь, что передаете в качестве аргументов командной строки точные адреса. Кроме того, сам HTML-файл может нуждаться в доработке, если вы вызываете k.js с сервера, чей адрес отличен от localhost:8080.
Приведенный код можно улучшить несколькими способами. В одном из них можно логировать вывод не в терминал, а в файл или другое постоянное хранилище. Это снизит вероятность потери данных по причине закрытия окна терминала или перезапуска сервера. К тому же если ваш кейлогер регистрирует нажатия клавиш одновременно на нескольких клиентах, данные в выводе могут смешиваться, усложняя сбор и анализ учетных данных разных пользователей. Этого можно избежать, определив более эффективный формат представления, который, например, группирует нажатые клавиши по уникальному клиенту/порту.
На этом знакомство с техниками сбора учетных данных закончено. Последним мы рассмотрим мультиплексирование HTTP‑соединений C2.
Мультиплексирование C2-соединений:
В последнем разделе главы мы покажем, как мультиплексировать HTTP-соединения Meterpreter к различным бэкенд-серверам управления. Meterpreter — это популярный гибкий инструмент исполнения команд (C2), являющийся частью фреймворка Metasploit. Здесь мы не будем излишне углубляться в подробности Metasploit или Meterpreter. Если вы с ними прежде не сталкивались, рекомендуем ознакомиться с одним из множества руководств или сайтов документации.
В этом же разделе поговорим о создании обратного HTTP-прокси в Go, который позволит динамически перенаправлять входящие сессии Meterpreter на основе HTTP-заголовка Host. Именно так и работает виртуальный хостинг сайтов. Тем не менее вместо предоставления различных локальных файлов и каталогов мы будем проксировать соединение на разных слушателей Meterpreter. Это будет интересным случаем применения по нескольким причинам.
Во-первых, прокси-сервер выступает в роли переадресатора, позволяя раскрывать только имя домена и IP-адрес, но не слушателей Meterpreter. Если переадресатор вдруг попадет в черный список, можно будет легко переместить его, не перемещая C2-сервер. Во-вторых, вы можете расширить приведенные здесь концепции для выполнения доменного фронтирования, техники задействования доверенных сторонних доменов (зачастую от облачных провайдеров) для обхода ограничивающего контроля исходящего трафика. Мы не будем разбирать здесь пример детально, но вам рекомендуем изучить эту тему подробнее, поскольку это очень мощная техника. И наконец, приведенный пример показывает, как можно совместно использовать одну комбинацию «хост/порт» в команде союзников, потенциально атакуя разные целевые организации. Поскольку порты 80 и 443 — это наиболее вероятные допустимые точки выхода, можно применять прокси-сервер для их прослушивания и перенаправления соединений на правильного слушателя.
Вот наш план. Мы настроим два отдельных обратных HTTP-слушателя Meterpreter. В этом примере они будут размещаться на виртуальной машине с IP-адресом 10.0.1.20, также вполне допускается их размещение на разных хостах. Мы привяжем этих слушателей к портам 10080 и 20080 соответственно. В реальном сценарии они могут выполняться где угодно, при условии что прокси-сервер сможет связаться с их портами. Убедитесь, что у вас установлен Metasploit (в Kali Linux он установлен по умолчанию), затем запустите слушателей:
При запуске слушателя мы передаем прокси-данные как значения LHOST и LPORT. Тем не менее устанавливаем продвинутые опции ReverseListener, BindAddress и ReverseListenerBindPort на действительный IP-адрес и порт, где должен запускаться слушатель. Это дает некоторую гибкость при использовании портов, в то же время позволяя явно идентифицировать прокси-сервер, которым в случае, например, настройки фронтирования домена может быть имя хоста.
Во втором экземпляре Metasploit мы делаем то же самое для запуска дополнительного слушателя на порте 20080. Единственное отличие здесь в привязке к другому порту:
Теперь создадим обратный прокси, исчерпывающий код которого приведен в коде ниже.
В первую очередь выполняется импорт пакета net/http/httputil, который содержит вспомогательную функциональность для создания обратного прокси. Это избавит вас от необходимости делать все с нуля.
После импорта пакетов определяются две переменные, которые являются картами. Первая, hostProxy, будет служить для сопоставления имен хостов с URL-адресом слушателя Metasploit, на который их нужно направлять. Вспомните, что переадресацию мы делаем на основе заголовка Host, который ваш прокси-сервер получает в HTTP-запросе. Поддержание этого сопоставления — простой способ определения мест назначения.
Вторая переменная, proxies, также будет использовать в качестве значения ключей имена хостов. Тем не менее соответствующие им значения в карте являются экземплярами *httputil.ReverseProxy. То есть эти значения будут не строковыми представлениями места назначения, а фактическими экземплярами прокси-сервера, на которые можно делать перенаправление.
Обратите внимание: данную информацию мы кодируем жестко, и это не самый удачный способ управления конфигурацией и проксирования данных. В более оптимальной реализации информация сохранялась бы во внешнем файле конфигурации, но это упражнение мы оставим вам для самостоятельной проработки.
С помощью функции init() мы определяем сопоставления между именами доменов и целевыми экземплярами Metasploit. В этом случае будем перенаправлять все запросы со значением заголовка Host, равным attacker1.com, на http://10.0.1.20:10080, а со значением attacker2.com — на http://10.0.1.20:20080. Конечно же, пока мы не делаем реальное перенаправление, а просто создаем зачаточную конфигурацию. Обратите внимание на то, что адреса назначения соответствуют значениям ReverseListenerBindAddress и ReverseListenerBindPort, которые мы использовали для слушателей Meterpreter ранее.
Далее все в той же функции init() мы перебираем карту hostProxy, делая парсинг целевых адресов для создания экземпляров net.URL. Полученный результат задействуется в качестве ввода в вызове функции httputil.NewSingleHostReverseProxy (net.URL), которая является вспомогательной функцией, создающей обратный прокси из URL. Более того, тип httputil.ReverseProxy удовлетворяет требованиям интерфейса http.Handler, то есть создаваемые экземпляры прокси можно использовать как обработчики для маршрутизатора. Делается это с помощью функции main(). Сначала создается маршрутизатор, после чего осуществляется перебор всех экземпляров прокси. Напомним, что ключ — это имя хоста, а значение имеет тип httputil.ReverseProxy. Для каждой пары «ключ/значение» карты мы добавляем в маршрутизатор функцию сопоставления. Тип Route из набора Gorilla MUX содержит такую функцию под названием Host, которая получает имя хоста для сопоставления со значениями заголовка Host входящих запросов. Для каждого имени хоста, которое нужно проверить, мы указываем маршрутизатору использовать соответствующий прокси. Это на удивление простое решение того, что в противном случае оказалось бы сложной задачей.
В завершение происходит запуск сервера и его привязка к порту 80. Сохранитесь и запустите программу. Это нужно будет сделать от имени привилегированного пользователя, поскольку привязка выполняется к привилегированному порту.
На данный момент у нас запущены два обратных HTTP-слушателя Meterpreter, а также должен работать обратный прокси-сервер. Последний шаг — генерирование тестовой полезной нагрузки для проверки его итоговой работоспособности. Для этого мы задействуем msfvenom — инструмент генерирования полезной нагрузки, который также поставляется вместе с Metasploit. С его помощью создадим два исполняемых файла Windows:
Эти команды создадут два файла с названиями payload1.exe и payload2.exe. Обратите внимание на то, что единственное различие между ними помимо самого имени заключается в значениях HttpHostHeader. Это гарантирует, что итоговая полезная нагрузка отправляет свои HTTP-запросы с конкретным значением заголовка Host. Также стоит заметить, что значения LHOST и LPORT соответствуют информации нашего обратного прокси-сервера, а не слушателей Meterpreter. Отправьте эти исполняемые файлы в систему Windows или на виртуальную машину. При их выполнении должны устанавливаться две сессии: одна в слушателе, привязанном к порту 10080, вторая в слушателе, привязанном к порту 20080. Выглядеть они должны так:
Если с помощью tcpdump или Wireshark вы проверите трафик, предназначенный для порта 10080 или 20080, то должны увидеть, что обратный прокси-сервер является единственным хостом, коммуницирующим со слушателем Metasploit. Вы также можете убедиться, что заголовок Host соответствующим образом устанавливается на attacker1.com для слушателя на порте 10080 и на attacker2.com для слушателя на порте 20080.
Вот и все. Вы справились! Теперь пора поднять планку. Я советую вам в качестве дополнительного упражнения доработать код для использования поэтапной полезной нагрузки. Это будет сопряжено с дополнительными трудностями, так как потребуется добиться того, чтобы обе стадии правильно перенаправлялись через прокси. Затем попробуйте реализовать это с помощью HTTPS вместо небезопасного HTTP. Так вы сможете глубже разобраться в проксировании трафика для вредоносных целей и повысить его эффективность.